Jak ulepszyć wzorzec konstruktora Blocha, aby był bardziej odpowiedni do stosowania w klasach o dużej rozszerzalności

34

Duży wpływ wywarła na mnie książka Joshua Bloch Effective Java (2. wydanie), prawdopodobnie bardziej niż jakakolwiek inna książka programowa, którą przeczytałem. W szczególności jego Wzorzec Budowniczego (punkt 2) miał największy efekt.

Mimo że konstruktor Blocha posunął mnie znacznie dalej w ciągu kilku miesięcy niż w ciągu ostatnich dziesięciu lat programowania, wciąż uderzam w tę samą ścianę: poszerzanie zajęć za pomocą powracających łańcuchów metod jest w najlepszym wypadku zniechęcające, aw najgorszym koszmarem --especially gdy generyczne wchodzą w grę, a zwłaszcza z autoreferencyjna generycznych (takich jak Comparable<T extends Comparable<T>>).

Mam dwie podstawowe potrzeby, z których tylko drugą chciałbym się skupić w tym pytaniu:

  1. Pierwszym problemem jest „jak współdzielić łańcuchy metod zwracających się bez konieczności ponownego ich wdrażania w każdej… pojedynczej… klasie?” Dla tych, którzy mogą być ciekawi, omówiłem tę część na dole tego postu, ale nie na tym chcę się tutaj skupić.

  2. Drugi problem, o który proszę o komentarz, brzmi: „w jaki sposób mogę zaimplementować program budujący w klasach, które same mają być rozszerzone o wiele innych klas?” Rozszerzenie klasy za pomocą konstruktora jest oczywiście trudniejsze niż rozszerzenie klasy bez. Rozszerzanie klasy, która ma konstruktor, który również implementuje Needable, a zatem ma z nią związane znaczące elementy ogólne , jest niewygodne.

Oto moje pytanie: jak mogę ulepszyć (jak to nazywam) Konstruktor Blocha, aby móc swobodnie dołączać konstruktora do dowolnej klasy - nawet jeśli ta klasa ma być „klasą podstawową”, która może być rozszerzone i rozszerzone wiele razy - bez zniechęcania przyszłego użytkownika lub użytkowników mojej biblioteki , z powodu dodatkowego bagażu, który narzuca im konstruktor (i jego potencjalne produkty generyczne)?


Dodatek
Moje pytanie koncentruje się na części 2 powyżej, ale chciałem trochę rozwinąć problem pierwszy, w tym sposób, w jaki sobie z nim poradziłem:

Pierwszym problemem jest „jak współdzielić łańcuchy metod zwracających się bez konieczności ponownego ich wdrażania w każdej… pojedynczej… klasie?” Nie ma to na celu zapobiec rozszerzaniu klas przed koniecznością ponownej implementacji tych łańcuchów, które oczywiście muszą - raczej, w jaki sposób zapobiegać podklasom , które chcą skorzystać z tych łańcuchów metod, przed koniecznością ponownego -implement każde samodzielne powrocie funkcji, aby ich użytkownicy, aby móc z nich skorzystać? W tym celu stworzyłem potrzebną do tego potrzebną konstrukcję, w której wydrukuję szkielety interfejsu i na razie zostawię to. Działa to dla mnie dobrze (ten projekt był latami w tworzeniu ... najtrudniej było uniknąć zależności cyklicznych):

public interface Chainable  {  
    Chainable chainID(boolean b_setStatic, Object o_id);  
    Object getChainID();  
    Object getStaticChainID();  
}
public interface Needable<O,R extends Needer> extends Chainable  {
    boolean isAvailableToNeeder();
    Needable<O,R> startConfigReturnNeedable(R n_eeder);
    R getActiveNeeder();
    boolean isNeededUsable();
    R endCfg();
}
public interface Needer  {
    void startConfig(Class<?> cls_needed);
    boolean isConfigActive();
    Class getNeededType();
    void neeadableSetsNeeded(Object o_fullyConfigured);
}
aliteralmind
źródło

Odpowiedzi:

21

Stworzyłem coś, co jest dla mnie dużym ulepszeniem w stosunku do wzorca budowniczego Josha Blocha. Nie mówiąc w żaden sposób, że jest „lepszy”, tylko że w bardzo specyficznej sytuacji ma pewne zalety - największą jest to, że oddziela budowniczego od klasy, która ma być budowana.

