Wzorzec Dependency Injection i Singleton Design

89

W jaki sposób identyfikujemy, kiedy należy użyć iniekcji zależności lub wzorca pojedynczego. Czytałem na wielu stronach internetowych, w których piszą „Użyj zastrzyku zależności zamiast wzorca pojedynczego”. Ale nie jestem pewien, czy całkowicie się z nimi zgadzam. W przypadku moich projektów na małą lub średnią skalę zdecydowanie widzę użycie wzorca pojedynczego.

Na przykład Logger. Mógłbym użyć Logger.GetInstance().Log(...) Ale zamiast tego, dlaczego muszę wstrzykiwać każdą klasę, którą tworzę, za pomocą instancji rejestrującej ?.

SysAdmin
źródło

Odpowiedzi:

66

Jeśli chcesz sprawdzić, co jest rejestrowane w teście, potrzebujesz iniekcji zależności. Ponadto rejestrator rzadko jest singletonem - generalnie masz rejestrator dla każdej klasy.

Obejrzyj tę prezentację na temat projektowania zorientowanego obiektowo pod kątem testowalności, a zobaczysz, dlaczego singletony są złe.

Problem z singletonami polega na tym, że reprezentują one stan globalny, który jest trudny do przewidzenia, zwłaszcza w testach.

Należy pamiętać, że obiekt może być de facto singletonem, ale nadal można go uzyskać za pomocą wstrzykiwania zależności, a nie za pośrednictwem Singleton.getInstance().

Wymieniam tylko kilka ważnych uwag, które przedstawił Misko Hevery w swojej prezentacji. Po obejrzeniu go uzyskasz pełną perspektywę, dlaczego lepiej jest, aby obiekt zdefiniował, jakie są jego zależności, ale nie zdefiniujesz sposobu ich tworzenia .

Bozho
źródło
89

Singletony są jak komunizm: oba brzmią świetnie na papierze, ale w praktyce wybuchają problemami.

Wzorzec singletonowy kładzie nieproporcjonalny nacisk na łatwość dostępu do obiektów. Całkowicie unika kontekstu, wymagając, aby każdy konsument używał obiektu o zasięgu AppDomain, nie pozostawiając żadnych opcji dla różnych implementacji. Osadza wiedzę o infrastrukturze w twoich zajęciach (wezwanie do GetInstance()), dodając dokładnie zerową moc ekspresji. W rzeczywistości zmniejsza to twoją siłę ekspresji, ponieważ nie możesz zmienić implementacji używanej przez jedną klasę bez zmiany jej dla wszystkich . Po prostu nie możesz dodać jednorazowych elementów funkcjonalności.

Ponadto, gdy klasa Foozależy Logger.GetInstance(), Fooskutecznie ukrywa swoje zależności przed konsumentami. Oznacza to, że nie możesz go w pełni zrozumieć Fooani używać z pewnością, dopóki nie przeczytasz jego źródła i nie odkryjesz faktu, od którego zależy Logger. Jeśli nie masz źródła, ogranicza to możliwość zrozumienia i efektywnego wykorzystania kodu, na którym polegasz.

Wzorzec singleton, zaimplementowany za pomocą statycznych właściwości / metod, to niewiele więcej niż sztuczka dotycząca implementowania infrastruktury. Ogranicza Cię na niezliczone sposoby, nie oferując dostrzegalnej korzyści w porównaniu z alternatywami. Możesz go używać w dowolny sposób, ale ponieważ istnieją realne alternatywy, które promują lepsze projektowanie, nigdy nie powinno to być zalecane.

Bryan Watts
źródło
9
@BryanWatts wszystko, co powiedziało, Singletony są nadal znacznie szybsze i mniej podatne na błędy w normalnej aplikacji o średniej skali. Zwykle nie mam więcej niż jednej możliwej implementacji (niestandardowego) Loggera, więc dlaczego ** miałbym: 1. Utworzyć interfejs do tego i zmieniać go za każdym razem, gdy muszę dodać / zmienić członków publicznych 2. Utrzymać a konfiguracja DI 3. Ukryj fakt, że w całym systemie znajduje się pojedynczy obiekt tego typu. 4. Powiąż się ze ścisłą funkcjonalnością spowodowaną przedwczesnym rozdzieleniem problemów.
Uri Abramson,
@UriAbramson: Wygląda na to, że już podjąłeś decyzję dotyczącą kompromisów, które preferujesz, więc nie będę próbował Cię przekonywać.
Bryan Watts,
@BryanWatts Tak nie jest, być może mój komentarz był trochę zbyt ostry, ale naprawdę bardzo mnie interesuje, gdy słyszę, co masz do powiedzenia na temat kwestii, o której wspomniałem ...
Uri Abramson
2
@UriAbramson: W porządku. Czy przynajmniej zgodziłbyś się, że wymiana implementacji na izolację podczas testowania jest ważna?
Bryan Watts,
6
Użycie singletonów i iniekcji zależności nie wykluczają się wzajemnie. Singleton może implementować interfejs, dlatego może służyć do spełnienia zależności od innej klasy. Fakt, że jest to singleton, nie zmusza każdego konsumenta do uzyskania referencji za pomocą metody / właściwości „GetInstance”.
Oliver
18

Inni bardzo dobrze wyjaśnili ogólny problem z singletonami. Chciałbym tylko dodać uwagę na temat konkretnego przypadku Loggera. Zgadzam się z Tobą, że dostęp do Loggera (lub dokładniej mówiąc do roota) jako singletona zwykle nie stanowi problemu, za pomocą metody statycznej getInstance()lub getRootLogger()metody. (chyba, że ​​chcesz zobaczyć, co jest rejestrowane przez klasę, którą testujesz - ale z mojego doświadczenia nie mogę sobie przypomnieć takich przypadków, w których było to konieczne. Z drugiej strony, dla innych może to być bardziej naglący problem).

