Metoda z wieloma parametrami

Czasami metody mają zbyt dużo parametrów przez co wywołanie ich jest niewygodne i może zajmować nawet dwie linie. Oczywiście pierwsza rzecz, którą powinniśmy zrobić jest sprawdzenie czy metoda czasami nie wykonuje zbyt wielu operacji tzn. przestrzega zasadę Single Responsibility.  Jeśli mamy pewność, że metoda przestrzega wszelkie zasady i jest dobrze zaprojektowana wtedy trzeba pomyśleć jak zmniejszyć liczbę parametrów. W poście przedstawię kilka prób uzyskania takiego efektu.

Logiczne wydaje się, utworzenie kontenera, który zawiera wszelkie potrzebne informacje do wykonania takiego zadania. Możemy zacząć od:

class FetchDataArgs
{
    public int Id { get; set; }
    public int Offset { get; set; }
    public int Limit { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
    // inne parametry
}
class Loader
{
    public void LoadData(FetchDataArgs fetchDataArgs)
    {        
    }
}

LoadData zamiast przyjmować wiele parametrów przyjmuje jeden kontener FetchDataArgs. Oczywiście ma to sens tylko w sytuacjach gdy jest takich parametrów np. siedem. Rozwiązanie działa i można teraz wykonać LoadData następująco:

FetchDataArgs fetchDataArgs=new FetchDataArgs();
fetchDataArgs.To = DateTime.Now;
fetchDataArgs.From = DateTime.Now;
// ...

Loader loader=new Loader();
loader.LoadData(fetchDataArgs);

Podejście jednak niezbyt eleganckie. Główną wadą jest możliwość modyfikowania FetchDataArgs w metodzie LoadData. Oznacza to, że wywołana metoda może modyfikować stan obiektu znajdującej się po za nią tzn.:

class Loader
{
    public void LoadData(FetchDataArgs fetchDataArgs)
    {
        fetchDataArgs.From = DateTime.Now;
    }
}

Metoda powinna mieć dostęp tylko do odczytu jeśli chodzi o parametry wejściowe. Metoda wywołana nie może zmieniać stanu metody wywołującej. Można tego uniknąć poprzez zdefiniowanie prywatnych setterow:

class FetchDataArgs
{
    public int Id { get; private set; }
    public int Offset { get; private set; }
    public int Limit { get; private set; }
    public DateTime From { get; private set; }
    public DateTime To { get; private set; }
    // inne parametry
}

Niestety aby takie pola zainicjalizować należałoby zdefiniować konstruktor, który będzie miał znów dużo parametrów (a tego chcemy uniknąć).

Dobry rozwiązaniem jest zdefiniowanie struktury:

struct FetchDataArgs
{
    public int Id;
    public int Offset;
    public int Limit;
    public DateTime From;
    public DateTime To;
    // inne parametry
}

Co takie rozwiązanie gwarantuje? Przekazanie struktury jako parametr spowoduje wykonanie jej kopii a co za tym idzie, niemożliwe będzie modyfikowanie stanu metody wywołującej.

Inne sprytne ale chyba zbyt skomplikowane rozwiązanie to zdefiniowanie interfejsu wyłącznie z getterami tzn.:

internal interface IFetchDataArgs
{
    int Id { get; }
    int Offset { get; }
    int Limit { get; }
    DateTime From { get; }
    DateTime To { get; }
}

class FetchDataArgs : IFetchDataArgs
{
    public int Id { get; set; }
    public int Offset { get; set; }
    public int Limit { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
    // inne parametry
}
class Loader
{
    public void LoadData(IFetchDataArgs fetchDataArgs)
    {
        fetchDataArgs.From = DateTime.Now; // brak settera - niemozliwe
    }
}

Metoda wywołująca tworzy FetchDataArgs zatem ma do do dyspozycji settery. Z kolei LoadData operuje na interfejsie zatem nie może zmodyfikować jego stanu (brak setterow w interfejsie). Oczywiście użytkownik mógłby zrzutować interfejs na obiekt i zmienić jego stan ale to jest już złą praktyką.

Code Review: przekazywanie parametrów

Dziś prosta zasada przekazywania parametrów ale jednak często łamana. Kod:

private static void Display(string[] strings)
{
   foreach (string text in strings)
   {
       Console.WriteLine(text);
   }
}

Powyższa metoda ma za zadanie wyświetlenie wyłącznie elementów. Parametr wejściowy (tablica string’ów) jest zbyt specyficzny i nie pozwala na przekazanie wszystkich zbiorów danych. Na przykład poniższy kod nie skompiluje się:

List<string> elements=new List<string>();
elements.Add("A");
elements.Add("B");

Display(elements);

Z tego względu zawsze należy używać bazowych interfejsów. W tym przypadku jest to IEnumerable:

private static void Display(IEnumerable<string>strings)
{
   foreach (string text in strings)
   {
       Console.WriteLine(text);
   }
}
private static void Main(string[] args)
{
   List<string> elements=new List<string>();
   elements.Add("A");
   elements.Add("B");

   Display(elements);
}

Ta sama zasada dotyczy się wszystkich typów klas – nie tylko kolekcji. Zamiast używać FileStream lepiej użyć bazowej klasy Stream, którą można użyć nie tylko dla zapisu do plików ale również do kompresji, transmisji danych przez sieć  itp.

// zła deklaracja
private static void CopyData(FileStream stream)
{        
}
// zła deklaracja
private static void CopyData(NetworkStream stream)
{
}
// poprawna, generyczna deklaracja
private static void CopyData(Stream stream)
{
}

IEnumerable nie zawsze jednak da się wykorzystać ponieważ np. dana funkcja musi dodać jakieś elementy do kolekcji. IEnumerable czy tablice są kolekcjami tylko do odczytu. BARDZO jednak częstym błędem jest pisanie kodu w następujący sposób:

private static void AddSomeItems(List<string> list)
{ 
}

A co jeśli użytkownik ma swoją własną kolekcję danych np. SortedList? Oczywiście nie będzie możliwe skorzystanie z naszej metody AddSomeItems. Z tego względu zawsze należy używać interfejsów w sygnaturach tzn.:

private static void AddSomeItems(IList<string>list)
{ 
}

W wielu przypadkach również typ zawracany lepiej deklarować jako interfejs:

private static IList<string> CreateList()
{      
}

W taki sposób łatwiej będzie w przyszłości zmienić wewnętrzną implementację CreateList bez łamania wszystkich zależnych bibliotek.

W niektórych jednak przypadkach lepiej zwracać typ specyficzny niż ogólny np.:

private static SqlCommand CreateSqlCommand()
{      
}
private static Command CreateSqlCommand()
{
}

Które rozwiązanie jest bardziej elastyczne? CreateSqlCommand tworzy specyficzne dla bazy Sql Server polecenie. Zwracanie ogólnego Command nie ma sensu.  Dostarczenie użytkownikowi SqlCommand daje mu dużo wyższą elastyczność – może dokonywać specyficznych dla SqlServer operacji. Implementacja wewnętrzna metody nigdy nie zmieni się do tego stopnia, że zwracany będzie inny typ.

Inicjalizacja pól a konstruktory

Przyjrzyjmy się następującemu fragmentowi kodu:

public class SampleClass
{
    private int _value = 10;
}

W rzeczywistości zostanie wygenerowany konstruktor, ustawiający pole _value na 10. Kod IL:

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldc.i4.s 10
    L_0003: stfld int32 SampleClass::_value
    L_0008: ldarg.0 
    L_0009: call instance void [mscorlib]System.Object::.ctor()
    L_000e: nop 
    L_000f: ret 
}

Na razie wszystko wygląda świetnie… Mamy konstruktor który ustawia zmienną na daną wartość a sami nie musimy pisać zbyt dużo kodu (inicjalizacja inline). Z kodu wynika, że najpierw wartość ustawiona jest na 10 a potem dopiero jest wywołany konstruktor klasy bazowej.

Po jakimś czasie jednak okazało się, że należy dodać kilka innych pól oraz dodatkowe konstruktory  tzn.:

public class SampleClass
{
    private int _value1 = 10;
    private int _value2 = 10;
    private int _value3 = 10;
    private int _value4 = 10;

