Rozważ te dwa przykłady:
Przekazywanie obiektu do konstruktora
class ExampleA
{
private $config;
public function __construct($config)
{
$this->config = $config;
}
}
$config = new Config;
$exampleA = new ExampleA($config);
Tworzenie instancji klasy
class ExampleB
{
private $config;
public function __construct()
{
$this->config = new Config;
}
}
$exampleA = new ExampleA();
Jaki jest prawidłowy sposób obsługi dodawania obiektu jako właściwości? Kiedy powinienem używać jednego na drugim? Czy testy jednostkowe wpływają na to, czego powinienem użyć?
Odpowiedzi:
Myślę, że pierwszy da ci możliwość stworzenia
config
obiektu w innym miejscu i przekazania goExampleA
. Jeśli potrzebujesz wstrzyknięcia zależności, może to być dobre, ponieważ możesz zapewnić, że wszystkie intencje współużytkują ten sam obiekt.Z drugiej strony, być może twój
ExampleA
wymaga nowego i czystegoconfig
obiektu, więc mogą wystąpić przypadki, w których drugi przykład jest bardziej odpowiedni, na przykład przypadki, w których każda instancja może potencjalnie mieć inną konfigurację.źródło
Nie zapomnij o testowalności!
Zwykle jeśli zachowanie
Example
klasy zależy od konfiguracji, którą chcesz móc przetestować bez zapisywania / modyfikowania wewnętrznego stanu instancji klasy (tzn. Chcesz mieć proste testy pod kątem szczęśliwej ścieżki i niewłaściwej konfiguracji bez modyfikowania właściwości / członekExample
klasy).Dlatego wybrałbym pierwszą opcję
ExampleA
źródło
Jeśli obiekt jest odpowiedzialny za zarządzanie czasem życia zależności, wówczas można utworzyć obiekt w konstruktorze (i zrzucić go w destruktorze). *
Jeśli obiekt nie jest odpowiedzialny za zarządzanie czasem życia zależności, powinien zostać przekazany do konstruktora i zarządzany z zewnątrz (np. Przez kontener IoC).
W tym przypadku nie sądzę, że twoja klasa A powinna wziąć odpowiedzialność za utworzenie $ config, chyba że jest również odpowiedzialna za usunięcie go lub jeśli konfiguracja jest unikalna dla każdej instancji klasy A.
* Aby pomóc w testowaniu, konstruktor może odwoływać się do klasy / metody fabryki w celu skonstruowania zależności w swoim konstruktorze, zwiększając spójność i testowalność.
źródło
Niedawno miałem dokładnie tę samą dyskusję z naszym zespołem architektury i istnieją pewne subtelne powody, aby to zrobić w taki czy inny sposób. Sprowadza się to głównie do Dependency Injection (jak zauważyli inni) i tego, czy naprawdę masz kontrolę nad tworzeniem nowego obiektu w konstruktorze.
W twoim przykładzie, co jeśli twoja klasa Config:
a) ma nietrywialny przydział, taki jak metoda puli lub metoda fabryczna.
b) może nie zostać przydzielony. Przekazywanie go do konstruktora starannie pozwala uniknąć tego problemu.
c) jest w rzeczywistości podklasą Config.
Przekazywanie obiektu do konstruktora daje największą elastyczność.
źródło
Poniższa odpowiedź jest błędna, ale zatrzymam ją, aby inni mogli się z niej uczyć (patrz poniżej)
W
ExampleA
można użyć tego samegoConfig
wystąpienia w wielu klasach. Jeśli jednakConfig
w całej aplikacji powinna znajdować się tylko jedna instancja, rozważ zastosowanie wzorca Singleton,Config
aby uniknąć wielu instancjiConfig
. A jeśliConfig
jest singletonem, możesz zamiast tego wykonać następujące czynności:Z
ExampleB
drugiej strony, zawsze otrzymasz osobne wystąpienieConfig
dla każdego wystąpieniaExampleB
.Która wersja powinna zostać zastosowana, zależy od tego, jak aplikacja będzie obsługiwać wystąpienia
Config
:ExampleX
powinna mieć osobną instancjęConfig
, idź zExampleB
;ExampleX
będzie dzielić jedno (i tylko jedno) wystąpienieConfig
, użyjExampleA with Config Singleton
;ExampleX
mogą używać różnych instancjiConfig
, trzymaj sięExampleA
.Dlaczego przejście
Config
na Singleton jest złe:Muszę przyznać, że wczoraj dowiedziałem się o wzorze Singleton (czytając książkę wzorców projektowych Head First ). Naiwnie poszedłem i zastosowałem go w tym przykładzie, ale jak wielu zauważyło, jeden sposób jest inny (niektórzy byli bardziej tajemniczy i mówili tylko „robisz to źle!”), To nie jest dobry pomysł. Tak więc, aby inni nie popełnili tego samego błędu, który właśnie popełniłem, poniżej znajduje się podsumowanie, dlaczego wzorzec Singleton może być szkodliwy (na podstawie komentarzy i tego, co go znalazłem w Google):
Jeśli
ExampleA
pobierze własne odwołanie doConfig
instancji, klasy będą ściśle powiązane. Nie będzieExampleA
możliwości użycia innej wersjiConfig
(powiedzmy podklasy). Jest to okropne, jeśli chcesz przetestowaćExampleA
przy użyciu instancji makiety,Config
ponieważ nie ma sposobu, aby to zrobićExampleA
.Założeniem będzie jeden i tylko jeden przypadek, który
Config
może się teraz utrzymywać , ale nie zawsze możesz być pewien, że to samo będzie obowiązywać w przyszłości . Jeśli w pewnym momencie okaże się, żeConfig
pożądanych będzie wiele instancji , nie ma możliwości osiągnięcia tego bez przepisania kodu.Chociaż jedno i jedyne wystąpienie
Config
może być prawdą przez całą wieczność, może się zdarzyć, że będziesz chciał móc skorzystać z jakiejś podklasyConfig
(wciąż mając tylko jedno wystąpienie). Ale, ponieważ kod pobiera bezpośrednio poprzez wystąpieniegetInstance()
oConfig
, który jeststatic
metoda, nie ma sposobu, aby uzyskać podklasy. Ponownie kod musi zostać przepisany.Fakt, że
ExampleA
zastosowaniaConfig
zostaną ukryte, przynajmniej podczas przeglądania interfejsu APIExampleA
. Może to być, ale nie musi, zła rzecz, ale osobiście uważam, że jest to niekorzystne; na przykład, utrzymując, nie ma prostego sposobu, aby dowiedzieć się, na które klasy będą miały wpływ zmiany,Config
bez patrzenia na implementację każdej innej klasy.Nawet jeśli fakt, że
ExampleA
używa się SingletonaConfig
, sam w sobie nie stanowi problemu, może on stać się problemem z testowego punktu widzenia. Obiekty Singleton będą nosić stan, który będzie trwał do momentu zakończenia aplikacji. Może to stanowić problem podczas uruchamiania testów jednostkowych, ponieważ chcesz, aby jeden test był izolowany od drugiego (tj. Że wykonanie jednego testu nie powinno wpływać na wynik innego). Aby to naprawić, obiekt Singleton musi zostać zniszczony między każdym uruchomieniem testowym (potencjalnie konieczne ponowne uruchomienie całej aplikacji), co może być czasochłonne (nie wspominając o żmudnych i irytujących).Powiedziawszy to, cieszę się, że popełniłem ten błąd tutaj, a nie podczas wdrażania prawdziwej aplikacji. W rzeczywistości zastanawiałem się nad przepisaniem mojego najnowszego kodu w celu użycia wzorca Singleton dla niektórych klas. Chociaż mógłbym z łatwością cofnąć zmiany (oczywiście wszystko jest przechowywane w SVN), nadal zmarnowałbym czas na robienie tego.
źródło
ExampleA
iConfig
- co nie jest dobrą rzeczą.Najprostszą rzeczą do zrobienia jest kilka
ExampleA
doConfig
. Powinieneś zrobić najprostszą rzecz, chyba że istnieje ważny powód, aby zrobić coś bardziej złożonego.Jednym z powodów, dla oddzielenia
ExampleA
iConfig
byłoby poprawić testowalnościExampleA
. Bezpośrednie sprzężenie się pogorszyć testowalnościExampleA
jeśliConfig
ma metod jest powolna, kompleks, lub szybko się zmienia. Do testowania metoda jest powolna, jeśli działa dłużej niż kilka mikrosekund. Jeśli wszystkie metodyConfig
są proste i szybkie, wtedy biorę proste podejście i jest bezpośrednio paręExampleA
doConfig
.źródło
Twój pierwszy przykład to przykład wzoru wstrzykiwania zależności. Klasa z zewnętrzną zależnością jest przekazywana przez konstruktora, settera itp.
Takie podejście powoduje luźny kod. Większość ludzi uważa, że luźne sprzężenie jest dobrą rzeczą, ponieważ można łatwo zastąpić konfigurację w przypadkach, w których jedna konkretna instancja obiektu musi być skonfigurowana inaczej niż inne, można przekazać próbny obiekt konfiguracyjny do testowania, i tak na.
Drugie podejście jest bliższe wzorcowi twórcy GRASP. W takim przypadku obiekt tworzy własne zależności. Powoduje to ściśle powiązany kod, co może ograniczyć elastyczność klasy i utrudnić testowanie. Jeśli potrzebujesz jednej instancji klasy, aby mieć inną zależność od innych, jedyną opcją jest jej podklasę.
Może to być jednak odpowiedni wzorzec w przypadkach, w których długość życia zależnego obiektu jest podyktowana czasem życia zależnego obiektu, i gdy zależny obiekt nie jest używany nigdzie poza obiektem zależnym od niego. Normalnie radziłbym DI jako pozycję domyślną, ale nie musisz całkowicie wykluczać drugiego podejścia, o ile masz świadomość jego konsekwencji.
źródło
Jeśli twoja klasa nie udostępnia
$config
klas zewnętrznych, utworzę ją w konstruktorze. W ten sposób zachowujesz prywatność swojego stanu wewnętrznego.Jeśli
$config
wymaga to poprawnego ustawienia własnego stanu wewnętrznego (np. Wymaga połączenia z bazą danych lub zainicjowania niektórych pól wewnętrznych) przed użyciem, wówczas sensowne jest odroczenie inicjalizacji do jakiegoś zewnętrznego kodu (być może klasy fabrycznej) i wstrzyknięcie go konstruktor. Lub, jak zauważyli inni, jeśli trzeba go udostępnić innym obiektom.źródło
Przykład A jest oddzielony od konkretnej klasy Config, co jest dobre , pod warunkiem otrzymania obiektu, jeśli nie jest to typ Config, ale typ abstrakcyjnej nadklasy Config.
Przykład B jest silnie sprzężony z konkretną konfiguracją klasy, która jest zła .
Tworzenie wystąpienia obiektu tworzy silne połączenie między klasami. Należy to zrobić w klasie Factory.
źródło