Dokładnie udokumentowałem tę alternatywę poniżej, którą nazywam Wzorcem Niewidomych.


Wzorzec projektu: niewidomy budowniczy

Jako alternatywa dla Wzorca budowniczego Joshua Blocha (pozycja 2 w Efektywnej Javie, wydanie 2) stworzyłem coś, co nazywam „Wzorcem niewidomych”, który ma wiele zalet Budowniczego Blocha i, oprócz jednej postaci, jest używany dokładnie w ten sam sposób. Niewidomi budowniczowie mają tę przewagę

  • oddzielenie konstruktora od otaczającej go klasy, eliminując cykliczną zależność,
  • znacznie zmniejsza rozmiar kodu źródłowego (co już nie jest ) klasy zamykającej, oraz
  • umożliwia rozszerzenie ToBeBuiltklasy bez konieczności rozszerzania jej konstruktora .

W tej dokumentacji będę się odnosił do budowanej klasy jako do ToBeBuiltklasy „ ”.

Klasa zaimplementowana w programie Bloch Builder

Konstruktor Bloch jest public static classzawarty w klasie, którą buduje. Przykład:

UserConfig klasy publicznej {
   prywatny końcowy ciąg sName;
   prywatny finał iAge;
   prywatny końcowy ciąg sFavColor;
   public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR
      //transfer
         próbować {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            zgłosić nowy wyjątek NullPointerException („uc_c”);
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // WALIDUJ WSZYSTKIE POLA TUTAJ
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
   //builder...START
   publiczna klasa statyczna Cfg {
      prywatny ciąg sName;
      prywatny int iAge;
      private String sFavColor;
      public Cfg (String s_name) {
         sName = nazwa_s;
      }
      // sety powracające ... START
         publiczny wiek Cfg (int i_age) {
            iAge = i_age;
            zwróć to;
         }
         public Cfg favouriteColor (String s_color) {
            sFavColor = s_color;
            zwróć to;
         }
      // ustawiacze samowracające ... KONIEC
      public UserConfig build () {
         return (new UserConfig (this));
      }
   }
   //builder...END
}

Tworzenie instancji klasy za pomocą programu Bloch Builder

UserConfig uc = new UserConfig.Cfg („Kermit”). Age (50). FavoriteColor („zielony”). Build ();

Ta sama klasa, zaimplementowana jako Blind Builder

Kreator niewidomych składa się z trzech części, z których każda znajduje się w osobnym pliku kodu źródłowego:

  1. ToBeBuiltKlasy (w tym przykładzie: UserConfig)
  2. Jego Fieldableinterfejs „ ”
  3. Budowniczy

1. Klasa do zbudowania

Klasa, która ma zostać zbudowana, przyjmuje swój Fieldableinterfejs jako jedyny parametr konstruktora. Konstruktor ustawia z niego wszystkie pola wewnętrzne i sprawdza każde z nich. Co najważniejsze, ta ToBeBuiltklasa nie ma wiedzy o swoim konstruktorze.

UserConfig klasy publicznej {
   prywatny końcowy ciąg sName;
   prywatny finał iAge;
   prywatny końcowy ciąg sFavColor;
    public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      //transfer
         próbować {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            wrzuć nowy wyjątek NullPointerException („uc_f”);
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // WALIDUJ WSZYSTKIE POLA TUTAJ
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
}

Jak zauważył jeden inteligentny komentator (który w niewytłumaczalny sposób usunął swoją odpowiedź), jeśli ToBeBuiltklasa również ją implementuje Fieldable, jej jedyny w swoim rodzaju konstruktor może być używany zarówno jako konstruktor główny, jak i konstruktor kopiujący (wadą jest to, że pola są zawsze sprawdzane, chociaż wiadomo, że pola w oryginale ToBeBuiltsą prawidłowe).

2. Interfejs „ Fieldable

Interfejs polowy jest „pomostem” między ToBeBuiltklasą a jej konstruktorem, definiując wszystkie pola niezbędne do zbudowania obiektu. Ten interfejs jest wymagany przez ToBeBuiltkonstruktora klas i jest implementowany przez konstruktora. Ponieważ interfejs ten może być zaimplementowany przez klasy inne niż konstruktor, każda klasa może łatwo utworzyć instancję ToBeBuiltklasy, bez konieczności korzystania z jej konstruktora. Ułatwia to także rozszerzanie ToBeBuiltklasy, gdy rozszerzanie jej konstruktora nie jest pożądane ani konieczne.

Jak opisano w poniższej sekcji, w ogóle nie dokumentuję funkcji tego interfejsu.

interfejs publiczny UserConfig_Fieldable {
   Ciąg getName ();
   int getAge ();
   Ciąg getFavoriteColor ();
}

3. Konstruktor

Konstruktor implementuje Fieldableklasę. W ogóle nie sprawdza poprawności i aby podkreślić ten fakt, wszystkie jego pola są publiczne i można je modyfikować. Chociaż ta publiczna dostępność nie jest wymagana, wolę ją i polecam, ponieważ wzmacnia to fakt, że sprawdzanie poprawności nie następuje, dopóki nie ToBeBuiltzostanie wywołany konstruktor. Jest to ważne, dlatego, że jest możliwe na inny wątek manipulować wypełniacz ponadto, przed przekazaniem go w ToBeBuilt„s konstruktora. Jedynym sposobem na zagwarantowanie poprawności pól - zakładając, że konstruktor nie może w jakiś sposób „zablokować” swojego stanu - jest ToBeBuiltsprawdzenie klasy przez klasę.

Wreszcie, podobnie jak w przypadku Fieldableinterfejsu, nie dokumentuję żadnego z jego modułów pobierających.

klasa publiczna UserConfig_Cfg implementuje UserConfig_Fieldable {
   public String sName;
   public int iAge;
    public String sFavColor;
    public UserConfig_Cfg (String s_name) {
       sName = nazwa_s;
    }
    // sety powracające ... START
       public UserConfig_Cfg age (int i_age) {
          iAge = i_age;
          zwróć to;
       }
       public UserConfig_Cfg favoriteColor (String s_color) {
          sFavColor = s_color;
          zwróć to;
       }
    // ustawiacze samowracające ... KONIEC
    //getters...START
       ciąg publiczny getName () {
          return sName;
       }
       public int getAge () {
          zwróć iAge;
       }
       public String getFavoriteColor () {
          return sFavColor;
       }
    //getters...END
    public UserConfig build () {
       return (new UserConfig (this));
    }
}

Tworzenie instancji klasy za pomocą programu Blind Builder

UserConfig uc = new UserConfig_Cfg („Kermit”). Age (50) .favoriteColor („zielony”). Build ();

Jedyną różnicą jest „ UserConfig_Cfg” zamiast „ UserConfig.Cfg

Notatki

Niedogodności:

  • Niewidomi budowniczowie nie mogą uzyskać dostępu do prywatnych członków swojej ToBeBuiltklasy,
  • Są bardziej gadatliwi, ponieważ narzędzia pobierające są teraz wymagane zarówno w kreatorze, jak iw interfejsie.
  • Wszystko dla jednej klasy nie jest już w jednym miejscu .

Kompilowanie Blind Buildera jest proste:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

FieldableInterfejs jest całkowicie opcjonalne

W przypadku ToBeBuiltklasy z kilkoma wymaganymi polami - takiej jak UserConfigprzykładowa klasa, konstruktorem może być po prostu

public UserConfig (String s_name, int i_age, String s_favColor) {

I wezwał konstruktora z

public UserConfig build () {
   return (new UserConfig (getName (), getAge (), getFavoriteColor ()));
}

Lub nawet całkowicie eliminując pobierających (w konstruktorze):

   return (new UserConfig (sName, iAge, sFavoriteColor));

Poprzez bezpośrednie przekazywanie pól ToBeBuiltklasa jest tak samo „ślepa” (nieświadoma swojego konstruktora), jak w przypadku Fieldableinterfejsu. Jednak w przypadku ToBeBuiltklas, które mają być „wielokrotnie rozszerzane i rozszerzane wielokrotnie” (co jest w tytule tego postu), wszelkie zmiany w dowolnym polu wymagają zmian w każdej podklasie, w każdym konstruktorze i ToBeBuiltkonstruktorze. Wraz ze wzrostem liczby pól i podklas staje się to niepraktyczne.

(Rzeczywiście, z kilkoma niezbędnymi polami, użycie konstruktora może być przesadą. Dla zainteresowanych, oto próbka niektórych z większych interfejsów Fieldable w mojej osobistej bibliotece.)

Klasy wtórne w paczce

Wybieram, aby mieć wszystkich konstruktorów i Fieldableklas, dla wszystkich konstruktorów niewidomych, w paczce ich ToBeBuiltklasy. Pakiet podrzędny ma zawsze nazwę „ z”. Zapobiega to zaśmiecaniu tych klas drugorzędnych listy pakietów JavaDoc. Na przykład

  • library.class.my.UserConfig
  • library.class.my.z.UserConfig_Fieldable
  • library.class.my.z.UserConfig_Cfg

Przykład walidacji

Jak wspomniano powyżej, wszystkie sprawdzanie poprawności odbywa się w ToBeBuiltkonstruktorze. Oto konstruktor ponownie z przykładowym kodem sprawdzającym:

public UserConfig (UserConfig_Fieldable uc_f) {
   //transfer
      próbować {
         sName = uc_f.getName ();
      } catch (NullPointerException rx) {
         wrzuć nowy wyjątek NullPointerException („uc_f”);
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // walidacja (powinna naprawdę skompilować wzorce ...)
      próbować {
         if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
            wrzuć nowy wyjątek IllegalArgumentException („uc_f.getName () (\” + sName + „\”) nie może być pusty i musi zawierać tylko litery, cyfry i znaki podkreślenia. ”);
         }
      } catch (NullPointerException rx) {
         wrzuć nowy wyjątek NullPointerException („uc_f.getName ()”);
      }
      jeśli (iAge <0) {
         wyrzuć nowy IllegalArgumentException („uc_f.getAge () („ + iAge + ”) jest mniejsza niż zero.”);
      }
      próbować {
         if (! Pattern.compile ("(?: czerwony | niebieski | zielony | gorący różowy)"). matcher (sFavColor) .matches ()) {
            wyrzuć nowy IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") nie jest czerwony, niebieski, zielony ani gorący różowy.");
         }
      } catch (NullPointerException rx) {
         wrzuć nowy wyjątek NullPointerException ("uc_f.getFavoriteColor ()");
      }
}

Dokumentowanie konstruktorów

Ta sekcja dotyczy zarówno konstruktorów Bloch, jak i konstruktorów niewidomych. Pokazuje, jak dokumentuję klasy w tym projekcie, czyniąc settery (w kreatorze) i ich gettery (w ToBeBuiltklasie) bezpośrednio ze sobą powiązane - za pomocą jednego kliknięcia myszy i bez potrzeby, aby użytkownik wiedział, gdzie funkcje te faktycznie znajdują się - i bez konieczności tworzenia przez program zbędnych dokumentów.

Getters: ToBeBuiltTylko w klasach

Gettery są dokumentowane tylko w ToBeBuiltklasie. Odpowiedniki pobierające zarówno w klasach, jak _Fieldablei_Cfg klasach są ignorowane. W ogóle ich nie dokumentuję.

/ **
   <P> Wiek użytkownika. </P>
   @return Int reprezentujący wiek użytkownika.
   @see UserConfig_Cfg # age (int)
   @ patrz getName ()
 ** /
public int getAge () {
   zwróć iAge;
}

Pierwszy @seeto link do jego setera, który należy do klasy konstruktora.

Settery: w klasie budowniczej

Setter jest udokumentowany tak, jakby to w ToBeBuiltklasie , a także, jeśli to robi walidacji (która naprawdę jest wykonywana przez ToBeBuilt„s konstruktora). Gwiazdka („ *”) to wizualna wskazówka wskazująca, że ​​cel łącza znajduje się w innej klasie.

/ **
   <P> Ustaw wiek użytkownika. </P>
   @param i_age Nie może być mniejsza niż zero. Uzyskaj za pomocą {@code UserConfig # getName () getName ()} *.
   @see #favoriteColor (String)
 ** /
public UserConfig_Cfg age (int i_age) {
   iAge = i_age;
   zwróć to;
}

Dalsza informacja

Wszystko razem: pełne źródło przykładu Blind Buildera z pełną dokumentacją

UserConfig.java

import java.util.regex.Pattern;
/ **
   <P> Informacje o użytkowniku - <I> [konstruktor: UserConfig_Cfg] </I> </P>
   <P> Sprawdzanie poprawności wszystkich pól następuje w tym konstruktorze klas. Jednak każdy wymóg sprawdzania poprawności jest dokumentowany tylko w funkcjach programu budującego. </P>
   <P> {@code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </P>
 ** /
UserConfig klasy publicznej {
   public static final void main (String [] igno_red) {
      UserConfig uc = new UserConfig_Cfg („Kermit”). Age (50) .favoriteColor („zielony”). Build ();
      System.out.println (uc);
   }
   prywatny końcowy ciąg sName;
   prywatny finał iAge;
   prywatny końcowy ciąg sFavColor;
   / **
      <P> Utwórz nową instancję. Spowoduje to ustawienie i sprawdzenie wszystkich pól. </P>
      @param uc_f Może nie być {@code null}.
    ** /
   public UserConfig (UserConfig_Fieldable uc_f) {
      //transfer
         próbować {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            wrzuć nowy wyjątek NullPointerException („uc_f”);
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      //uprawomocnić
         próbować {
            if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
               wrzuć nowy wyjątek IllegalArgumentException („uc_f.getName () (\” + sName + „\”) nie może być pusty i musi zawierać tylko litery, cyfry i znaki podkreślenia. ”);
            }
         } catch (NullPointerException rx) {
            wrzuć nowy wyjątek NullPointerException („uc_f.getName ()”);
         }
         jeśli (iAge <0) {
            wyrzuć nowy IllegalArgumentException („uc_f.getAge () („ + iAge + ”) jest mniejsza niż zero.”);
         }
         próbować {
            if (! Pattern.compile ("(?: czerwony | niebieski | zielony | gorący różowy)"). matcher (sFavColor) .matches ()) {
               wyrzuć nowy IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") nie jest czerwony, niebieski, zielony ani gorący różowy.");
            }
         } catch (NullPointerException rx) {
            wrzuć nowy wyjątek NullPointerException ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> Nazwa użytkownika. </P>
         @return Nie - {@ kod null}, niepuste ciąg.
         @see UserConfig_Cfg # UserConfig_Cfg (String)
         @see #getAge ()
         @see #getFavoriteColor ()
       ** /
      ciąg publiczny getName () {
         return sName;
      }
      / **
         <P> Wiek użytkownika. </P>
         @return Liczba większa niż lub równa zero.
         @see UserConfig_Cfg # age (int)
         @ patrz #getName ()
       ** /
      public int getAge () {
         zwróć iAge;
      }
      / **
         <P> Ulubiony kolor użytkownika. </P>
         @return Nie - {@ kod null}, niepuste ciąg.
         @see UserConfig_Cfg # age (int)
         @ patrz #getName ()
       ** /
      public String getFavoriteColor () {
         return sFavColor;
      }
   //getters...END
   public String toString () {
      return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> Wymagane przez konstruktora {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable)}. </P>
 ** /
interfejs publiczny UserConfig_Fieldable {
   Ciąg getName ();
   int getAge ();
   Ciąg getFavoriteColor ();
}

UserConfig_Cfg.java

import java.util.regex.Pattern;
/ **
   <P> Kreator {@link UserConfig}. </P>
   <P> Sprawdzanie poprawności wszystkich pól następuje w konstruktorze <CODE> UserConfig </CODE>. Jednak każde wymaganie sprawdzania poprawności jest dokumentem tylko w funkcjach ustawiania klas. </P>
 ** /
klasa publiczna UserConfig_Cfg implementuje UserConfig_Fieldable {
   public String sName;
   public int iAge;
   public String sFavColor;
   / **
      <P> Utwórz nową instancję z nazwą użytkownika. </P>
      @param s_name Nie może być {@code null} ani pusty i musi zawierać tylko litery, cyfry i znaki podkreślenia. Uzyskaj za pomocą {@code UserConfig # getName () getName ()} {@ code ()} .
    ** /
   public UserConfig_Cfg (String s_name) {
      sName = nazwa_s;
   }
   // sety powracające ... START
      / **
         <P> Ustaw wiek użytkownika. </P>
         @param i_age Nie może być mniejsza niż zero. Uzyskaj za pomocą {@code UserConfig # getName () getName ()} {@ code ()} .
         @see #favoriteColor (String)
       ** /
      public UserConfig_Cfg age (int i_age) {
         iAge = i_age;
         zwróć to;
      }
      / **
         <P> Ustaw ulubiony kolor użytkownika. </P>
         @param s_color Musi być {@code „czerwony”}, {@code „niebieski”}, {@code zielony} lub {@code „gorący różowy”}. Uzyskaj za pomocą {@code UserConfig # getName () getName ()} {@ code ()} *.
         @see #age (int)
       ** /
      public UserConfig_Cfg favoriteColor (String s_color) {
         sFavColor = s_color;
         zwróć to;
      }
   // ustawiacze samowracające ... KONIEC
   //getters...START
      ciąg publiczny getName () {
         return sName;
      }
      public int getAge () {
         zwróć iAge;
      }
      public String getFavoriteColor () {
         return sFavColor;
      }
   //getters...END
   / **
      <P> Zbuduj UserConfig zgodnie z konfiguracją. </P>
      @return <CODE> (nowy {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (this)) </CODE>
    ** /
   public UserConfig build () {
      return (new UserConfig (this));
   }
}

aliteralmind
źródło
1
Zdecydowanie jest to poprawa. Wdrożony tutaj Bloch's Builder łączy dwie konkretne klasy, z których jedna ma zostać zbudowana i jej budowniczy. To jest zły projekt per se . Konstruktor niewidomych, który opisujesz, zrywa to połączenie, ponieważ klasa , która ma być budowana, definiuje swoją zależność konstrukcyjną jako abstrakcję , którą inne klasy mogą zaimplementować w odsprzężeniu. W dużym stopniu zastosowałeś istotną zorientowaną obiektowo wytyczną projektową.
rucamzu
3
Powinieneś naprawdę gdzieś o tym blogować, jeśli jeszcze tego nie zrobiłeś, fajny kawałek algorytmu! Nie chcę już tego udostępniać :-).
Martijn Verburg
4
Dziękuję za miłe słowa. To jest teraz pierwszy post na moim nowym blogu: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind
Jeśli zarówno konstruktor, jak i obiekty zbudowane implementują Fieldable, wzorzec zaczyna przypominać ten, który określiłem jako ReadableFoo / MutableFoo / ImmutableFoo, chociaż zamiast metody, aby zmienna rzecz była „kompilacją” członka konstruktora, ja wywołaj go asImmutablei umieść w ReadableFoointerfejsie [używając tej filozofii, wywołanie buildniezmiennego obiektu po prostu zwróci odwołanie do tego samego obiektu].
supercat
1
@ThomasN Musisz rozszerzyć *_Fieldablei dodać do niego nowe programy pobierające oraz rozszerzyć i dodać nowe programy *_Cfgustawiające, ale nie rozumiem, dlaczego musisz odtwarzać istniejące programy pobierające i ustawiające. Są dziedziczone i jeśli nie potrzebują innej funkcjonalności, nie trzeba ich ponownie tworzyć.
aliteralmind
13

Myślę, że pytanie tutaj zakłada coś od samego początku, nie próbując tego udowodnić, że wzorzec konstruktora jest z natury dobry.

tl; dr Myślę, że wzór konstruktora rzadko, jeśli w ogóle, jest dobrym pomysłem.


Cel wzorca budowniczego

Celem wzorca konstruktora jest utrzymanie dwóch reguł, które ułatwią konsumpcję klasy:

  1. Obiekty nie powinny mieć możliwości konstruowania w stanach niespójnych / nieużytecznych / nieprawidłowych.

    • Odnosi się to do scenariuszach, w których na przykład Personobiekt może być skonstruowana bez konieczności to Idwypełnione, a wszystkie fragmenty kodu, które używają tego obiektu może wymagać się Idtylko do prawidłowej pracy z Person.
  2. Konstruktory obiektów nie powinny wymagać zbyt wielu parametrów .

Cel wzorca konstruktora jest więc nie kontrowersyjny. Myślę, że wiele z tego pragnienia i jego wykorzystania opiera się na analizie, która posunęła się zasadniczo tak daleko: chcemy tych dwóch reguł, to daje te dwie reguły - choć uważam, że warto zbadać inne sposoby realizacji tych dwóch zasad.


Po co męczyć się, patrząc na inne podejścia?

Myślę, że powód dobrze pokazuje sam fakt tego pytania; zastosowanie struktur budowniczych powoduje złożoność i wiele ceremonii. To pytanie pyta, jak rozwiązać część tej złożoności, ponieważ jak często złożoność, tworzy scenariusz, który zachowuje się dziwnie (dziedziczenie). Ta złożoność zwiększa również koszty utrzymania (dodawanie, zmienianie lub usuwanie właściwości jest znacznie bardziej złożone niż w innych przypadkach).


Inne podejścia

Jakie są zatem podejścia do powyższej zasady nr 1? Kluczem, do którego odnosi się ta reguła, jest to, że po zbudowaniu obiekt ma wszystkie informacje potrzebne do prawidłowego funkcjonowania - a po zbudowaniu informacji tych nie można zmienić zewnętrznie (więc są to informacje niezmienne).

Jednym ze sposobów przekazania wszystkich niezbędnych informacji obiektowi podczas budowy jest po prostu dodanie parametrów do konstruktora. Jeśli konstruktor zażąda tych informacji, nie będziesz w stanie zbudować tego obiektu bez wszystkich tych informacji, dlatego zostanie on skonstruowany w prawidłowy stan. Ale co jeśli obiekt wymaga dużej ilości informacji, aby był ważny? O cholera, jeśli tak jest, to podejście złamałoby zasadę nr 2 powyżej .

Ok, co jeszcze tam jest? Cóż, możesz po prostu wziąć wszystkie te informacje, które są niezbędne, aby twój obiekt był w spójnym stanie, i połączyć je w inny obiekt, który jest pobierany podczas budowy. Twój kod powyżej zamiast wzorca budowniczego byłby wtedy:

//DTO...START
public class Cfg  {
   public String sName    ;
   public int    iAge     ;
   public String sFavColor;
}
//DTO...END

public class UserConfig  {
   private final String sName    ;
   private final int    iAge     ;
   private final String sFavColor;
   public UserConfig(Cfg uc_c)  {
      ...
   }

   public String toString()  {
      return  "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
   }
}

Nie różni się to zbytnio od wzorca konstruktora, choć jest nieco prostsze i, co najważniejsze, spełniamy teraz zasadę nr 1 i zasadę nr 2 .

Dlaczego więc nie pójść trochę dalej i zrobić z tego pełny builder? To po prostu niepotrzebne . W tym podejściu spełniłem oba cele wzorca konstruktora - coś nieco prostszego, łatwiejszego w utrzymaniu i wielokrotnego użytku . Ta ostatnia kwestia jest kluczowa, ponieważ ten przykład jest wymyślony i nie nadaje się do celu semantycznego w świecie rzeczywistym, więc pokażmy, w jaki sposób takie podejście prowadzi do wielokrotnego użytku DTO, a nie do pojedynczej klasy celu .

public class NetworkAddress {
   public String Ip;
   public int Port;
   public NetworkAddress Proxy;
}

public class SocketConnection {
   public SocketConnection(NetworkAddress address) {
      ...
   }
}

public class FtpClient {
   public FtpClient(NetworkAddress address) {
      ...
   }
}

Więc kiedy budujesz spójne DTO w ten sposób, mogą one zarówno spełniać cel wzorca konstruktora, prościej i przy większej wartości / użyteczności. Ponadto takie podejście rozwiązuje złożoność dziedziczenia, jaką daje wzorzec konstruktora:

public class SslCert {
   public NetworkAddress Authority;
   public byte[] PrivateKey;
   public byte[] PublicKey;
}

public class FtpsClient extends FtpClient {
   public FtpsClient(NetworkAddress address, SslCert cert) {
      super(address);
      ...
   }
}

Może się okazać, że DTO nie zawsze jest spójne lub aby grupy właściwości były spójne, trzeba je rozbić na wiele DTO - to nie jest tak naprawdę problem. Jeśli twój obiekt wymaga 18 właściwości i możesz zrobić 3 spójne DTO z tymi właściwościami, masz prostą konstrukcję, która spełnia cele konstruktorów, a następnie niektóre. Jeśli nie możesz wymyślić spójnych grup, może to być znak, że twoje obiekty nie są spójne, jeśli mają właściwości, które są tak całkowicie niezwiązane - ale nawet wtedy tworzenie pojedynczego niespójnego DTO jest nadal preferowane ze względu na prostszą implementację plus rozwiązanie problemu z dziedziczeniem.


Jak poprawić wzorzec konstruktora

Ok, więc odsuńmy na bok wszystkie felgi, masz problem i szukasz rozwiązania projektowego, aby je rozwiązać. Moja sugestia: klasy dziedziczące mogą mieć po prostu klasę zagnieżdżoną, która dziedziczy z klasy konstruktora superklasy, więc klasa dziedzicząca ma zasadniczo taką samą strukturę jak klasa super i ma wzorzec konstruktora, który powinien działać dokładnie tak samo z dodatkowymi funkcjami dla dodatkowych właściwości podklasy.


Kiedy jest to dobry pomysł

Odkładając na bok, wzorzec budowniczego ma niszę . Wszyscy to wiemy, ponieważ wszyscy nauczyliśmy się tego konkretnego konstruktora w tym czy innym momencie: StringBuilder- tutaj celem nie jest prosta konstrukcja, ponieważ łańcuchy nie mogą być łatwiejsze do skonstruowania i połączenia itp. Jest to świetny konstruktor, ponieważ zapewnia korzyści w zakresie wydajności .

Korzyści płynące z wydajności są zatem następujące: masz mnóstwo obiektów, są one niezmiennego typu, musisz zwinąć je do jednego obiektu niezmiennego typu. Jeśli robisz to stopniowo, utworzysz tutaj wiele obiektów pośrednich, więc robienie tego wszystkiego naraz jest o wiele bardziej wydajne i idealne.

Myślę więc, że kluczem do tego, kiedy jest to dobry pomysł, jest dziedzina problemowa StringBuilder: Konieczność przekształcenia wielu instancji typów niezmiennych w jedną instancję typu niezmiennego .

Jimmy Hoffa
źródło
Nie sądzę, żeby twój przykład spełniał którąkolwiek z zasad. Nic nie stoi na przeszkodzie, aby utworzyć Cfg w nieprawidłowym stanie, a mimo że parametry zostały przeniesione z ctor, właśnie zostały przeniesione do mniej idiomatycznego i bardziej pełnego miejsca. fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()oferuje zwięzły interfejs API do tworzenia foos i może zaoferować faktyczne sprawdzanie błędów w samym kreatorze. Bez konstruktora sam obiekt musi sprawdzić swoje dane wejściowe, co oznacza, że ​​nie jesteśmy w lepszej sytuacji niż kiedyś.
Phoshi
DTO mogą mieć swoje właściwości sprawdzane na wiele sposobów deklaracyjnie za pomocą adnotacji na seterze, jednak chcesz to zrobić - sprawdzanie poprawności jest osobnym problemem, aw jego podejściu do konstruktora pokazuje sprawdzanie poprawności występujące w konstruktorze, ta sama logika idealnie pasowałaby w moim podejściu. Jednak ogólnie lepiej byłoby użyć DTO, aby sprawdzić poprawność, ponieważ, jak pokazuję - DTO może być używany do konstruowania wielu typów, a więc posiadanie na nim sprawdzania poprawiłoby się do sprawdzania wielu typów. Konstruktor sprawdza tylko jeden konkretny typ, dla którego został stworzony.
Jimmy Hoffa
Być może najbardziej elastycznym sposobem byłoby posiadanie statycznej funkcji sprawdzania poprawności w kreatorze, która akceptuje pojedynczy Fieldableparametr. Ja nazwałbym tę funkcję walidacji z ToBeBuiltkonstruktora, ale może być wywołana przez cokolwiek, z dowolnego miejsca. Eliminuje to potencjał nadmiarowego kodu, bez wymuszania konkretnej implementacji. (I nic nie stoi na przeszkodzie, abyś przeszedł w poszczególnych polach do funkcji sprawdzania poprawności, jeśli nie podoba ci się ta Fieldablekoncepcja - ale teraz byłyby co najmniej trzy miejsca, w których lista pól musiałaby być utrzymywana.)
aliteralmind
+1 A klasa, która ma zbyt wiele zależności w swoim konstruktorze, jest oczywiście niewystarczająco spójna i powinna zostać przekształcona w mniejsze klasy.
Basilevs,
@ JimmyHoffa: Ach, rozumiem, po prostu o tym zapomniałeś. Nie jestem pewien, czy widzę różnicę między tym a konstruktorem, a zatem inne niż to przekazuje instancję config do ctor zamiast wywoływać .build na jakimś konstruktorze i że konstruktor ma bardziej oczywistą ścieżkę do sprawdzania poprawności wszystkich dane. Każda pojedyncza zmienna może znajdować się w swoich prawidłowych zakresach, ale jest niepoprawna w tej konkretnej permutacji. .build może to sprawdzić, ale przekazanie elementu do ctor wymaga sprawdzenia błędów w samym obiekcie - obrzydliwe!
Phoshi