    public SampleClass()
    {
    }
    public SampleClass(int arg1)
    {
    }
    public SampleClass(int arg1, int arg2)
    {
    }

    public SampleClass(int arg1, int arg2, int arg3)
    {
    }
}

Kod IL jednak już nie wygląda tak prosto:

// Konstruktor bezparametrowy
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldc.i4.s 10
    L_0003: stfld int32 SampleClass::_value1
    L_0008: ldarg.0 
    L_0009: ldc.i4.s 10
    L_000b: stfld int32 SampleClass::_value2
    L_0010: ldarg.0 
    L_0011: ldc.i4.s 10
    L_0013: stfld int32 SampleClass::_value3
    L_0018: ldarg.0 
    L_0019: ldc.i4.s 10
    L_001b: stfld int32 SampleClass::_value4
    L_0020: ldarg.0 
    L_0021: call instance void [mscorlib]System.Object::.ctor()
    L_0026: nop 
    L_0027: nop 
    L_0028: nop 
    L_0029: ret 
}
// ctor 2
.method public hidebysig specialname rtspecialname instance void .ctor(int32 arg1) cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldc.i4.s 10
    L_0003: stfld int32 SampleClass::_value1
    L_0008: ldarg.0 
    L_0009: ldc.i4.s 10
    L_000b: stfld int32 SampleClass::_value2
    L_0010: ldarg.0 
    L_0011: ldc.i4.s 10
    L_0013: stfld int32 SampleClass::_value3
    L_0018: ldarg.0 
    L_0019: ldc.i4.s 10
    L_001b: stfld int32 SampleClass::_value4
    L_0020: ldarg.0 
    L_0021: call instance void [mscorlib]System.Object::.ctor()
    L_0026: nop 
    L_0027: nop 
    L_0028: nop 
    L_0029: ret 
}

