Zasada segregacji interfejsów: Co zrobić, jeśli interfejsy w znacznym stopniu się pokrywają?

9

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 UiInterface1i UiInterface2?

q126y
źródło
Kiedy natrafiam na nakładające się na siebie interfejsy, tworzę interfejs nadrzędny, który grupuje typowe metody, a następnie dziedziczy po tym wspólnym, aby utworzyć specjalizacje. ALE! Jeśli nigdy nie chcesz, aby ktokolwiek korzystał ze wspólnego interfejsu bez specjalizacji, musisz przejść do duplikacji kodu, ponieważ jeśli wprowadzisz wspólny wspólny interfejs, ludzie mogą z niego skorzystać.
Andy
Pytanie jest dla mnie trochę niejasne, można odpowiedzieć na wiele różnych rozwiązań w zależności od przypadku. Dlaczego nakładanie się wzrosło?
Arthur Havlicek

Odpowiedzi:

1

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 QueryInterfacepodejś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:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the 
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
     const Vec2i xy = position->xy();
     auto parent = parenting->parent();
     if (parent)
     {
         // If the entity has a parent, return the sum of the
         // parent position and the entity's local position.
         return xy + abs_position(dynamic_cast<IPosition*>(parent),
                                  dynamic_cast<IParenting*>(parent));
     }
     return xy;
}

... 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 IParentingi IPositionw jednym miejscu, IGuiElementcoś 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 QueryInterfaceczęść), gramy w podejście do downcastingu, ale konsolidujemy rzutowanie do jednego centralnego miejsca w bazie kodu i możemy zrobić coś takiego:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
     // `Object::query_interface` returns nullptr if the interface is
     // not provided by the entity. `Object` is an abstract base class
     // inherited by all entities using this interface query system.
     IPosition* position = obj->query_interface<IPosition>();
     assert(position && "obj does not implement IPosition!");
     const Vec2i xy = position->xy();

     IParenting* parenting = obj->query_interface<IParenting>();
     if (parenting && parenting->parent()->query_interface<IPosition>())
     {
         // If the entity implements IParenting and has a parent, 
         // return the sum of the parent position and the entity's 
         // local position.
         return xy + abs_position(parenting->parent());
     }
     return xy;
}

... 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ć:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
     const Vec2i xy = obj.xy();
     if (obj.parent())
     {
         // If the entity has a parent, return the sum of the parent 
         // position and the entity's local position.
         return xy + abs_position(obj.parent());
     }
     return xy;
}

... oczywiście w takim przypadku parentmetoda musiałaby zwrócić ten sam Entitytyp, 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 IPositionPlusParentingwywodząca się z obu IPositioniIParenting(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
11

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:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

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?

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

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.

pdr
źródło
1
„Przede wszystkim pamiętaj, że SOLIDNE zasady to tylko ... zasady. Nie są regułami. Nie są srebrną kulą. Są tylko zasadami. Nie należy odbierać ich znaczenia, zawsze powinieneś się pochylać w kierunku podążania za nimi. Ale kiedy wprowadzą poziom bólu, powinieneś je porzucić, dopóki ich nie będziesz potrzebować. ”. Powinno to znajdować się na pierwszej stronie każdej książki wzorców / zasad projektowania. Powinien pojawiać się również co 50 stron jako przypomnienie.
Christian Rodriguez