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


14

Читаючи статті про Інтернет-провайдера, схоже, є два суперечливих визначення ISP:

Згідно з першим визначенням (див. 1 , 2 , 3 ), ISP зазначає, що класи, що реалізують інтерфейс, не повинні змушувати реалізовувати функції, які їм не потрібні. Таким чином, жировий інтерфейс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 , відповідь Назара Мерзи ), ISP стверджує, що для 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();

Таким чином, колишнє визначення, мета провайдера - " першим полегшити життя класів, що реалізують інтерфейс IFat ", тоді як з останньою метою ISP - " полегшити життя клієнтів, що викликають методи MyService ".

Яке з двох різних визначень провайдера насправді правильне?

@MARJAN VENEMA

1.

Отже, коли ви збираєтеся розділити IFat на менший інтерфейс, які методи в кінцевому підсумку, в якому ISmallinterface слід вирішити, виходячи з того, наскільки згуртовані члени.

Хоча має сенс розміщувати згуртовані методи в одному інтерфейсі, я вважав, що за допомогою шаблону ISP потреби клієнта мають перевагу над "згуртованістю" інтерфейсу. Іншими словами, я подумав, що з ISP ми повинні зв'язати в одному інтерфейсі ті методи, які потрібні певним клієнтам, навіть якщо це означає, що виключати з цього інтерфейсу ті методи, які, заради згуртованості, також повинні бути розміщені всередині того самого інтерфейсу?

Таким чином, якщо було багато клієнтів, яким потрібно буде коли-небудь зателефонувати CutGreens, але не також GrillMeat, тоді дотримуватися шаблону провайдера ми повинні лише вкладати CutGreensвсередину ICook, але не також GrillMeat, хоча ці два методи є дуже згуртованими ?!

2.

Я думаю, що ваша плутанина випливає з прихованого припущення в першому визначенні: що класи-реалізатори вже дотримуються принципу єдиної відповідальності.

Під "реалізацією класів, що не відповідають SRP" ви посилаєтесь на ті класи, які реалізують, IFatабо на класи, які реалізують ISmall_1/ ISmall_2? Я припускаю, що ви маєте на увазі класи, які реалізують IFat? Якщо так, то чому ви вважаєте, що вони вже не дотримуються SRP?

Спасибі


4
Чому не може бути декількох визначень, які обидва подаються за одним принципом?
Бобсон

5
Ці визначення не суперечать одне одному.
Майк Партрідж

1
Звичайно, потреби клієнта не мають переваги над згуртованістю інтерфейсу. Ви можете скористатися цим "правилом" способом далеко і в кінцевому підсумку з єдиними методами інтерфейсів у всіх місцях, які абсолютно не мають сенсу. Перестаньте дотримуватися правил і починайте думати про цілі, заради яких ці правила були створені. З "класами, що не відповідають SRP", я не говорив про якісь конкретні класи у вашому прикладі або про те, що вони вже не слідкують за SRP. Прочитайте ще раз. Перше визначення призводить до розщеплення інтерфейсу лише в тому випадку, якщо інтерфейс не відповідає ISP, а клас відповідає SRP.
Мар'ян Венема

2
Друге визначення не стосується реалізаторів. Він визначає інтерфейси з погляду абонентів і не робить жодних припущень щодо того, існують чи ні реалізатори. Це, мабуть, передбачає, що якщо ви дотримуєтесь ISP і приступаєте до реалізації цих інтерфейсів, ви, звичайно, дотримуєтесь SRP під час їх створення.
Мар'ян Венема

2
Як ви заздалегідь знаєте, які клієнти існуватимуть і які методи будуть потрібні? Ви не можете. Те, що ви можете знати заздалегідь, - наскільки згуртований ваш інтерфейс.
Тулен Кордова

Відповіді:


6

Обидва вірні

Завдяки тому, як я читаю, мета провайдера (Принцип розбиття інтерфейсу) полягає в тому, щоб інтерфейси були невеликими та зосередженими: всі члени інтерфейсу повинні мати дуже високу згуртованість. Обидва визначення призначені для уникнення інтерфейсів "jack-of-all-trades-master-of-none".

Сегрегація інтерфейсу та 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.
Tulains Córdova

@ user61852: Якщо декларація інтерфейсу та реалізатор знаходяться в одному блоці, я негайно отримую залежність від цього реалізатора. Не обов’язково під час виконання, але, звичайно, на рівні проекту, просто тим, що він там є. Проект вже не може складатись без нього чи що б він не використовував. Крім того, ін'єкція залежності залежить від принципу інверсії залежності. Можливо, вас зацікавить DIP в дикій природі
Мар'ян Венема

Я повторно використав приклади вашого коду в цьому питанні programmers.stackexchange.com/a/271142/61852 , вдосконалюючи його після того, як він вже був прийнятий. Я надав вам належну заслугу за приклади.
Tulains Córdova

Cool @ user61852 :) (і спасибі за заслуги)
Marjan Venema

14

