При чтении статей об 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?
Благодарность
источник
Ответы:
Оба верны
Как я понимаю, цель ISP (принцип разделения интерфейсов) состоит в том, чтобы поддерживать интерфейсы небольшими и сфокусированными: все члены интерфейса должны иметь очень высокую степень сцепления. Оба определения предназначены для того, чтобы избежать интерфейсов "мастер на все руки".
Разделение интерфейса и SRP (принцип единой ответственности) имеют одну и ту же цель: обеспечить небольшие, высокосвязные программные компоненты. Они дополняют друг друга. Разделение интерфейсов гарантирует, что интерфейсы являются небольшими, сфокусированными и очень связными. Следование принципу единой ответственности гарантирует, что классы маленькие, целенаправленные и очень сплоченные.
Первое упоминание, которое вы упоминаете, ориентировано на разработчиков, второе - на клиентов. Который, в отличие от @ user61852, я принимаю за пользователей / абонентов интерфейса, а не за их реализацию.
Я думаю, что ваша путаница проистекает из скрытого предположения в первом определении: что реализующие классы уже следуют принципу единой ответственности.
Для меня второе определение, с клиентами как вызывающими интерфейс, является лучшим способом достижения намеченной цели.
отделяющий
В своем вопросе вы заявляете:
Но это переворачивает мир с ног на голову.
Поэтому, когда вы собираетесь разделить
IFat
интерфейс на более мелкие, какие методы в конечном итогеISmall
должны быть выбраны, исходя из того, насколько сплоченными являются участники.Рассмотрим этот интерфейс:
Какие методы вы бы использовали
ICook
и почему? Вы бы соединились толькоCleanSink
сGrillMeat
тем, что у вас есть класс, который делает именно это, и пару других вещей, но не похожий ни на один из других методов? Или вы бы разбили его на два более взаимосвязанных интерфейса, таких как:Примечание к декларации интерфейса
Определение интерфейса предпочтительно должно быть само по себе в отдельном модуле, но если ему абсолютно необходимо жить с вызывающим или реализующим устройством, оно действительно должно быть с вызывающим. В противном случае вызывающая сторона получает непосредственную зависимость от реализатора, который полностью отрицает назначение интерфейсов. См. Также: Объявление интерфейса в том же файле, что и базовый класс, это хорошая практика? на программистов и почему мы должны размещать интерфейсы с классами, которые их используют, а не с теми, которые их реализуют? на StackOverflow.
источник
ICook
вместо типаSomeCookImplementor
, как предписывает DIP, тогда это не не должно зависеть отSomeCookImplementor
.Вы путаете слово «клиент», которое используется в документах «Банды четырех», с словом «клиент», как в потребителе услуги.
«Клиент» в соответствии с определениями Gang of Four - это класс, реализующий интерфейс. Если класс A реализует интерфейс B, то они говорят, что A является клиентом B. В противном случае фраза «клиенты не должны принуждать к реализации интерфейсов, которые они не используют» , не имеет смысла, так как «клиенты» (как у потребителей) не имеют ничего не реализую. Фраза имеет смысл только тогда, когда вы видите «клиент» как «разработчик».
Если под «клиентом» подразумевается класс, который «потребляет» (вызывает) методы другого класса, реализующего большой интерфейс, то достаточно вызвать два нужных вам метода и игнорировать остальные, чтобы вы были отделены от остальных методы, которые вы не используете.
Суть этого принципа заключается в том, чтобы избежать того, чтобы «клиент» (класс, реализующий интерфейс) реализовывал фиктивные методы для обеспечения соответствия всему интерфейсу, когда он заботится только о наборе связанных методов.
Также он направлен на то, чтобы иметь как можно меньшее количество сцеплений, чтобы изменения, внесенные в одном месте, вызывали меньшее влияние. Разделяя интерфейсы, вы уменьшаете связь.
Эта проблема возникает, когда интерфейс делает слишком много и имеет методы, которые следует разделить на несколько интерфейсов вместо одного.
Оба ваших примера кода в порядке . Только во втором вы предполагаете, что «клиент» означает «класс, который использует / вызывает службы / методы, предлагаемые другим классом».
Я не нахожу противоречий в концепциях, изложенных в трех приведенных вами ссылках.
Просто имейте в виду, что «клиент» является разработчиком , в разговоре SOLID.
источник
ISP полностью изолирует клиента от того, что он знает больше об услуге, чем ему нужно знать (например, защищает ее от несвязанных изменений). Ваше второе определение верно. На мой взгляд, только одна из этих трех статей предлагает иное ( первая ), и это просто неправильно. (Правка: нет, не так, просто вводит в заблуждение)
Первое определение гораздо более тесно связано с LSP.
источник