Niestety nie została dokonana żadna optymalizacja. Dla każdego konstruktora dodano inicjalizacje pól. Zamiast w jednym konstruktorze inicjalizować pola a potem wywoływać go z pozostałych, kod jest duplikowany w każdym z nich. Przy wielu polach i konstruktorach nie jest to optymalne rozwiązanie. Każdy wygenerowany konstruktor składa się z 3 części: inicjalizacja pól, wywołanie konstruktora bazowego a na końcu wykonanie ciała właściwego konstruktora.

Warto się zatem zastanowić nad następującym rozwiązaniem problemu:

public class SampleClass
{
    private int _value1;
    private int _value2;
    private int _value3;
    private int _value4;

    public SampleClass()
    {
        _value1 = 10;
        _value2 = 10;
        _value3 = 10;
        _value4 = 10;
    }
    public SampleClass(int arg1):this()
    {
    }
    public SampleClass(int arg1, int arg2):this()
    {
    }

    public SampleClass(int arg1, int arg2, int arg3):this()
    {
    }
}

Oczywiście nie są to jakieś rzeczywiste problemy, które spowolnią aplikację. Traktować to należy jako ciekawostkę ale uważam, że lepiej znać takie anomalia i pisać zawsze lepszy kod.

Tablice – rzutowanie

W dzisiejszym wpisie chciałbym przyjrzeć się trochę bardziej tablicom i interfejsom jakie implementują. Zaglądając do dokumentacji dowiemy się, że Array implementuje:

[SerializableAttribute]
[ComVisibleAttribute(true)]
public abstract class Array : ICloneable, 
    IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable

IEnumerable nie powinno wydawać się dziwne ponieważ oczekujemy od tablic możliwości dostępu do elementów poprzez foreach:

int[] numbers = new[] { 5, 2, 52, 5 };

foreach(int number in numbers)
{
  Console.WriteLine(number);
}

IList z kolei może być zaskakujące ponieważ kojarzy się ona z modyfikowalną kolekcją danych. W rzeczywistości IList posiada właściwość IsReadOnly określającą czy daną listę można modyfikować. Nic zatem nie stoi na przeszkodzie aby zastosować następujące rzutowanie:

int[] numbers = new[] { 5, 2, 52, 5 };

IList list = numbers;
list.Add(3); // exception here

Kod się skompiluje bez problemu ale w czasie runtime zostanie wyrzucony wyjątek(“Collection was of a fixed size”) ponieważ tablica jest zawsze listą tylko do odczytu. Rozważmy metodę o następującej sygnaturze:

private void DisplayElements(IList elements)
{        
}

Z powyższych rozważań wynika, że następujące wywołania są prawidłowe:

