Два противоречивых определения принципа разделения интерфейсов - какое из них правильное?

14

При чтении статей об ISP, кажется, есть два противоречивых определения ISP:

Согласно первому определению (см. 1 , 2 , 3 ), провайдер заявляет, что классы, реализующие интерфейс, не должны принуждаться к реализации функций, которые им не нужны. Таким образом, толстый интерфейсIFat

interface IFat
{
     void A();
     void B();
     void C();
     void D();
}

class MyClass: IFat
{ ... }

следует разделить на более мелкие интерфейсы ISmall_1иISmall_2

interface ISmall_1
{
     void A();
     void B();
}

interface ISmall_2
{
     void C();
     void D();
}

class MyClass:ISmall_2
{ ... }

так как таким образом мой MyClassспособен реализовать только те методы, которые необходимы ( D()и C()), не будучи вынужден также обеспечить фиктивные реализации для A(), B()и C():

Но согласно второму определению (см. 1 , 2 , ответ Назар Мерза ), провайдер заявляет, что MyClientвызывающие методы MyServiceне должны знать о тех методах, MyServiceкоторые ему не нужны. Другими словами, если MyClientтребуется только функциональность C()и D(), то вместо

class MyService 
{
    public void A();
    public void B();
    public void C();
    public void D();
}

/*client code*/      
MyService service = ...;
service.C(); 
service.D();

мы должны разделить MyService'sметоды на клиентские интерфейсы:

public interface ISmall_1
{
     void A();
     void B();
}

public interface ISmall_2
{
     void C();
     void D();
}

class MyService:ISmall_1, ISmall_2 
{ ... }

/*client code*/
ISmall_2 service = ...;
service.C(); 
service.D();

Таким образом, в первом определении цель ISP состоит в том, чтобы « облегчить жизнь классам, реализующим интерфейс IFat », а в последнем случае цель ISP - « облегчить жизнь клиентам, вызывающим методы MyService ».

Какое из двух разных определений провайдера действительно правильно?

@MARJAN VENEMA

1.

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

Хотя имеет смысл поместить связные методы в один и тот же интерфейс, я подумал, что с помощью шаблона ISP потребности клиента имеют приоритет перед «связностью» интерфейса. Другими словами, я думал, что с провайдером мы должны объединить в одном и том же интерфейсе те методы, которые нужны конкретным клиентам, даже если это означает, что нужно исключить из этого интерфейса те методы, которые, ради целостности, также должны быть помещены в тот же интерфейс?

Таким образом, если было много клиентов, которым когда-либо нужно было только звонить CutGreens, но не также GrillMeat, то для того, чтобы придерживаться шаблона ISP, мы должны только помещать его CutGreensвнутрь ICook, но не также GrillMeat, даже если эти два метода очень взаимосвязаны ?!

2.

Я думаю, что ваша путаница проистекает из скрытого предположения в первом определении: что реализующие классы уже следуют принципу единой ответственности.

Под "реализацией классов, не следующих за SRP", вы имеете в виду те классы, которые реализуют, IFatили классы, которые реализуют ISmall_1/ ISmall_2? Я полагаю, вы имеете в виду классы, которые реализуют IFat? Если так, почему вы предполагаете, что они еще не следуют SRP?

Благодарность

EdvRusj
источник
4
Почему не может быть нескольких определений, которые обслуживаются по одному и тому же принципу?
Бобсон
5
Эти определения не противоречат друг другу.
Майк Партридж
1
Нет, конечно, потребности клиента не имеют приоритета над связностью интерфейса. Вы можете пойти по этому «правилу» далеко и в конечном итоге использовать единственные интерфейсы методов, которые абсолютно не имеют никакого смысла. Прекратите следовать правилам и начните думать о целях, для которых эти правила были созданы. Что касается "классов, не следующих за SRP", я не говорил ни о каких конкретных классах в вашем примере или о том, что они уже не следовали за SRP. Прочитай заново. Первое определение приводит к разделению интерфейса, только если интерфейс не следует за ISP, а класс следует за SRP.
Марьян Венема
2
Второе определение не заботится о разработчиках. Он определяет интерфейсы с точки зрения вызывающих и не делает никаких предположений о том, существуют ли уже реализации или нет. Вероятно, предполагается, что когда вы следуете за ISP и приступаете к реализации этих интерфейсов, вы, конечно, будете следовать SRP при их создании.
Марьян Венема
2
Как узнать заранее, какие клиенты будут существовать и какие методы им понадобятся? Ты не можешь То, что вы можете знать заранее, это то, насколько сплочен ваш интерфейс.
Тулаинс Кордова

Ответы:

6

Оба верны

