Widziałem historię kilku projektów bibliotek klas С # i Java na GitHub i CodePlex, i widzę trend przechodzenia do klas fabrycznych w przeciwieństwie do bezpośredniego tworzenia instancji obiektów.
Dlaczego powinienem intensywnie korzystać z klas fabrycznych? Mam całkiem niezłą bibliotekę, w której obiekty są tworzone w staromodny sposób - przy użyciu publicznych konstruktorów klas. W ostatnich aktach autorzy szybko zmienili wszystkich publicznych konstruktorów tysięcy klas na wewnętrzne, a także stworzyli jedną ogromną klasę fabryczną z tysiącami CreateXXX
metod statycznych, które tylko zwracają nowe obiekty przez wywołanie wewnętrznych konstruktorów klas. Zewnętrzny interfejs API projektu jest uszkodzony, dobra robota.
Dlaczego taka zmiana byłaby przydatna? Jaki jest sens refaktoryzacji w ten sposób? Jakie są zalety zastąpienia wywołań konstruktorów klasy publicznej wywołaniami metod statycznych?
Kiedy powinienem używać publicznych konstruktorów, a kiedy powinienem używać fabryk?
Odpowiedzi:
Klasy fabryczne są często wdrażane, ponieważ pozwalają projektowi ściślej przestrzegać zasad SOLID . W szczególności zasady segregacji interfejsów i zasady inwersji zależności.
Fabryki i interfejsy pozwalają na znacznie bardziej długoterminową elastyczność. Pozwala to na bardziej niezależny - a zatem i bardziej testowalny - projekt. Oto niewyczerpująca lista powodów, dla których możesz pójść tą ścieżką:
Rozważ tę sytuację.
Zespół A (-> środki zależą od):
Chcę przenieść klasę B do zestawu B, który jest zależny od zestawu A. Z tymi konkretnymi zależnościami muszę przenieść większość całej mojej hierarchii klas. Jeśli użyję interfejsów, mogę uniknąć dużego bólu.
Montaż A:
Teraz mogę przenieść klasę B do zestawu B bez żadnego bólu. Nadal zależy to od interfejsów w zestawie A.
Korzystanie z kontenera IoC w celu rozwiązania zależności pozwala na jeszcze większą elastyczność. Nie ma potrzeby aktualizowania każdego wywołania do konstruktora za każdym razem, gdy zmieniasz zależności klasy.
Przestrzeganie zasady segregacji interfejsu i zasady inwersji zależności pozwala nam budować wysoce elastyczne, oddzielone od siebie aplikacje. Po opracowaniu aplikacji tego typu nigdy więcej nie będziesz chciał wrócić do używania
new
słowa kluczowego.źródło
Jak powiedział whatsisname , uważam, że jest to przypadek projektowania oprogramowania kultowego . Fabryki, szczególnie abstrakcyjne, są użyteczne tylko wtedy, gdy moduł tworzy wiele instancji klasy, a użytkownik chce dać użytkownikowi tego modułu możliwość określenia, który typ ma zostać utworzony. To wymaganie jest w rzeczywistości dość rzadkie, ponieważ przez większość czasu potrzebujesz tylko jednej instancji i możesz ją przekazać bezpośrednio zamiast tworzyć jawną fabrykę.
Chodzi o to, że fabryki (i singletony) są niezwykle łatwe do wdrożenia, więc ludzie często z nich korzystają, nawet w miejscach, gdzie nie są konieczne. Więc kiedy programista myśli „Jakie wzorce projektowe powinienem zastosować w tym kodzie?” Fabryka jako pierwsza przychodzi mu na myśl.
Wiele fabryk powstaje, ponieważ „Może kiedyś będę musiał stworzyć te klasy inaczej”. Co jest wyraźnym naruszeniem YAGNI .
A fabryki stają się przestarzałe po wprowadzeniu frameworka IoC, ponieważ IoC jest tylko rodzajem fabryki. I wiele platform IoC jest w stanie tworzyć implementacje określonych fabryk.
Ponadto nie ma wzorca projektowego, który mówi o tworzeniu ogromnych klas statycznych za pomocą
CreateXXX
metod wywołujących tylko konstruktory. I nie jest to szczególnie nazywane Fabryką (ani Fabryką Abstrakcyjną).źródło
Moda na wzorce fabryczne wynika z niemal dogmatycznego przekonania programistów w językach „C” (C / C ++, C #, Java), że użycie słowa kluczowego „new” jest złe i należy go unikać za wszelką cenę (lub najmniej scentralizowany). To z kolei wynika z bardzo ścisłej interpretacji zasady pojedynczej odpowiedzialności („S” SOLID), a także zasady inwersji zależności („D”). Mówiąc wprost, SRP mówi, że idealnie obiekt kodu powinien mieć jeden „powód do zmiany” i tylko jeden; ten „powód zmiany” jest głównym celem tego obiektu, jego „odpowiedzialnością” w bazie kodu i wszystko, co wymaga zmiany kodu, nie powinno wymagać otwarcia tego pliku klasy. DIP jest jeszcze prostszy; obiekt kodu nigdy nie powinien być zależny od innego konkretnego obiektu,
W tym przypadku, używając „new” i konstruktora publicznego, podłączasz kod wywołujący do konkretnej metody konstrukcyjnej określonej konkretnej klasy. Twój kod musi teraz wiedzieć, że istnieje klasa MyFooObject i ma konstruktor, który pobiera ciąg i liczbę całkowitą. Jeśli ten konstruktor kiedykolwiek potrzebuje więcej informacji, wszystkie zastosowania konstruktora muszą zostać zaktualizowane, aby przekazać te informacje, w tym te, które piszesz teraz, a zatem muszą mieć coś ważnego do przekazania, więc muszą albo lub zmień, aby go zdobyć (dodając więcej obowiązków do zużywających się obiektów). Ponadto, jeśli MyFooObject zostanie kiedykolwiek zastąpiony w bazie kodu przez BetterFooObject, wszystkie zastosowania starej klasy muszą się zmienić, aby zbudować nowy obiekt zamiast starego.
Zamiast tego wszyscy konsumenci MyFooObject powinni być bezpośrednio zależni od „IFooObject”, który określa zachowanie klas implementujących, w tym MyFooObject. Teraz konsumenci IFooObjects nie mogą po prostu zbudować IFooObject (bez wiedzy, że konkretna konkretna klasa jest IFooObject, której nie potrzebują), dlatego zamiast tego muszą otrzymać instancję klasy lub metody implementującej IFooObject z zewnątrz, przez inny obiekt, który ma obowiązek wiedzieć, jak stworzyć właściwy obiekt IFooObject na okoliczność, która w naszym języku jest zwykle znana jako Fabryka.
Tutaj teoria spotyka się z rzeczywistością; obiekt nigdy nie może być cały czas zamknięty na wszystkie rodzaje zmian. W tym przypadku IFooObject jest teraz dodatkowym obiektem kodu w bazie kodu, który musi się zmieniać za każdym razem, gdy zmieni się interfejs wymagany przez konsumentów lub implementacje IFooObjects. Wprowadza to nowy poziom złożoności związany ze zmianą sposobu interakcji obiektów między sobą w ramach tej abstrakcji. Ponadto konsumenci nadal będą musieli się zmienić i głębiej, jeśli sam interfejs zostanie zastąpiony nowym.
Dobry programista wie, jak zrównoważyć YAGNI („Nie będziesz go potrzebował”) z SOLID, analizując projekt i znajdując miejsca, które najprawdopodobniej będą musiały się zmienić w określony sposób, i przefaktoryzuj je, aby były bardziej tolerancyjne na tego typu zmiany, ponieważ w tym przypadku „ty to będzie to potrzebne”.
źródło
Foo
określiła konstruktora publicznego, który powinien być użyteczny do tworzeniaFoo
instancji lub tworzenia innych typów w ramach tego samego pakietu / zestawu , ale nie powinien być przydatny do tworzenia typy wyprowadzone gdzie indziej. Nie znam żadnego szczególnie ważnego powodu, dla którego język / środowisko nie mogło zdefiniować osobnych konstruktorów do użycia wnew
wyrażeniach, w przeciwieństwie do wywoływania z konstruktorów podtypów, ale nie znam żadnych języków, które to odróżniają.Konstruktory są w porządku, jeśli zawierają krótki, prosty kod.
Gdy inicjalizacja staje się czymś więcej niż przypisywaniem kilku zmiennych do pól, fabryka ma sens. Oto niektóre z korzyści:
Długi, skomplikowany kod ma większy sens w dedykowanej klasie (fabryce). Jeśli ten sam kod zostanie wstawiony do konstruktora, który wywołuje kilka metod statycznych, spowoduje to zanieczyszczenie głównej klasy.
W niektórych językach i niektórych przypadkach zgłaszanie wyjątków w konstruktorach jest naprawdę złym pomysłem , ponieważ może wprowadzać błędy.
Podczas wywoływania konstruktora użytkownik wywołujący musi znać dokładny typ instancji, którą chcesz utworzyć. Nie zawsze tak jest (
Feeder
po prostu muszę je zbudowaćAnimal
, aby je nakarmić; nie dbam o to, czy jest toDog
aCat
).źródło
Feeder
może użyć żadnego z nich i zamiast tego wywołać metodęKennel
obiektugetHungryAnimal
.Builder Pattern
. Czyż nieJeśli pracujesz z interfejsami, możesz pozostać niezależny od faktycznej implementacji. Fabrykę można skonfigurować (za pomocą właściwości, parametrów lub innej metody), aby utworzyć instancję jednego z wielu różnych wdrożeń.
Jeden prosty przykład: chcesz komunikować się z urządzeniem, ale nie wiesz, czy będzie to przez Ethernet, COM czy USB. Definiujesz jeden interfejs i 3 implementacje. W czasie wykonywania możesz następnie wybrać metodę, którą chcesz, a fabryka zapewni odpowiednią implementację.
Używaj go często ...
źródło
Jest to objaw ograniczenia w systemach modułowych Java / C #.
Zasadniczo nie ma powodu, dla którego nie powinieneś być w stanie zamienić jednej implementacji klasy na inną z tym samym sygnaturą konstruktora i metody. Tam są języki, które pozwalają na to. Jednak Java i C # nalegają, aby każda klasa miała unikalny identyfikator (w pełni kwalifikowaną nazwę), a kod klienta kończy się na niej mocno zakodowaną zależnością.
Można rodzaj obejść ten problem przez majstrowanie przy użyciu systemu plików i opcje kompilatora, dzięki czemu
com.example.Foo
mapy do innego pliku, ale to jest zaskakujące i nieintuicyjne. Nawet jeśli to zrobisz, Twój kod jest nadal powiązany tylko z jedną implementacją klasy. Tzn. Jeśli napiszesz klasęFoo
zależną od klasyMySet
, możesz wybrać implementacjęMySet
w czasie kompilacji, ale nadal nie możesz utworzyć instancjiFoo
za pomocą dwóch różnych implementacjiMySet
.Ta niefortunna decyzja projektowa zmusza ludzi do
interface
niepotrzebnego wykorzystywania s, aby zabezpieczyć swój kod na przyszłość przed możliwością, że będą później potrzebować innej implementacji lub ułatwić testowanie jednostek. Nie zawsze jest to możliwe; jeśli masz jakieś metody, które sprawdzają prywatne pola dwóch instancji klasy, nie będziesz w stanie zaimplementować ich w interfejsie. Dlatego na przykład nie widziszunion
wSet
interfejsie Java . Mimo to, poza typami liczbowymi i kolekcjami, metody binarne nie są powszechne, więc zwykle można się z tym pogodzić.Oczywiście, jeśli zadzwonisz
new Foo(...)
, nadal będziesz zależny od klasy , więc potrzebujesz fabryki, jeśli chcesz, aby klasa mogła bezpośrednio utworzyć interfejs. Jednak zwykle lepszym pomysłem jest zaakceptowanie instancji w konstruktorze i pozwolenie komuś innemu decydować, której implementacji użyć.Od Ciebie zależy, czy warto rozbudować bazę kodową interfejsami i fabrykami. Z jednej strony, jeśli dana klasa jest wewnętrzną bazą kodu, refaktoryzacja kodu w taki sposób, aby używał innej klasy lub interfejsu w przyszłości, jest banalna; jeśli sytuacja się pojawi, możesz skorzystać z YAGNI i dokonać refaktora później. Ale jeśli klasa jest częścią publicznego interfejsu API opublikowanej biblioteki, nie ma możliwości poprawienia kodu klienta. Jeśli nie używasz,
interface
a później potrzebujesz wielu implementacji, utkniesz między skałą a trudnym miejscem.źródło
new
byłyby po prostu cukrem syntaktycznym do wywoływania specjalnie nazwanej metody statycznej (która byłaby generowana automatycznie, gdyby typ miał „publiczny konstruktor” ”, ale nie zawiera jawnie metody). IMHO, jeśli kod chce tylko nudnej domyślnej rzeczy, która się implementujeList
, powinien mieć możliwość nadania jej interfejsu bez konieczności posiadania przez klienta wiedzy o konkretnej implementacji (npArrayList
.).Moim zdaniem po prostu używają Prostej Fabryki, która nie jest właściwym wzorcem projektowym i nie powinna być mylona z Fabryką Abstrakcyjną lub Metodą Fabryczną.
A ponieważ stworzyli „ogromną klasę tkanin z tysiącami statycznych metod CreateXXX”, brzmi to anty-wzór (może klasa Boga?).
Myślę, że Proste fabryki i metody tworzenia statycznego (które nie wymagają klasy zewnętrznej) mogą być przydatne w niektórych przypadkach. Na przykład, gdy konstrukcja obiektu wymaga różnych kroków, takich jak tworzenie innych obiektów (np. Preferowanie kompozycji).
Nie nazwałbym nawet tego, że to Fabryka, ale tylko kilka metod zawartych w losowej klasie z sufiksem „Fabryka”.
źródło
int x
iIFooService fooService
. Nie chceszfooService
wszędzie chodzić , więc tworzysz fabrykę metodąCreate(int x)
i wstrzykujesz usługę wewnątrz fabryki.IFactory
wszędzie krążyć wokółIFooService
.Jako użytkownik biblioteki, jeśli biblioteka ma metody fabryczne, powinieneś ich użyć. Zakłada się, że metoda fabryczna daje autorowi biblioteki elastyczność w zakresie wprowadzania pewnych zmian bez wpływu na kod. Mogą na przykład zwrócić instancję jakiejś podklasy w metodzie fabrycznej, która nie działałaby z prostym konstruktorem.
Jako twórca biblioteki używałbyś metod fabrycznych, jeśli sam chciałbyś skorzystać z tej elastyczności.
W opisanym przypadku masz wrażenie, że zastąpienie konstruktorów metodami fabrycznymi było po prostu bezcelowe. Z pewnością był to ból dla wszystkich zaangażowanych; biblioteka nie powinna niczego usuwać z interfejsu API bez bardzo ważnego powodu. Gdybym więc dodał metody fabryczne, pozostawiłbym dostępne konstruktory, być może przestarzałe, dopóki metoda fabryczna nie wywoła już więcej tego konstruktora, a kod używający zwykłego konstruktora działa gorzej niż powinien. Twoje wrażenie może mieć rację.
źródło
Wydaje się to być przestarzałe w dobie Scali i programowania funkcjonalnego. Solidny fundament funkcji zastępuje klasy gazillionów.
Należy również zauważyć, że podwójne Java
{{
nie działa już w przypadku korzystania z fabryki, tjCo pozwoli ci stworzyć anonimową klasę i ją dostosować.
W przypadku dylematu przestrzeni czasowej czynnik czasu jest teraz tak bardzo zmniejszony dzięki szybkim procesorom, że funkcjonalne programowanie umożliwiające zmniejszenie przestrzeni, tj. Rozmiar kodu, jest teraz praktyczne.
źródło