int[] numbersArray = new[] { 5, 2, 52, 5 };
List<int> numbersGenericList=new List<int>();
ArrayList nonGenericList=new ArrayList();

DisplayElements(numbersArray);
DisplayElements(numbersGenericList);
DisplayElements(nonGenericList);

Na pewnym forum, jeden z użytkowników zasugerował, że IList to jedyny interfejs eksponujący dostęp przez indexer tzn.:

int number=numbersArray[5]

Osobiście myślę, że w .NET Framework powinien być dodatkowy interfejs ponieważ fakt, że tablica implementuję listę nie jest dla mnie oczywisty.

To nie koniec niespodzianek…. Okazuje się, że CLR dokonuje pewnych “trików” i w rzeczywistości jest więcej interfejsów. Zróbmy mały eksperyment:

SampleClass[] array=new SampleClass[4];

foreach(Type item in array.GetType().GetInterfaces())
{
    Console.WriteLine(item.ToString());
}

Kod wyświetlający wszystkie implementowane interfejsy zwróci:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[SampleClass]
System.Collections.Generic.ICollection`1[SampleClass]
System.Collections.Generic.IEnumerable`1[SampleClass]

Niespodzianką są generyczne implementacje. Zgodnie z MSDN każda tablica implementuje wyłącznie niegeneryczne wersje (tzn. IList, ICollection). CLR jednak dynamicznie implementuje te interfejsy jeśli do czynienia ma z tablicą jednowymiarową. Dla tablic wielowymiarowych tablica implementuje wyłącznie niegeneryczne wersje:

SampleClass[,] array=new SampleClass[4,5];

foreach(Type item in array.GetType().GetInterfaces())
{
    Debug.WriteLine(item.ToString());
}
Output:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable

Na tym etapie wiemy, że tablice jednowymiarowe implementują zarówno generyczne jak i niegeneryczne interfejsy, z kolei wielowymiarowe, wyłącznie niegeneryczne. Przyjrzyjmy się dla odmiany, jak wygląda sprawa z typami prostymi:

int[] array=new int[5];

foreach(Type item in array.GetType().GetInterfaces())
{
  Debug.WriteLine(item.ToString());
}
Output:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

Na pierwszy rzut oka wygląda to tak samo ale jednak w przypadku typu referencyjnego możliwe jest rzutowanie do typów bazowych:

// OK
SampleClass[] array = new SampleClass[5];    
IList<object> array1 = array;
IList<SampleBaseClass> array2 = array;
// BLAD - tylko tablica dynamicznie implementuje bazowe interfejsy
List<SampleClass> list=new List<SampleClass>();
List<object> list1 = list;

W przypadku typów prostych jest to niemożliwe – powyższe rzutowania zawsze zakończą się błędem kompilacji. Z powyższych rozważań wynika, że tablica może zostać przekazana do metod o naprawdę różnorakich sygnaturach. Niestety wynika z tego jeszcze jedna przykra sprawa – użytkownik poprzez przekazanie tablicy do metody, która spodziewa się listy, może spowodować błąd jeśli taka metoda oczekuje  modyfikowalnej kolekcji.

Boxing, unboxing–test

Dziś mały eksperyment. Chciałbym pokazać jaki jest faktycznie spadek wydajności jeśli zachodzi potrzeba boxing’u i unboxing’u. Wiele o tym ostatnio pisałem ale nie pokazałem najważniejszego – liczb opisujących wydajność.  Na początek porównanie boxing z unboxing:

 private static void TestBoxingAndUnboxing()
{
   object boxedValue = null;

   // boxing
   Stopwatch stopwatch = Stopwatch.StartNew();
   for (int i = 0; i < Iterations; i++)
   {
       boxedValue = i;
   }
   stopwatch.Stop();
   Console.WriteLine("Boxing:{0}",stopwatch.ElapsedMilliseconds);
   
   // unboxing
   stopwatch = Stopwatch.StartNew();
   for (int i = 0; i < Iterations; i++)
   {
       int unboxed = (int) boxedValue;
   }
   stopwatch.Stop();
   Console.WriteLine("Unboxing:{0}", stopwatch.ElapsedMilliseconds);
}