Как я понимаю, цель ISP (принцип разделения интерфейсов) состоит в том, чтобы поддерживать интерфейсы небольшими и сфокусированными: все члены интерфейса должны иметь очень высокую степень сцепления. Оба определения предназначены для того, чтобы избежать интерфейсов "мастер на все руки".

Разделение интерфейса и SRP (принцип единой ответственности) имеют одну и ту же цель: обеспечить небольшие, высокосвязные программные компоненты. Они дополняют друг друга. Разделение интерфейсов гарантирует, что интерфейсы являются небольшими, сфокусированными и очень связными. Следование принципу единой ответственности гарантирует, что классы маленькие, целенаправленные и очень сплоченные.

Первое упоминание, которое вы упоминаете, ориентировано на разработчиков, второе - на клиентов. Который, в отличие от @ user61852, я принимаю за пользователей / абонентов интерфейса, а не за их реализацию.

Я думаю, что ваша путаница проистекает из скрытого предположения в первом определении: что реализующие классы уже следуют принципу единой ответственности.

Для меня второе определение, с клиентами как вызывающими интерфейс, является лучшим способом достижения намеченной цели.

отделяющий

В своем вопросе вы заявляете:

поскольку таким образом мой MyClass может реализовывать только те методы, которые ему нужны (D () и C ()), без необходимости также предоставлять фиктивные реализации для A (), B () и C ():

Но это переворачивает мир с ног на голову.

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

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

Рассмотрим этот интерфейс:

interface IEverythingButTheKitchenSink
{
     void DoDishes();
     void CleanSink();
     void CutGreens();
     void GrillMeat();
}

Какие методы вы бы использовали ICookи почему? Вы бы соединились только CleanSinkс GrillMeatтем, что у вас есть класс, который делает именно это, и пару других вещей, но не похожий ни на один из других методов? Или вы бы разбили его на два более взаимосвязанных интерфейса, таких как:

interface IClean
{
     void DoDishes();
     void CleanSink();
}

interface ICook
{
     void CutGreens();
     void GrillMeat();
}

Примечание к декларации интерфейса

Определение интерфейса предпочтительно должно быть само по себе в отдельном модуле, но если ему абсолютно необходимо жить с вызывающим или реализующим устройством, оно действительно должно быть с вызывающим. В противном случае вызывающая сторона получает непосредственную зависимость от реализатора, который полностью отрицает назначение интерфейсов. См. Также: Объявление интерфейса в том же файле, что и базовый класс, это хорошая практика? на программистов и почему мы должны размещать интерфейсы с классами, которые их используют, а не с теми, которые их реализуют? на StackOverflow.

Марьян Венема
источник
1
Вы видите обновление, которое я сделал?
EdvRusj
«вызывающая сторона получает непосредственную зависимость от реализатора » ... только если вы нарушаете DIP (принцип инверсии зависимостей), если внутренние переменные, параметры, возвращаемые значения и т. д. имеют тип ICookвместо типа SomeCookImplementor, как предписывает DIP, тогда это не не должно зависеть от SomeCookImplementor.
Тулаинс Кордова
@ user61852: Если объявление интерфейса и реализатор находятся в одном модуле, я сразу получаю зависимость от этого реализатора. Не обязательно во время выполнения, но, скорее всего, на уровне проекта, просто потому, что он есть. Проект больше не может компилироваться без него или чего бы то ни было. Кроме того, внедрение зависимости не совпадает с принципом обращения зависимости. Вы можете быть заинтересованы в DIP в дикой природе
Марьян Venema
Я повторно использовал примеры кода в этом вопросе programmers.stackexchange.com/a/271142/61852 , улучшая его после того, как он был уже принят. Я дал вам должное за примеры.
Тулаинс Кордова
Cool @ user61852 :) (и спасибо за кредит)
Марьян Венема
14

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

«Клиент» в соответствии с определениями Gang of Four - это класс, реализующий интерфейс. Если класс A реализует интерфейс B, то они говорят, что A является клиентом B. В противном случае фраза «клиенты не должны принуждать к реализации интерфейсов, которые они не используют» , не имеет смысла, так как «клиенты» (как у потребителей) не имеют ничего не реализую. Фраза имеет смысл только тогда, когда вы видите «клиент» как «разработчик».

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

Суть этого принципа заключается в том, чтобы избежать того, чтобы «клиент» (класс, реализующий интерфейс) реализовывал фиктивные методы для обеспечения соответствия всему интерфейсу, когда он заботится только о наборе связанных методов.

Также он направлен на то, чтобы иметь как можно меньшее количество сцеплений, чтобы изменения, внесенные в одном месте, вызывали меньшее влияние. Разделяя интерфейсы, вы уменьшаете связь.

Эта проблема возникает, когда интерфейс делает слишком много и имеет методы, которые следует разделить на несколько интерфейсов вместо одного.

