Принцип сегрегации интерфейса: что делать, если интерфейсы имеют значительное перекрытие?

9

Из Agile Software Development, Принципы, Шаблоны и Практики: Pearson New International Edition :

Иногда методы, вызываемые различными группами клиентов, перекрываются. Если перекрытие небольшое, то интерфейсы для групп должны оставаться отдельными. Общие функции должны быть объявлены во всех перекрывающихся интерфейсах. Класс сервера будет наследовать общие функции от каждого из этих интерфейсов, но он будет реализовывать их только один раз.

Дядя Боб рассказывает о случае, когда есть незначительные совпадения.

Что мы должны делать, если существует значительное совпадение?

Скажем у нас

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

Что мы должны делать, если между UiInterface1и UiInterface2?

q126y
источник
Когда я сталкиваюсь с сильно перекрывающимися интерфейсами, я создаю родительский интерфейс, который группирует общие методы, а затем наследует от этого общего для создания специализаций. НО! Если вы никогда не хотите, чтобы кто-либо использовал общий интерфейс без специализации, тогда вам действительно нужно пойти на дублирование кода, потому что, если вы представите общий интерфейс parrent, люди могут его использовать.
Энди
Вопрос для меня немного расплывчатый, можно ответить разными способами в зависимости от ситуации. Почему наложение выросло?
Артур Гавличек

Ответы:

1

Кастинг

Это почти наверняка будет полной касательной к подходу цитируемой книги, но один из способов лучше соответствовать 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.


источник
11

Это вызов, который вы должны сделать, в каждом конкретном случае.

Прежде всего, помните, что твердые принципы - это просто ... принципы. Они не правила. Они не серебряная пуля. Они просто принципы. Это не умаляет их важности, вы всегда должны стремиться следовать за ними. Но после того, как они вводят уровень боли, вы должны бросить их, пока они вам не понадобятся.

Имея это в виду, подумайте, почему вы в первую очередь выделяете свои интерфейсы. Идея интерфейса заключается в том, чтобы сказать: «Если для этого потребляющего кода требуется реализовать набор методов для потребляемого класса, мне нужно заключить контракт на реализацию: если вы предоставите мне объект с этим интерфейсом, я могу работать с этим."

Цель провайдера заключается в том, чтобы сказать: «Если требуемый контракт является лишь подмножеством существующего интерфейса, я не должен применять существующий интерфейс в будущих классах, которые могут быть переданы моему методу».

Рассмотрим следующий код:

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

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

Теперь у нас есть ситуация, когда, если мы хотим передать новый объект в ConsumeX, он должен реализовать X () и Y (), чтобы соответствовать контракту.

Так стоит ли сейчас менять код, чтобы он выглядел как следующий пример?

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

public interface B
{
    void X();
}

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

Интернет-провайдер предлагает нам, поэтому мы должны склоняться к этому решению. Но без контекста трудно быть уверенным. Вполне вероятно, что мы расширим А и В? Вполне вероятно, что они будут расширяться независимо? Возможно ли, что B когда-либо реализует методы, которые A не требуются? (Если нет, мы можем сделать A производным от B.)

Это суждение, которое вы должны сделать. И, если у вас действительно недостаточно информации для выполнения этого вызова, вам, вероятно, следует выбрать самый простой вариант, который вполне может быть первым кодом.

Почему? Потому что потом легко передумать. Когда вам нужен этот новый класс, просто создайте новый интерфейс и реализуйте оба в своем старом классе.

прецизионный самописец
источник
1
«Прежде всего, помните, что твердые принципы - это просто ... принципы. Они не правила. Они не серебряная пуля. Они просто принципы. Это не означает, что их важность не должна отвлекаться, вы всегда должны опираться в направлении следования за ними. Но когда они вводят боль, вы должны бросить их, пока они вам не понадобятся ». Это должно быть на первой странице каждой книги по шаблонам / принципам дизайна. Это должно появиться также каждые 50 страниц в качестве напоминания.
Кристиан Родригес