Wynik to: boxing-1178, unboxing:128. Nie powinno to dziwić – tak jak pisałem kiedyś boxing jest dużo wolniejszy. Ponadto proszę zwrócić uwagę, że przy boxingu potem GC musi zwolnić stworzone obiekty co jest dodatkową utratą wydajności.

Kolejny test to dodawanie elementów do listy. Pierwsza lista (slowList) wymaga boxing’u przy dodawaniu elementu oraz unboxing’u przy każdym czytaniu. Druga kolekcja za to przyjmuje właściwy typ a nie ogólny object.

private static void TestLists()
{
   List<int> fastList=new List<int>();
   List<object> slowList=new List<object>();

   // boxing
   Stopwatch stopwatch = Stopwatch.StartNew();
   for (int i = 0; i < Iterations; i++)
   {
       slowList.Add(i);
       int read = (int)slowList[i];
   }
   stopwatch.Stop();
   Console.WriteLine("Boxing:{0}",stopwatch.ElapsedMilliseconds);
           
   stopwatch = Stopwatch.StartNew();
   for (int i = 0; i < Iterations; i++)
   {
       fastList.Add(i);
       int read = fastList[i];
   }
   stopwatch.Stop();
   Console.WriteLine("Fast list:{0}", stopwatch.ElapsedMilliseconds);
}

Wynik: fastList-235, slowList-2065. Jak pokazuje prosty eksperyment różnica jest ogromna i unikanie boxing’u to nie tylko czytelniejszy kod ale również dużo szybszy.

Statyczne konstruktory–wydajność część II

W poprzednim poście napisałem kilka słów o dwóch sposobach wywoływania konstruktorów statycznych. Dziś chciałbym pokazać, że faktycznie ma to wpływ na wydajność. Rozważmy następujący przykład:

public class BeforeInitSementics
{
    public static int Value = 10;
}
public class PreciseSemantics
{
    public static int Value;
    static PreciseSemantics()
    {
        Value = 20;
    }
}
internal class Program
{
    private const int Iterations = 100000000;

    private static void Test1()
    {
        // Precise
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            PreciseSemantics.Value = 10;
        }
        stopwatch.Stop();
        Console.WriteLine("Precise:{0}",stopwatch.ElapsedMilliseconds);
        
        // Before-init
        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            BeforeInitSementics.Value = 10;
        }
        stopwatch.Stop();
        Console.WriteLine("Before-init:{0}", stopwatch.ElapsedMilliseconds);
    }
    private static void Main(string[] args)
    {
        Test1();
    }
}

W trybie Release na moim komputerze uzyskałem wynik 567 oraz 152. Różnica jest więc dość znacząca (kilkukrotna). Jak wiemy z poprzedniego wpisu, semantyka BeforeInit pozwala wyemitować wywołanie konstruktora w jakimkolwiek momencie. Kompilator jest więc na tyle sprytny, że emituje to przed pętlą for (tak więc cała logika będzie wykonana tylko raz). W przypadku semantyki precise musi to nastąpić linię przed dostępem do pierwszego pola. Z tego wynika, że podejście precise będzie dużo wolniejsze – w każdej iteracji musi zostać wykonana pewna logika taka jak synchronizacja, sprawdzenie czy konstruktor został już wywołany itp.

Rozważmy kolejny przykład:

public class BeforeInitSementics
{
    public static int Value = 10;
}
public class PreciseSemantics
{
    public static int Value;
    static PreciseSemantics()
    {
        Value = 20;
    }
}
internal class Program
{
    private const int Iterations = 100000000;

    private static void Test1()
    {
        // Precise
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            PreciseSemantics.Value = 10;
        }
        stopwatch.Stop();
        Console.WriteLine("Precise:{0}",stopwatch.ElapsedMilliseconds);
        
        // Before-init
        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            BeforeInitSementics.Value = 10;
        }
        stopwatch.Stop();
        Console.WriteLine("Before-init:{0}", stopwatch.ElapsedMilliseconds);
    }
    private static void Test2()
    {
        // Precise
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            PreciseSemantics.Value = 10;
        }
        stopwatch.Stop();
        Console.WriteLine("Precise:{0}", stopwatch.ElapsedMilliseconds);

        // Before-init
        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < Iterations; i++)
        {
            BeforeInitSementics.Value = 10;
        }
        stopwatch.Stop();
        Console.WriteLine("Before-init:{0}", stopwatch.ElapsedMilliseconds);
    }
    private static void Main(string[] args)
    {
        Test1();
        Test2();
    }
}