Оба ваших примера кода в порядке . Только во втором вы предполагаете, что «клиент» означает «класс, который использует / вызывает службы / методы, предлагаемые другим классом».

Я не нахожу противоречий в концепциях, изложенных в трех приведенных вами ссылках.

Просто имейте в виду, что «клиент» является разработчиком , в разговоре SOLID.

Тулаинс Кордова
источник
Но согласно @pdr, хотя примеры кода во всех ссылках действительно соответствуют ISP, определение ISP больше относится к «изоляции клиента (класса, вызывающего методы другого класса) от знания о сервисе больше, чем о» предотвращение принудительного использования клиентами (разработчиками) интерфейсов, которые они не используют ".
EdvRusj
1
@EdvRusj Мой ответ основан на документах на веб-сайте Object Mentor (предприятие Боба Мартина), написанных самим Мартином, когда он был в знаменитой «Банде четырех». Как вы знаете, Gnag of Four - это группа инженеров-программистов, включая Мартина, которые создали аббревиатуру SOLID, определили и задокументировали принципы. docs.google.com/a/cleancoder.com/file/d/…
Тулаинс Кордова
Итак, вы не согласны с @pdr и, таким образом, вы находите первое определение ISP (см. Мой оригинальный пост) более приемлемым?
EdvRusj
@ EdvRusj Я думаю, что оба правы. Но второй добавляет ненужную путаницу, используя метафору клиент / сервер. Если бы мне пришлось выбирать одну, я бы пошел с официальной Бандой четырех, которая является первой. Но что важно, это уменьшить взаимосвязь и ненужные зависимости, что в конце концов является духом принципов SOLID. Неважно, какой из них прав. Важно то, что вы должны разделять интерфейсы в соответствии с поведением. Это все. Но если есть сомнения, просто перейдите к первоисточнику.
Тулаинс Кордова
3
Я так не согласен с вашим утверждением, что «клиент» является разработчиком в SOLID talk. Во-первых, это лингвистическая глупость - называть провайдера (исполнителя) клиентом того, что он предоставляет (реализует). Я также не видел ни одной статьи о SOLID, которая пытается это передать, но я, возможно, просто пропустил это. Наиболее важно, хотя он устанавливает реализатор интерфейса как тот, который решает, что должно быть в интерфейсе. И это не имеет смысла для меня. Вызывающие / пользователи интерфейса определяют, что им нужно от интерфейса, и разработчики (во множественном числе) этого интерфейса обязаны его предоставлять.
Марьян Венема
5

ISP полностью изолирует клиента от того, что он знает больше об услуге, чем ему нужно знать (например, защищает ее от несвязанных изменений). Ваше второе определение верно. На мой взгляд, только одна из этих трех статей предлагает иное ( первая ), и это просто неправильно. (Правка: нет, не так, просто вводит в заблуждение)

Первое определение гораздо более тесно связано с LSP.

прецизионный самописец
источник
3
В ISP клиенты не должны быть вынуждены ПОТРЕБЛЯТЬ компоненты интерфейса, которые они не используют. В LSP сервисы не должны принуждаться к реализации метода D, потому что вызывающему коду требуется метод A. Они не противоречат друг другу, они дополняют друг друга.
pdr
2
@EdvRusj, объект, который реализует InterfaceA, который вызывает ClientA, на самом деле может быть точно таким же объектом, который реализует InterfaceB, необходимый клиенту B. В редких случаях, когда один и тот же клиент должен видеть один и тот же объект в разных классах, код не будет обычно "трогать". Вы будете рассматривать его как A для одной цели и B для другой цели.
Эми Бланкеншип
1
@EdvRusj: Это может помочь, если вы переосмыслите свое определение интерфейса здесь. Это не всегда интерфейс в терминах C # / Java. Вы можете иметь сложную службу с несколькими простыми классами, обернутыми вокруг нее, так что клиент A использует класс-оболочку AX для «взаимодействия» со службой X. Таким образом, когда вы изменяете X таким образом, что влияет на A и AX, вы не вынужден влиять на BX и B.
pdr
1
@EdvRusj: Было бы точнее сказать, что A и B не волнует, если они оба вызывают X или один вызывает Y, а другой - Z. Это фундаментальная точка ISP. Таким образом, вы можете выбрать, какую реализацию вы хотите использовать, и легко передумать позже. Интернет-провайдер не поддерживает ни один маршрут, ни другой, но LSP и SRP могут.
pdr
1
@EdvRusj Нет, клиент A сможет заменить Service X на Service y, оба из которых будут реализовывать интерфейс AX. X и / или Y могут реализовывать другие интерфейсы, но когда клиент вызывает их как AX, он не заботится об этих других интерфейсах.
Эми Бланкеншип