Ви плутаєте слово "клієнт", яке використовується в документах "Банда чотирьох", з "клієнтом", як у споживача послуги.

"Клієнт", за задумом визначення Gang of Four, - це клас, який реалізує інтерфейс. Якщо клас A реалізує інтерфейс B, то вони кажуть, що A є клієнтом B. Інакше фраза "клієнтів не слід змушувати реалізовувати інтерфейси, які вони не використовують" не мала б сенсу, оскільки "клієнти" (як у споживачів) не нічого не реалізую. Фраза має сенс лише тоді, коли ви бачите "клієнта" як "виконавця".

Якщо "клієнт" мав на увазі клас, який "споживає" (викликає) методи іншого класу, який реалізує великий інтерфейс, то, зателефонувавши до двох ваших методів і проігнорувавши решту, було б достатньо, щоб ви не були відокремлені від решти методи, які ви не використовуєте.

Суть принципу полягає у тому, щоб уникнути того, щоб "клієнту" (класу, що реалізує інтерфейс), довелося реалізовувати фіктивні методи, щоб відповідати цілому інтерфейсу, коли він піклується лише про набір пов'язаних методів.

Крім того, вона спрямована на те, щоб якомога менше було з'єднання, щоб зміни, зроблені в одному місці, спричинили менший вплив. Сегрегуючи інтерфейси, ви зменшуєте зв'язок.

Ці проблеми з’являються, коли інтерфейс робить занадто багато і мають методи, які слід розділити на кілька інтерфейсів, а не лише один.

Обидва ваші приклади коду в порядку . Це лише те, що у другому ви припускаєте, що "клієнт" означає "клас, який споживає / викликає послуги / методи, запропоновані іншим класом".

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

Просто майте зрозуміти, що "клієнт" - це реалізатор у SOLID talk.


Але, згідно з @pdr, хоча приклади коду у всіх посиланнях дотримуються провайдера, визначення провайдера більше стосується "ізоляції клієнта (класу, який викликає методи іншого класу) від знання більше про послугу", ніж про " запобігання змушенню клієнтів (виконавців) впроваджувати інтерфейси, які вони не використовують ".
EdvRusj

1
@EdvRusj Моя відповідь ґрунтується на документах на веб-сайті Object Mentor (підприємство Bob Martin), написаних самим Мартіном, коли він був у знаменитій банді чотирьох. Як відомо, Gnag of Four була групою інженерів програмного забезпечення, включаючи Мартіна, яка створила акронім SOLID, визначила та задокументувала принципи. docs.google.com/a/cleancoder.com/file/d/…
Tulains Córdova

Отже, ви не згодні з @pdr, і таким чином ви вважаєте, що перше визначення провайдера (див. Моє оригінальне повідомлення) більш приємним?
EdvRusj

@EdvRusj Я думаю, що обидва мають рацію. Але другий додає зайвої плутанини за допомогою метафори клієнт / сервер. Якщо мені доведеться вибрати один, я б поїхав із офіційною бандою чотирьох, яка перша. Але що важливо, щоб зменшити зв’язок та зайві залежності, що є духом принципів SOLID. Не має значення, хто з них правий. Важливим є те, що ви повинні відокремити інтерфейси відповідно до поведінки. Це все. Але коли ви сумніваєтесь, просто перейдіть до першоджерела.
Tulains Córdova

3
Я так не згоден з вашим твердженням, що "клієнт" є реалізатором у SOLID розмові. Для однієї мовної дурниці називати провайдера (реалізатором) клієнтом того, що він надає (реалізує). Я також не бачив жодної статті про SOLID, яка намагається передати це, але я, можливо, просто пропустила це. Найголовніше, хоча він встановлює реалізатор інтерфейсу як той, що вирішує, що має бути в інтерфейсі. І це не має для мене сенсу. Абоненти / користувачі інтерфейсу визначають, що їм потрібно від інтерфейсу, а реалізатори (множини) цього інтерфейсу зобов'язані його надати.
Мар'ян Венема

5

Інтернет-провайдер полягає в тому, щоб відмовити клієнта від того, щоб знати більше про послугу, ніж він повинен знати (захищаючи її, наприклад, від непов'язаних змін). Ваше друге визначення правильне. На мій прочитання, лише одна з цих трьох статей говорить про інше ( перша ), і це просто неправильно. (Редагувати: Ні, не помиляюся, просто вводячи в оману.)

Перше визначення набагато тісніше пов'язане з LSP.


3
У провайдері клієнтів не слід змушувати споживати компоненти інтерфейсу, які вони не використовують. У LSP SERVICES не слід змушувати реалізовувати метод D, оскільки для викликового коду потрібен метод A. Вони не суперечать один одному, вони є взаємодоповнюючими.
пдр

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 можуть.
пдр

1
@EdvRusj Ні, клієнт A міг би замінити Service X на Service y, обидва вони реалізували б інтерфейс AX. X та / або Y можуть реалізовувати інші інтерфейси, але коли Клієнт називає їх як AX, це не хвилює цих інших інтерфейсів.
Емі Бланкенсіп
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.