Test 1 wydrukuje takie same liczby jak w kroku I (567,151). Test2 z kolei wyświetli dwie podobne wartości (np. 151,151). Dlaczego? Wynika to z zasady emitowania wywołań do konstruktora. Kompilując metodę, kompilator sprawdza czy konstruktor został już wywołany. Jeśli tak, nie trzeba już emitować żadnego kodu JIT wywołującego konstruktor statyczny. W Test1 kod taki został wyemitowany ponieważ na tamtym etapie nie było jeszcze żadnego wywołania. Wyemitowany kod to m.in. synchronizacja, sprawdzenie czy konstruktor został już wywołany itp. W Test2 jest pewność, że konstruktor statyczny został wykonany (w Test1), zatem nie trzeba emitować jakiejkolwiek logiki. W takim przypadku Before i Precise zachowują się dokładnie tak samo.

Statyczne konstruktory–wydajność

Konstruktory statyczne zwykłe służą do inicjalizowania pól statycznych lub walidacji typów generycznych np.:

class Generic<T> where T: struct
{
    static Generic() {
        if (!typeof(T).IsEnum) {
            throw new ArgumentException("T must be an enum");
        }
    }
}

Dobrą informacją jest fakt, że statyczne konsturktory są thread-safe co ułatwia implementację pewnych wzorów projektowych. Domyślnie statyczne konstuktory są generowane wyłącznie gdy dana klasa posiada jakieś pola statyczne. Pytanie brzmi: kiedy konstruktor statyczny jest wywoływany?

CLR gwarantuje tylko, że:

  1. operacje w konstruktorze są thread-safe,
  2. konstruktor zostanie wywołany przed dostępem do pierwszego pola danej klasy. CLR nie określa kiedy dokładnie do wywołania dojdzie.

Punkt pierwszy powinien być jasny ale drugi wymaga z pewnością rozwinięcia. Istnieją dwa możliwe przypadki:

  1. Konstruktor może zostać wywołany od razu przed dostępem do danego pola (precise semantics).
  2. Konstruktor może być wywołany w jakimkolwiek momencie  przed dostępem do pola (before-init-semantics). Zgodnie z tym podejściem, konstruktor może zostać wywołany DUŻO wcześniej niż tego byśmy spodziewali się.

Preferowane jest drugie podejście ponieważ daje więcej swobody CLR i dzięki temu wywołanie może zostać zoptymalizowane. Programiści nie mogą jawnie określić sposobu wywołania konstruktora ale istnieją pewne zasady, które ułatwią zrozumienie jak CLR wybiera odpowiednią semantykę. Przykład:

public class BeforeInitSemantics
{
   public static int Value = 10;
}
public class PreciseSemantics
{
   public static int Value;
   static PreciseSemantics()
   {
       Value = 20;
   }    
}

Pierwsza klasa zostanie oznaczona jako before-init ponieważ konstruktor nie jest jawnie zdefiniowany. Przez to zostanie wygenerowany automatycznie (to właśnie w nim Value będzie ustawiona na 10). Taka sytuacja jest najoptymalniejsza ponieważ CLR sam zadba o jak najwyższą wydajność. Można o tym przekonać się zaglądając do IL:

.class auto ansi nested public beforefieldinit BeforeInitSementics
    extends [mscorlib]System.Object
{
    .method private hidebysig specialname rtspecialname static void .cctor() cil managed
    {
    }

    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }
    .field public static int32 Value
}

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: call instance void [mscorlib]System.Object::.ctor()
    L_0006: ret 
}

Pierwsza klasa została oznaczona flagą beforefieldinit. Reguła jest prosta i wszystkie klasy z jawnymi konstruktorami nie będą opatrzone flagą beforefieldinit. Gdyby umożliwić wywołanie jawnych konstruktorów, które zawierają nieznana logikę, mogłoby to spowodować efekty uboczne. W pełni jest to zrozumiałe, ale  czy nie byłoby lepiej gdyby użytkownicy samu również mogli decydować o tym? Programista, który zaimplementował dany konstruktor zdaje sobie sprawę czy logika w nim zawarta może przynieść efekty uboczne.

