Кастинг
Это почти наверняка будет полной касательной к подходу цитируемой книги, но один из способов лучше соответствовать ISP - это использовать образ мышления в одной центральной области вашей кодовой базы, используя QueryInterface
подход в стиле COM.
Многие соблазны проектировать перекрывающиеся интерфейсы в чистом контексте интерфейса часто возникают из-за желания сделать интерфейсы «самодостаточными», а не выполнять одну точную, подобную снайперской ответственности.
Например, может показаться странным проектировать клиентские функции следующим образом:
// 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;
}
... а также довольно некрасиво / опасно, учитывая, что мы утратили ответственность за выполнение подверженного ошибкам приведения к клиентскому коду с использованием этих интерфейсов и / или многократной передачи одного и того же объекта в качестве аргумента нескольким параметрам одного и того же функция. Таким образом, мы в конечном итоге часто хотим разработать более разбавленный интерфейс, который объединяет проблемы IParenting
и IPosition
в одном месте, например IGuiElement
или что-то подобное, который затем становится подверженным частичному перекрытию с проблемами ортогональных интерфейсов, которые также будут испытывать желание иметь больше функций-членов для та же самая причина "самодостаточности".
Смешивание обязанностей против кастинга
При проектировании интерфейсов с полностью искаженной, сверхингулярной ответственностью часто возникает соблазн либо принять какой-то устаревший интерфейс, либо консолидировать интерфейсы для выполнения нескольких обязанностей (и, следовательно, наступить как на ISP, так и на SRP).
Используя подход в стиле COM (только QueryInterface
часть), мы играем на подходе понижения рейтинга, но консолидируем приведение к одному центральному месту в кодовой базе и можем сделать что-то более похожее на это:
// 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;
}
... конечно, мы надеемся, с безопасными типами упаковщиками и всем, что вы можете построить централизованно, чтобы получить что-то более безопасное, чем необработанные указатели.
При этом соблазн проектировать перекрывающиеся интерфейсы часто сводится к абсолютному минимуму. Он позволяет вам проектировать интерфейсы с очень особенными обязанностями (иногда только с одной функцией-членом внутри), которые вы можете смешивать и сопоставлять со всеми, что вам нравится, не беспокоясь о ISP, и получая гибкость псевдодуковой типизации во время выполнения в C ++ (хотя, конечно, с компромисс между штрафами времени выполнения для объектов запроса, чтобы видеть, поддерживают ли они определенный интерфейс). Часть времени выполнения может быть важна, скажем, в установке с комплектом разработки программного обеспечения, где функции не будут заранее иметь информацию о времени компиляции плагинов, которые реализуют эти интерфейсы.
Шаблоны
Если шаблоны возможны (у нас есть необходимая информация времени компиляции, которая не теряется к тому времени, когда мы получаем объект, т.е.), тогда мы можем просто сделать это:
// 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;
}
... конечно, в таком случае parent
метод должен был бы возвращать тот же Entity
тип, и в этом случае мы, вероятно, хотим напрямую избегать интерфейсов (так как они часто захотят потерять информацию о типе в пользу работы с базовыми указателями).
Компонентная система
Если вы начнете придерживаться подхода в стиле COM дальше с точки зрения гибкости или производительности, вы часто будете получать систему с компонентами-сущностями, похожую на те, что применяются в индустрии игровых движков. В этот момент вы будете полностью перпендикулярны многим объектно-ориентированным подходам, но ECS может быть применим к дизайну GUI (одно место, которое я рассматривал, используя ECS за пределами фокусировки на сцене, но считал это слишком поздно после остановившись на подходе в стиле COM, чтобы попробовать там).
Обратите внимание, что это решение в стиле COM полностью разработано для разработки инструментария GUI, и ECS будет еще больше, так что это не то, что будет поддержано большим количеством ресурсов. Тем не менее, это, безусловно, позволит вам уменьшить соблазны проектировать интерфейсы, которые имеют перекрывающиеся обязанности до абсолютного минимума, что часто делает его незаботным.
Прагматический подход
Альтернатива, конечно, это немного ослабить вашу защиту или разработать интерфейсы на более детальном уровне, а затем начать наследовать их, чтобы создать более грубые интерфейсы, которые вы используете, например, IPositionPlusParenting
которые происходят от обоих IPosition
иIParenting
(надеюсь, с лучшим именем, чем это). С чистыми интерфейсами это не должно нарушать ISP так же, как те обычно применяемые монолитные глубокие иерархические подходы (Qt, MFC и т. Д., Где документация часто чувствует необходимость скрывать нерелевантные члены, учитывая чрезмерный уровень нарушения ISP с такими типами). дизайнов), поэтому прагматичный подход может просто допустить некоторое совпадение здесь и там. Тем не менее, такой подход в стиле COM исключает необходимость создания консолидированных интерфейсов для каждой комбинации, которую вы когда-либо будете использовать. В таких случаях полностью исключается проблема «самодостаточности», и это часто устраняет основной источник соблазна для разработки интерфейсов с накладывающимися обязанностями, которые хотят бороться как с SRP, так и с ISP.