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:
// 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 IParenting
i IPosition
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:
// 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 parent
metoda musiałaby zwrócić ten sam Entity
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 obu IPosition
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.