W następnym poście pokażę, że różnice w wydajności mogą być dość znaczne i dla pewnych systemów ma to znaczenie.

Prawo Demeter

Dziś w poście o kolejnej regule pisania dobrego kodu. Prawo Demeter mówi o relacjach między klasami a konkretnie o tym z jakimi obiektami dana klasa może komunikować się. W skrócie, prawo nakazuje odwoływanie się tylko do “bliskich przyjaciół” czyli obiektów, które są bardzo znane danemu obiektowi. Brzmi to trochę abstrakcyjnie dlatego przenieśmy to na  świat programowania obiektowego. Każda metoda obiektu A może wywoływać wyłącznie metody następujących obiektów:

1. obiektów przekazanych jako parametr wejściowy np.: 

private void Method(Employee employee)
{
   int salary = employee.CalculateSalary();
}

2. obiektów, które stanowią bezpośrednie pole\właściwość danej klasy np:  

class MoneyManager
{
    private Employee _employee;
    
    private void Update()
    {
        _employee.GivePayRise(); // OK
    }
}

3. obiektów dostępnych globalnie (np. Singleton):

private void Method(Employee employee)
{
   Employee.Instance.GivePayRise(); // łamie inne zasady ale zgodne z Demeter.
}

4. obiektów stworzonych przez daną metodę:

private void Update()
{
    Employee employee= new Developer();
   int salary = employee.CalculateSalary();
}

Powyższe przykłady pokazują scenariusze zgodne z prawem Demeter. Aby w pełni zrozumieć regułę warto pokazać kilka przykładów łamiących zasadę. Na przykład:

private void Method()
{
    Parent.Parent.Update();
}

Odwołanie się do Parent jest w porządku ponieważ stanowi on składową klasy. Niedozwolone jednak jest drugie odwołanie bo nie jest to bezpośredni “przyjaciel” danej klasy.  W świecie obiektowym, Demeter można uprościć do reguły pojedynczej kropki tzn. jeśli dana linijka kodu zawiera więcej niż jedną kropkę to znaczy, że kod odwołuje się do zbyt dalekich obiektów.

Dzięki przestrzeganiu Demeter, metoda odwołuje się wyłącznie do obiektów, które dobrze zna pod względem wewnętrznej struktury. Pisanie kodu spełniającego prawo Demeter, może polepszyć modyfikowalność i elastyczność projektu poprzez zmieszczenie liczby referencji. W końcu gdy dana klasa odwołuje się poprzez pięć obiektów do szóstego wtedy analiza błędów i testowanie jest utrudnione. Wadą podejścia jest możliwość powstawia wielu metod, które służą wyłącznie jako wrappery tzn. delegują wykonanie danego kodu. Aby to zobrazować rozważmy następujący kod łamiący Demeter:

class EmployeeInfo
{
    public int Salary{get;set;}
}
class Employee
{
    public EmployeeInfo Info{get;set;}
}
class CompanyManager
{
    private Company _company;
    
    public void UpdateSalaries()
    {
        foreach(Employee employee in _company.Employees)
        {
            employee.Info.Salary+=3;
        }
    }

}

Kod nie spełnia reguły bo zachodzi tutaj relacja Company->Employee->EmployeeInfo. W celu naprawienia problemu wystarczy stworzyć dodatkową metodę:

class EmployeeInfo
{
    public int Salary{get;set;}
}
class Employee
{
    public EmployeeInfo Info{get;private set;}
    public GivePayRise(int value)
    {
        Info.Salary+=value;
    }
}
class CompanyManager
{
    private Company _company;
    
    public void UpdateSalaries()
    {
        foreach(Employee employee in _company.Employees)
        {
            employee.GivePayRise(3);
        }
    }

}

W powyższym przypadku, dodanie metody ma sens, ale  w wielu przypadkach jest to po prostu zaśmiecanie kodu. Podsumowując, dzięki Demeter klasa nie musi mieć szerokiej wiedzy o innych obiektach bo odwołuje się wyłącznie do dobrze znanych klas – mniejsza wiedza to mniejsze ryzyko wystąpienia błędu.

Pure methods

