Klasy podstawowe jako fabryki?

14

W weekend pisałem trochę kodu i chciałem napisać fabrykę jako metodę statyczną w klasie bazowej.

Moim pytaniem jest po prostu wiedzieć, czy jest to podejście idomatyczne?

Wydaje mi się, że może nie być tak, że klasa podstawowa ma wiedzę o klasie pochodnej.

To powiedziawszy, nie jestem pewien prostszego sposobu na uzyskanie tego samego rezultatu. Cała inna klasa fabryczna wydaje mi się (przynajmniej dla mnie) niepotrzebną złożonością (?)

Coś jak:

class Animal
{
  public static Animal CreateAnimal(string name)
  {
     switch(name)
     {
        case "Shark":
          return new SeaAnimal();
          break;
        case "Dog":
          return new LandAnimal();
          break;
        default:
          throw new Exception("unknown animal");
     }
  }
}
class LandAnimal : Animal
{
}

class SeaAnimal : Animal
{
}
Aaron Anodide
źródło
Jak przetestujesz swoje fabryki?
z kodem w tym pytaniu nie zrobiłbym tego. ale odpowiedź pomogła mi wprowadzić ścieżkę integracji testów w moim stylu kodowania
Aaron Anodide
Pomyśl, że wciągnięcie zarówno Świata Morskiego, jak i Krainy Świata w swojej klasie Zwierząt, sprawia, że ​​trudniej jest sobie z nim poradzić w odizolowanym otoczeniu.

Odpowiedzi:

13

Zaletą oddzielnej klasy fabrycznej jest to, że można ją wyśmiewać w testach jednostkowych.

Ale jeśli nie zamierzasz tego robić lub uczynić go polimorficznym w jakikolwiek inny sposób, to statyczna metoda Factory na samej klasie jest OK.

pdr
źródło
dzięki, czy możesz podać przykład, o którym mówisz, kiedy mówisz polimorficzny w jakikolwiek inny sposób?
Aaron Anodide
Mam na myśli to, że jeśli kiedykolwiek będziesz w stanie zastąpić jedną metodę fabryczną inną. Wyśmiewanie go w celach testowych jest tylko jednym z najczęstszych tego przykładów.
pdr
18

Możesz użyć Generics, aby uniknąć instrukcji switch i oddzielić późniejsze implementacje od klasy podstawowej:

 public static T CreateAnimal<T>() where T: new, Animal
 {
    return new T();
 }

Stosowanie:

LandAnimal rabbit = Animal.CreateAnimal();  //Type inference should should just figure out the T by the return indicated.

lub

 var rabbit = Animal.CreateAnimal<LandAnimal>(); 
Nick Nieslanik
źródło
2
@PeterK. Fowler odnosi się do instrukcji switch w Code Smells, ale jego rozwiązaniem jest to, że powinny one zostać wyodrębnione do klas Factory, a nie zastąpione. To powiedziawszy, jeśli zawsze będziesz znać typ w czasie programowania, generics jest jeszcze lepszym rozwiązaniem. Ale jeśli tego nie zrobisz (jak sugeruje oryginalny przykład), to argumentowałbym, że użycie refleksji w celu uniknięcia zmiany metody fabrycznej może być fałszywą ekonomią.
pdr
instrukcje switch i łańcuchy if-else-if są takie same. używam tego pierwszego ze względu na sprawdzanie czasu kompilacji, które wymusza obsługę domyślnego przypadku
Aaron Anodide
4
@PeterK, w instrukcji switch kod znajduje się w jednym miejscu. W pełnej wersji OO ten sam kod może być rozłożony na wiele oddzielnych klas. Istnieje szara strefa, w której dodatkowa złożoność posiadania wielu fragmentów jest mniej pożądana niż instrukcja switch.
@Thorbjorn, miałem taką dokładną myśl, ale nigdy tak zwięźle, dziękuję za wypowiedzenie słów
Aaron Anodide
5