IMO zwykle pojedynczy rejestrator nie jest zmartwieniem, ponieważ nie zawiera żadnego stanu związanego z testowaną klasą. Oznacza to, że stan loggera (i jego możliwe zmiany) nie mają żadnego wpływu na stan testowanej klasy. Więc to nie utrudnia testów jednostkowych.

Alternatywą byłoby wstrzyknięcie rejestratora za pośrednictwem konstruktora do (prawie) każdej klasy w aplikacji. Dla spójności interfejsów należy go wstrzyknąć nawet, jeśli dana klasa obecnie niczego nie rejestruje - alternatywą byłoby to, że gdy odkryjesz w pewnym momencie, że teraz musisz coś zalogować z tej klasy, potrzebujesz loggera, a więc musisz dodać parametr konstruktora dla DI, łamiąc cały kod klienta. Nie podoba mi się obie te opcje i czuję, że używanie DI do logowania tylko skomplikowałoby moje życie, aby zachować zgodność z teoretyczną zasadą, bez żadnych konkretnych korzyści.

Więc moim zdaniem: klasa, która jest używana (prawie) powszechnie, ale nie zawiera stanu związanego z twoją aplikacją, może być bezpiecznie zaimplementowana jako Singleton .

Péter Török
źródło
1
Nawet wtedy singleton może być uciążliwy. Jeśli zdasz sobie sprawę, że twój rejestrator potrzebuje dodatkowych parametrów w niektórych przypadkach, albo możesz stworzyć nową metodę (i pozwolić, aby rejestrator stał się brzydszy) lub złamać wszystkich użytkowników rejestratora, nawet jeśli ich to nie obchodzi.
kyoryu
4
@kyoryu, mówię o „zwykłym” przypadku, który IMHO implikuje używanie (de facto) standardowej struktury logowania. (Które przy okazji można konfigurować za pomocą plików właściwości / XML). Oczywiście są wyjątki - jak zawsze. Jeśli wiem, że moja aplikacja jest wyjątkowa pod tym względem, rzeczywiście nie użyję Singletona. Jednak przepracowanie się, mówiąc „to może się kiedyś przydać” jest prawie zawsze daremnym wysiłkiem.
Péter Török
Jeśli już używasz DI, nie jest to dużo dodatkowa inżynieria. (A tak przy okazji, nie zgadzam się z tobą, jestem za). Wiele rejestratorów wymaga informacji o „kategorii” lub czegoś w tym rodzaju, a dodanie dodatkowych parametrów może powodować ból. Ukrywanie go za interfejsem może pomóc w utrzymaniu czystości kodu i może ułatwić przełączanie się na inną strukturę rejestrowania.
kyoryu
1
@kyoryu, przepraszam za błędne założenie. Widziałem negatyw i komentarz, więc połączyłem kropki - źle, jak się okazuje :-( Nigdy nie doświadczyłem potrzeby przejścia na inny framework do logowania, ale z drugiej strony rozumiem, że może to być legalne problem w niektórych projektach
Péter Török
5
Popraw mnie, jeśli się mylę, ale singleton byłby dla LogFactory, a nie rejestrującego. Dodatkowo LogFactory prawdopodobnie będzie częścią apache commons lub fasadą wyrębową Slf4j. Tak więc przełączanie implementacji logowania jest i tak bezbolesne. Czy prawdziwym problemem związanym z używaniem DI do wstrzyknięcia LogFactory nie jest fakt, że musisz teraz przejść do applicationContext, aby utworzyć każdą instancję w swojej aplikacji?
HDave
9

Chodzi głównie o testy, ale nie do końca. Singltony były popularne ze względu na łatwość ich konsumowania, ale singletony mają wiele wad.

  • Trudne do przetestowania. Oznacza to, jak upewnić się, że rejestrator działa prawidłowo.
  • Trudne do przetestowania. Oznacza to, że jeśli testuję kod, który używa rejestratora, ale nie jest to główny cel mojego testu, nadal muszę się upewnić, że moje środowisko testowe obsługuje rejestrator
  • Czasami nie chcesz singltona, ale większej elastyczności

DI zapewnia łatwe wykorzystanie klas zależnych - po prostu umieść je w argumentach konstruktora, a system zapewni Ci to - jednocześnie zapewniając elastyczność testowania i konstrukcji.

Scott Weinstein
źródło
Nie całkiem. Chodzi o wspólny zmienny stan i statyczne zależności, które na dłuższą metę są bolesne. Testowanie jest tylko oczywistym i często najbardziej bolesnym przykładem.
kyoryu
2

Jedynym przypadkiem, w którym powinieneś kiedykolwiek używać Singletona zamiast Dependency Injection, jest sytuacja, gdy Singleton reprezentuje niezmienną wartość, taką jak List.Empty lub tym podobne (zakładając niezmienne listy).

Sprawdzenie wnętrzności dla Singletona powinno brzmieć „czy byłbym w porządku, gdyby była to zmienna globalna zamiast Singletona?” Jeśli nie, używasz wzorca Singleton do umieszczania na papierze zmiennej globalnej i powinieneś rozważyć inne podejście.

kyoryu
źródło
1

Właśnie przeczytałem artykuł Monostate - to fajna alternatywa dla Singletona, ale ma kilka dziwnych właściwości:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

Czy to nie jest przerażające - ponieważ Mapper jest naprawdę zależny od połączenia z bazą danych, aby wykonać save () - ale jeśli wcześniej utworzono inny program mapujący - może pominąć ten krok w pozyskiwaniu jego zależności. Choć schludny, jest też trochę bałaganiarski, prawda?

sunwukung
źródło