Nie wiem, czy istnieje jakieś bardziej oficjalne tłumaczenie pure methods,więc aby uniknąć śmiesznych nazw przez resztę postu będę posługiwał się nazwą angielską. Nie chodzi mi o metody czysto wirtualne, które są powszechnie znane.  W .NET istnieje atrybut PureAttribute, którym możemy oznaczać metody pure. Warto zacząć jednak od czystej definicji, czym jest metoda pure?

W skrócie metoda pure nie zmienia stanu obiektu a jedynie operuje na parametrach wejściowych. Aby spełniać wymogi metod pure należy:

  1. Parametry wejściowe nie mogą być zmieniane. Muszą stanowić one wyłącznie dane na których się operuje w celu wygenerowania wyjścia.
  2. Jeśli parametry wejściowe to typy referencyjne, nie można zmieniać ich stanu (bezpośrednio przez zmianę właściwości lub pośrednio przez wywołanie metod).
  3. Nie można zmieniać stanów innych klas np. po przez ustawienie pola statycznego.

Co daje takie podejście? Przede wszystkim metody zaimplementowane w ten sposób są bezpieczniejsze i nie przynoszą efektów ubocznych. Z punktu widzenia wielowątkowości łatwiej je wdrożyć ponieważ każda operuje tylko na lokalnym stosie a nie na współdzielonym stanie obiektu. Z łatwością można je wywoływać w jakiejkolwiek kolejności i nie przyniesie to nieoczekiwanego rezultatu. Testy jednostkowe ograniczają się do do przetestowania różnych kombinacji parametrów wejściowych oraz wyjścia.

Wszelkie zapytania np. LINQ są doskonałym przykładem na użycie metod pure. Implementując własne rozszerzenia (extension methods) niedopuszczalne jest aby metoda modyfikowała parametry wejściowe lub stan obiektu. Modyfikacja stanu jest powodem wielu błędów. Autorzy zbyt często zakładają, że użytkownicy wywołają metody w odpowiedniej kolejności. W .NET istnieje atrybut Pure do identyfikowania takich metod:

class Manager
{
    [Pure]
    public void CalculateSomething(int x,int y)
    {        
    }
}

Należy zaznaczyć, że kompilator nie sprawdza metody pod kątem czystości i zmiana stanu wciąż jest możliwa:

class Manager
{
    private int _x;
    [Pure]
    public void CalculateSomething(int x,int y)
    {
        _x = x;
    }
}

Atrybut ma znaczenie wyłącznie informacyjne.  To na programiście spada odpowiedzialność zagwarantowania poprawności metody. Atrybut Pure jest częścią tzw. kontraktów (code contracts) ale o tym mam nadzieję, uda mi się przygotować osobne wpisy. Kiedyś napisałem już jeden wprowadzający post do kontraktów tutaj.

Ignorowanie pól klasy bazowej podczas serializacji

Klasa XmlSerializer służy do serializacji całych klas. Za pomocą atrybutu XmlIgnore można wykluczyć właściwości, których nie chcemy zapisywać. Problem pojawia się gdy w klasie pochodnej chcemy wykluczyć jakieś pola z klasy bazowej tj.:

class Employee
{
    public string FirstName { get; set; }
}
class Manager:Employee
{
    //...
}

Serializaując Employee chcemy zapisać FirstName. Z kolei serializując Manger nie chcemy już tego robić. Jak uzyskać taki efekt? Dodając atrybut XmlIgnore w Employee  spowodujemy, że dla wszystkich instancji pole będzie ignorowane.

XmlSerializer wspiera jednak pola bool, które określają czy należy dane pole zapisywać czy nie. Na przykład, aby zignorować FirstName w Manager wystarczy:

class Employee
{
    [XmlIgnore]
    public bool FirstNameSpecified = true;

    public string FirstName { get; set; }
}
class Manager:Employee
{
    public Manager()
    {
        FirstNameSpecified = false;
    }
    //...
}

Wystarczy stworzyć pole, według wzorca NazwaPolaSpecified. XmlSerializer rozpoznaje takie zmienne i w przypadku gdy mają wartość false, serializacja jest ignorowana.  Pola Specified dają większą kontrolę ponieważ można decydować o serialziacji w czasie wykonywania programu a nie kompilacji. Ważne aby oznaczyć Specified atrybutem XmlIgnore ponieważ jest to pole publiczne i bez tego zostałoby zapisanie tak jak inne publiczne dane.