Powtarzanie kodu w kilku miejscach zwykle jest sygnałem zaniedbania i nie powinno mieć miejsca. Nawet (a może: w szczególności!) gdy kod ten jest prosty, głupi, niewymagający myślenia i będący efektem tzw. clipboard inheritance (ctrl+c, ctrl+v).
Tyczy się to również konstruktorów klas. Tą część kodu łatwo jest przegapić, bo wszelakie ułatwiacze umożliwiają automatyczne ich wygenerowanie. A co jeśli mamy ich kilka? Poniższy przykład obrazuje stan, do którego NIE CHCEMY doprowadzić:
1: public class User 2: { 3: public int Id { get; set; } 4: public string FirstName { get; set; } 5: public string LastName { get; set; } 6: public IList<string> Roles { get; set; } 7: 8: public User() 9: { 10: Id = 0; 11: FirstName = string.Empty; 12: LastName = string.Empty; 13: Roles = new List<string>(); 14: } 15: 16: public User(int id) 17: { 18: Id = id; 19: FirstName = string.Empty; 20: LastName = string.Empty; 21: Roles = new List<string>(); 22: } 23: 24: public User(int id, string firstName, string lastName) 25: { 26: Id = id; 27: FirstName = firstName; 28: LastName = lastName; 29: Roles = new List<string>(); 30: } 31: 32: public User(int id, string firstName, string lastName, IList<string> roles) 33: { 34: Id = id; 35: FirstName = firstName; 36: LastName = lastName; 37: Roles = roles; 38: } 39: }
Poradzić sobie z tym syfem można w bardzo prosty sposób: wystarczy wykorzystać (wzorzec? konstrukcję? praktykę?) constructor chaining. Całą "logikę" umieszczamy w konstruktorze posiadającym największą liczbę parametrów, po czym wywołania "uboższych" przeciążeń (:)) delegujemy do tego jedynego, wspieranego ultimate ctor:
1: public class User 2: { 3: private const int EMPTY_ID = 0; 4: private const string EMPTY_FIRST_NAME = ""; 5: private const string EMPTY_LAST_NAME = ""; 6: 7: public int Id { get; set; } 8: public string FirstName { get; set; } 9: public string LastName { get; set; } 10: public IList<string> Roles { get; set; } 11: 12: public User() 13: : this(EMPTY_ID, EMPTY_FIRST_NAME, EMPTY_LAST_NAME, new List<string>()) 14: { 15: } 16: 17: public User(int id) 18: : this(id, EMPTY_FIRST_NAME, EMPTY_LAST_NAME, new List<string>()) 19: { 20: } 21: 22: public User(int id, string firstName, string lastName) 23: : this(id, firstName, lastName, new List<string>()) 24: { 25: } 26: 27: public User(int id, string firstName, string lastName, IList<string> roles) 28: { 29: Id = id; 30: FirstName = firstName; 31: LastName = lastName; 32: Roles = roles; 33: } 34: }
W tym przykładzie poszedłem o krok dalej definiując zbiór stałych, ale w większości przypadków można pominąć tą czynność.
Nie wiem dlaczego, ale wielokrotnie spotykałem się z ignorancją programistów w tym zakresie. Przecież tak banalne rozwiązanie może wyeliminować tak wiele zbędnego kodu. A gdy dodamy do tego wywoływanie konstruktorów klas bazowych i nadal będziemy ignorować możliwość wzajemnego wywoływania konstruktorów to mamy na własne życzenie gotowy mega-fe mega-bajzel.
Dodać w temacie można, że dostępne w C# 4.0 parametry opcjonalne pozwolą na jeszcze łatwiejsze pozbycie się tego problemu.
a ja jestem przeciwny takiemu tworzeniu obiektów.
czyli zamiast pustego konstrutkora i tych innych IMO powinno byc:
var user = new UserBuilder()
.WithEmptyId()
.WithName("Jacek")
.WithDefaultRoles();
// to tylko przyklad
Ogolnie jezeli klasa juz potrzebuje wiecej niz 2 konstrutkorow to ja bym tworzyl Builder. Daje Ci to mozliwosc zarzadzania tym jakby sie potem moglo wszystko rozrosnac. Poza tym, dzieki builderowi tez nie powtarzasz kodu.
Dodatkowo w przykladie uzyles obiektu User -> zadko sie zdarza ze jest to obiekt, ktorego mozna dowolnie "zmieniac". Jego dane sa immutable a IMO kazda zmiania powinna byc gdzies odnotowana – przynajmniej ja staram sie zawsze takie podejscie zastosowac :)
Dodatkowo MS juz zapowiedzial a na grupach dyskusyjnych toczyla sie dyskusja na temat wlasnie zastapienia "paru" konstruktorow za pomoca Optional/Named Parameters. Ogolnie nie po to powstala ta technologia i wykorzystywanie tego nagminnie moze prowadzic do niezlego zamieszania w kodzie. Tutaj akurat wszyscy byli zgodni. Jak chcesz to podesle Ci spakowana taka dyskusje (IMO domyslasz sie gdzie sie toczyla ;)).
Gutek
@Gutek:
Co do buildera to zgadzam sie ze jest fajny, ale jest tez bardziej czasochlonny niz po prostu konstruktor (a szczegolnie jego wersja ‘fluent’).
User z kolei to tylko przyklad, akurat mialem w VS taka klase otwarta z jakiegos poprzedniego posta:).
Dyskusja: domyślam się gdzie byla, ale nawet jak podeslesz to pewnie i tak nie bedzie mi sie chcialo czytac z tego samego wzgledu z ktorego nie czytam ‘na zywo’:). Jestem pewny ze przesadzanie z default params moze prowadzic do balaganu, ale dopiero w praktyce okaze sie jak to naprawde jest. Poki co mozna jedynie gdybac – tak jak mialo to miejsce w przypadku ‘var’ z C# 3.0.
Generalnie chodzi o to zeby nie powtarzac kodu, a zawarcie wszystkiego w jednym konstruktorze jest na to sposobem – oczywiscie nie jedynym.
kurcze, uzywam takich rozwiazan od dawna i nie wiedzialem, ze one są "chained" :)
podobne rozwiazania uzywam w normalnych funkcjach przeladowanych
gutek – na pewno masz racje z builderami ale to oczywiscie wszystko zalezy od kontekstu
squash
cos o "builderach" tak jakby:
http://www.lostechies.com/blogs/gabrielschenker/archive/2010/01/02/fluent-silverlight-implementing-a-fluent-api.aspx
Gutek