Podsumowując, fabryka może być również ogólna.

interface IFactory<K, T> where K : IComparable
{
    T Create(K key);
}

Następnie można stworzyć dowolną fabrykę obiektów, która z kolei mogłaby stworzyć dowolny typ obiektu. Nie jestem pewien, czy jest to prostsze, z pewnością bardziej ogólne.

Nie widzę nic złego w instrukcji switch dla małej implementacji fabrycznej. Gdy przejdziesz do dużej liczby obiektów lub możliwych różnych hierarchii klas, myślę, że bardziej ogólne podejście jest bardziej odpowiednie.

Jon Raynor
źródło
1

Kiepski pomysł. Po pierwsze, narusza zasadę otwartego zamknięcia. W przypadku każdego nowego zwierzęcia musiałbyś znów zadzierać z klasą podstawową i potencjalnie byś go złamał. Zależności pójdą w złą stronę.

Jeśli musisz stworzyć zwierzęta z konfiguracji, taka konstrukcja byłaby w porządku, chociaż lepszym rozwiązaniem byłoby użycie refleksji w celu uzyskania typu pasującego do nazwy i utworzenia go za pomocą uzyskanej informacji o typie.

Ale powinieneś utworzyć dedykowaną klasę fabryczną, niezwiązaną z hierarchią klas zwierząt, i zwrócić interfejs IAnimal zamiast typu podstawowego. Wtedy przydałoby się, osiągnęlibyście pewne oddzielenie.

Martin Maat
źródło
0

To pytanie jest dla mnie aktualne - wczoraj pisałem prawie dokładnie taki kod. Po prostu zamień „Animal” na wszystko, co jest istotne w moim projekcie, chociaż pozostanę przy „Animal” w celu omówienia tutaj. Zamiast instrukcji „switch” miałem nieco bardziej złożoną serię instrukcji „if”, obejmującą więcej niż tylko porównanie jednej zmiennej z pewnymi ustalonymi wartościami. Ale to szczegół. Statyczna metoda fabryczna wydawała się zgrabnym sposobem projektowania rzeczy, ponieważ projekt powstał z refaktoryzacji wcześniejszych, szybkich i brudnych elementów kodu.

Odrzuciłem ten projekt ze względu na klasę podstawową mającą wiedzę o klasie pochodnej. Jeśli klasy LandAnimal i SeaAnimal są małe, czyste i łatwe, mogą znajdować się w tym samym pliku źródłowym. Ale mam duże niechlujne metody odczytu plików tekstowych niezgodnych z żadnymi oficjalnie zdefiniowanymi standardami - chcę, aby moja klasa LandAnimal miała własny plik źródłowy.

Prowadzi to do zależności plików cyklicznych - LandAnimal pochodzi od Animal, ale Animal musi już wiedzieć, że istnieje LandAnimal, SeaAnimal i piętnaście innych klas. Wyciągnąłem metodę fabryczną, umieściłem ją we własnym pliku (w mojej głównej aplikacji, a nie w bibliotece Animals) Posiadanie statycznej metody fabrycznej wydawało się urocze i sprytne, ale zdałem sobie sprawę, że tak naprawdę nie rozwiązało to żadnego problemu projektowego.

Nie mam pojęcia, jak to się ma do idiomatycznego C #, ponieważ często zmieniam języki, zwykle ignoruję idiomy i konwencje specyficzne dla języków i stosy deweloperów poza moją zwykłą pracą. Jeśli cokolwiek, mój C # może wyglądać „pytonicznie”, jeśli to ma sens. Dążę do ogólnej jasności.

Nie wiem też, czy przydałoby się stosowanie statycznej metody fabrycznej w przypadku małych, prostych klas, które raczej nie zostaną rozszerzone w przyszłości - posiadanie tego wszystkiego w jednym pliku źródłowym może być przyjemne w niektórych przypadkach.

DarenW
źródło