Z Agile Software Development, zasad, wzorców i praktyk: Pearson New International Edition :
Czasami metody przywoływane przez różne grupy klientów nakładają się. Jeśli nakładanie się jest niewielkie, interfejsy dla grup powinny pozostać osobne. Wspólne funkcje powinny być deklarowane we wszystkich nakładających się interfejsach. Klasa serwera odziedziczy wspólne funkcje z każdego z tych interfejsów, ale zaimplementuje je tylko raz.
Wujek Bob, mówi o sprawie, gdy zachodzi niewielkie nakładanie się.
Co powinniśmy zrobić, jeśli nakładają się na siebie?
Powiedzmy, że mamy
Class UiInterface1;
Class UiInterface2;
Class UiInterface3;
Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};
Co powinniśmy zrobić, jeśli występuje znaczące nakładanie się między UiInterface1
i UiInterface2
?
Odpowiedzi:
Odlew
To prawie na pewno będzie całkowicie styczne do podejścia cytowanej książki, ale jednym ze sposobów lepszego dostosowania się do ISP jest przyjęcie nastawienia rzutowego w jednym centralnym obszarze bazy kodu przy użyciu
QueryInterface
podejścia w stylu COM.Wiele pokus, aby zaprojektować nakładające się interfejsy w czystym kontekście interfejsu, często wynika z chęci uczynienia interfejsów „samowystarczalnymi”, więcej niż wykonywania jednej precyzyjnej, podobnej do snajpera odpowiedzialności.
Na przykład może wydawać się dziwne projektowanie takich funkcji klienta:
... a także dość brzydkie / niebezpieczne, biorąc pod uwagę, że ponosimy odpowiedzialność za podatne na błędy rzutowanie na kod klienta za pomocą tych interfejsów i / lub przekazywanie tego samego obiektu jako argumentu wiele razy do wielu parametrów tego samego funkcjonować. Dlatego często chcemy zaprojektować bardziej rozwodniony interfejs, który konsoliduje obawy
IParenting
iIPosition
w jednym miejscu,IGuiElement
coś podobnego lub coś takiego, co następnie staje się podatne na nakładanie się na obawy dotyczące interfejsów ortogonalnych, które również będą kuszone, aby mieć więcej funkcji członkowskich dla ten sam powód „samowystarczalności”.Mieszanie odpowiedzialności a casting
Projektując interfejsy z całkowicie destylowaną, wyjątkowo osobliwą odpowiedzialnością, pokusa często będzie polegać albo na akceptacji downcastingu, albo na konsolidacji interfejsów w celu wypełnienia wielu obowiązków (a zatem stąpania zarówno po ISP, jak i SRP).
Stosując podejście w stylu COM (tylko
QueryInterface
część), gramy w podejście do downcastingu, ale konsolidujemy rzutowanie do jednego centralnego miejsca w bazie kodu i możemy zrobić coś takiego:... oczywiście mam nadzieję, że dzięki opakowaniom bezpiecznym dla typu i wszystkim, co możesz zbudować centralnie, aby uzyskać coś bezpieczniejszego niż surowe wskaźniki.
Dzięki temu pokusa projektowania nakładających się interfejsów jest często zmniejszana do absolutnego minimum. Pozwala projektować interfejsy o bardzo szczególnych obowiązkach (czasami tylko jedna funkcja członka w środku), które można miksować i dopasowywać do swoich potrzeb, nie martwiąc się o dostawcę usług internetowych, i uzyskując elastyczność pisania pseudo-duck w czasie wykonywania w C ++ (choć oczywiście z kompromis kar wykonawczych w celu przeszukiwania obiektów w celu sprawdzenia, czy obsługują one określony interfejs). Część środowiska wykonawczego może być ważna, powiedzmy, w ustawieniu z zestawem programistycznym, w którym funkcje nie będą miały wcześniej informacji o czasie kompilacji wtyczek, które implementują te interfejsy.
Szablony
Jeśli szablony są możliwe (mamy z wyprzedzeniem niezbędne informacje o czasie kompilacji, które nie są tracone do czasu, gdy zdobywamy obiekt, tj.), Możemy po prostu to zrobić:
... oczywiście w takim przypadku
parent
metoda musiałaby zwrócić ten samEntity
typ, w którym to przypadku prawdopodobnie chcemy całkowicie uniknąć interfejsów (ponieważ często będą chcieli utracić informacje o typie na rzecz pracy ze wskaźnikami bazowymi).System encji-komponentów
Jeśli zaczniesz stosować podejście oparte na modelu COM z punktu widzenia elastyczności lub wydajności, często otrzymasz system elementów składowych podobny do tego, jakie silniki gier stosują w branży. W tym momencie będziesz całkowicie prostopadły do wielu podejść obiektowych, ale ECS może mieć zastosowanie do projektowania GUI (jedno miejsce, które rozważałem za pomocą ECS poza skupieniem się na scenie, ale uznałem to za późno po decydując się na podejście w stylu COM, aby spróbować).
Zauważ, że to rozwiązanie w stylu COM jest całkowicie dostępne, jeśli chodzi o projekty zestawów narzędzi GUI, a ECS byłoby jeszcze więcej, więc nie jest to coś, co będzie wspierane przez wiele zasobów. Jednak na pewno pozwoli ci to zmniejszyć pokusy projektowania interfejsów, które mają nakładające się obowiązki do absolutnego minimum, często czyniąc to bez obaw.
Pragmatyczne podejście
Alternatywą jest oczywiście odpocząć baczności trochę, lub zaprojektować interfejsy na szczegółowym poziomie, a następnie uruchomić dziedziczy ich do tworzenia interfejsów grubsze, że używasz, jak
IPositionPlusParenting
wywodząca się z obuIPosition
iIParenting
(mam nadzieję, że ma lepszą nazwę). Dzięki czystym interfejsom nie powinno to naruszać ISP w takim stopniu, jak powszechnie stosowane monolityczne podejścia głębokohierarchiczne (Qt, MFC itp.), W których dokumentacja często odczuwa potrzebę ukrycia nieistotnych członków ze względu na nadmierny poziom naruszania ISP tego rodzaju wzorów), więc pragmatyczne podejście może po prostu zaakceptować pewne nakładanie się tu i tam. Jednak tego rodzaju podejście w stylu COM pozwala uniknąć potrzeby tworzenia skonsolidowanych interfejsów dla każdej kombinacji, jakiej kiedykolwiek użyjesz. W takich przypadkach problem „samowystarczalności” zostaje całkowicie wyeliminowany, co często eliminuje ostateczne źródło pokusy projektowania interfejsów, które mają nakładające się obowiązki, które chcą walczyć zarówno z SRP, jak i ISP.źródło
Jest to wezwanie do osądu, które należy wykonać indywidualnie dla każdego przypadku.
Przede wszystkim pamiętaj, że zasady SOLID to tylko ... zasady. To nie są zasady. Nie są srebrną kulą. To tylko zasady. Nie oznacza to, że tracą na znaczeniu, zawsze powinieneś skłaniać się za ich przestrzeganiem. Ale kiedy wprowadzą pewien poziom bólu, powinieneś je porzucić, dopóki ich nie potrzebujesz.
Mając to na uwadze, pomyśl przede wszystkim o tym, dlaczego oddzielasz interfejsy. Ideą interfejsu jest powiedzenie: „Jeśli ten konsumujący kod wymaga zestawu metod do zaimplementowania w używanej klasie, muszę zawrzeć umowę dotyczącą implementacji: jeśli podasz mi obiekt z tym interfejsem, mogę pracować z tym."
Celem dostawcy usług internetowych jest powiedzenie „Jeśli wymagana przeze mnie umowa jest tylko podzbiorem istniejącego interfejsu, nie powinienem egzekwować istniejącego interfejsu na żadnych przyszłych klasach, które mogłyby zostać przekazane mojej metodzie”.
Rozważ następujący kod:
Teraz mamy sytuację, w której jeśli chcemy przekazać nowy obiekt do ConsumeX, musi on zaimplementować X () i Y (), aby dopasować kontrakt.
Czy powinniśmy teraz zmienić kod, aby wyglądał jak następny przykład?
ISP sugeruje, że powinniśmy, więc powinniśmy pochylić się nad tą decyzją. Ale bez kontekstu trudno być pewnym. Czy jest prawdopodobne, że przedłużymy A i B? Czy prawdopodobne jest, że przedłużą się one niezależnie? Czy jest prawdopodobne, że B kiedykolwiek wdroży metody, których A nie wymaga? (Jeśli nie, możemy sprawić, że A wywodzi się z B.)
To jest wezwanie do osądu, które musisz wykonać. A jeśli naprawdę nie masz wystarczająco dużo informacji, aby wykonać to połączenie, prawdopodobnie powinieneś wybrać najprostszą opcję, która może być pierwszym kodem.
Dlaczego? Ponieważ później łatwo zmienić zdanie. Kiedy potrzebujesz tej nowej klasy, po prostu stwórz nowy interfejs i zaimplementuj obie w starej klasie.
źródło