Як застосувати принцип розбиття інтерфейсу в C?


15

У мене є модуль, скажімо, "M", у якого є кілька клієнтів, скажімо, "C1", "C2", "C3". Я хочу розподілити простір імен модуля M, тобто декларації API та даних, які він відкриває, у файли заголовків таким чином, що -

  1. для будь-якого клієнта видно лише ті дані та API, які йому потрібні; решта простору імен модуля прихована від клієнта, тобто дотримуйтесь принципу розбиття інтерфейсу .
  2. декларація не повторюється у кількох файлах заголовків, тобто не порушує DRY .
  3. модуль M не має ніяких залежностей від своїх клієнтів.
  4. клієнт не впливає на зміни, внесені в частини модуля M, які не використовуються.
  5. існуючі клієнти не впливають на додавання (або видалення) більшої кількості клієнтів.

В даний час я займаюся цим, розділяючи простір імен модуля залежно від потреб його клієнтів. Наприклад, на зображенні нижче зображено різні частини простору імен модуля, необхідні його 3 клієнтам. Клієнтські вимоги перекриваються. Простір імен модуля розділено на 4 окремі файли заголовків - 'a', '1', '2' та '3' .

Розбиття простору імен модулів

Однак це порушує деякі вищезазначені вимоги, тобто R3 та R5. Вимога 3 порушена, оскільки цей розподіл залежить від характеру клієнтів; також при додаванні нового клієнта цей розділ змінюється і порушує вимогу 5. Як видно з правого боку вищевказаного зображення, при додаванні нового клієнта простір імен модуля тепер розділений на 7 файлів заголовків - 'a ',' b ',' c ',' 1 ',' 2 * ',' 3 * 'і' 4 ' . Файли заголовків, призначені для двох старших клієнтів, змінюються, тим самим викликаючи їх перебудову.

Чи є спосіб досягти інтерфейсної сегрегації в C неусмисленим способом?
Якщо так, то як би ви поводилися з наведеним вище прикладом?

Я думаю, було б нереальне гіпотетичне рішення -
Модуль має 1 жирний заголовок, що охоплює все його ім’я. Цей заголовочний файл розділений на адреси, що містять адреси, і підрозділи, такі як сторінка Вікіпедії. Потім у кожного клієнта є специфічний файл заголовка, призначений для нього. Спеціальні для клієнта файли заголовків - це лише список гіперпосилань на розділи / підрозділи файлу заголовка жиру. І система збирання повинна розпізнавати специфічний для клієнта файл заголовка як «модифікований», якщо будь-який з розділів, на який він вказує у заголовку Модуля, змінений.


1
Чому ця проблема характерна лише для C? Це тому, що C не має спадщини?
Роберт Харві

Крім того, чи порушує ваш провайдер кращі роботи в дизайні?
Роберт Харві

2
C насправді не підтримує поняття OOP (наприклад, інтерфейси чи успадкування). Ми робимо грубі (але творчі) хаки. Шукаєте хак для імітації інтерфейсів. Як правило, весь файл заголовка є інтерфейсом до модуля.
work.bin

1
structце те, що ви використовуєте в C, коли потрібно інтерфейс. Зрозуміло, методи трохи складні. Вам це може бути цікаво: cs.rit.edu/~ats/books/ooc.pdf
Роберт Харві

Я не міг придумати еквівалент інтерфейсу за допомогою structі function pointers.
work.bin

Відповіді:


5

Сегрегація інтерфейсу, як правило, не повинна базуватися на вимогах клієнта. Вам слід змінити весь підхід, щоб досягти цього. Я б сказав, модулюйте інтерфейс, групуючи функції в когерентні групи. Тобто групування засноване на узгодженості самих функцій, а не вимог клієнта. У такому випадку у вас буде набір інтерфейсів, I1, I2, ... і т.д. Клієнт C1 може використовувати I2 самостійно. Клієнт C2 може використовувати I1 та I5 тощо. Зверніть увагу, що якщо клієнт використовує більше одного Ii, це не проблема. Якщо ви розклали інтерфейс на когерентні модулі, саме там і лежить суть справи.

Знову ж таки, ISP не базується на клієнтах. Йдеться про розкладання інтерфейсу на менші модулі. Якщо це зроблено належним чином, це також забезпечить, щоб клієнти були піддані такої кількості функцій, скільки їм потрібно.

При такому підході ваші клієнти можуть збільшуватися до будь-якої кількості, але на вас M не впливає. Кожен клієнт буде використовувати одну чи якусь комбінацію інтерфейсів, виходячи зі своєї потреби. Чи будуть випадки, коли клієнту, C, потрібно включити, наприклад, I1 та I3, але не використовувати всі функції цих інтерфейсів? Так, це не проблема. Просто використовується найменша кількість інтерфейсів.


Ви , звичайно , мав в виду НЕ перетинаються або неперекривающіеся групи, я гадаю?
Док Браун

Так, нерозбірливі та неперекриваються.
Назар Мерза

3

Інтерфейс Сегрегація Принцип говорить:

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

Тут є кілька питань без відповіді. Одне:

Як маленький?

Ти кажеш:

В даний час я займаюся цим, розділяючи простір імен модуля залежно від потреб його клієнтів.

Я називаю це ручне введення качки . Ви будуєте інтерфейси, які розкривають лише те, що потрібно клієнту. Принцип поділу інтерфейсу - це не просто ручне введення качок.

Але ISP - це не просто заклик до "узгоджених" рольових інтерфейсів, які можна використовувати повторно. Жоден "узгоджений" дизайн рольового інтерфейсу не може ідеально захистити від додавання нового клієнта з власними рольовими потребами.

ISP - це спосіб ізолювати клієнтів від впливу змін у сервісі. Потрібно було зробити збірку швидшою, коли ви вносите зміни. Звичайно, це має інші переваги, як, наприклад, не ламати клієнтів, але це було головним моментом. Якщо я змінюю count()підпис функції сервісу, це добре, якщо клієнтам, які не користуються count(), не потрібно редагувати та перекомпілювати.

Це ЧОМУ я дбаю про принцип сегрегації інтерфейсів. Я не вважаю, що віра є важливою. Це вирішує справжню проблему.

Тож спосіб його застосування повинен вирішити проблему для вас. Не існує жодного способу відмовитись від мозку, щоб застосувати провайдер, який не можна перемогти лише правильним прикладом необхідних змін. Ви повинні подивитися на те, як система змінюється, і зробити вибір, який дозволить спокійно впоратись. Давайте вивчимо варіанти.

Спершу запитайте себе: чи важко змінити інтерфейс служби зараз? Якщо ні, виходьте на вулицю і грайте, поки не заспокоїтесь. Це не інтелектуальна вправа. Будь ласка, переконайтеся, що вилікування не гірше захворювання.

  1. Якщо багато клієнтів використовують один і той же підмножина функцій, це аргументується "узгодженими" багаторазовими інтерфейсами. Підмножина, ймовірно, зосереджена на одній ідеї, яку ми можемо розглядати як роль, яку надає послуга клієнту. Приємно, коли це працює. Це не завжди працює.

  2.  

    1. Якщо багато клієнтів використовують різні підмножини функцій, можливо, клієнт насправді використовує послугу через кілька ролей. Це нормально, але ролі важко помітити. Знайдіть їх і спробуйте розірвати їх на частини. Це може повернути нас у випадку 1. Клієнт просто використовує послугу через більш ніж один інтерфейс. Будь ласка, не починайте кастинг послуги. Якщо що-небудь, це означало б передачу послуги клієнту не раз. Це працює, але це змушує мене сумніватися, чи служба не є великою кулькою грязі, яку потрібно розбити.

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

  3. Якщо багато клієнтів використовують різні підмножини, перекриваються, очікується додавання нових клієнтів, які потребуватимуть непередбачуваних підмножин, і ви не бажаєте розбивати послугу, тоді розглядайте більш функціональне рішення. Оскільки перші два варіанти не спрацювали, і ви насправді в поганому місці, де нічого не слідує шаблону, і вноситься більше змін, тоді подумайте про надання кожній функції власного інтерфейсу. Покінчити тут не означає, що провайдер провалився. Якщо щось не вдалося, це була об'єктно-орієнтована парадигма. Інтерфейси єдиного методу слідують провайдеру в крайніх випадках. Це досить трохи введення клавіатури, але ви можете виявити, що це раптом робить інтерфейси багаторазовими. Знову ж таки, будьте впевнені, що немає

Тож виявляється, вони дійсно можуть отримати дуже малі.

Я сприйняв це питання як завдання застосувати Інтернет-провайдер в самих крайніх випадках. Але майте на увазі, що крайнощів найкраще уникати. У продуманому дизайні, який застосовує інші принципи SOLID, ці проблеми зазвичай не виникають і не мають значення, майже стільки ж.


Ще одне питання без відповіді:

Кому належать ці інтерфейси?

Попереду я бачу інтерфейси, розроблені з тим, що я називаю "бібліотечним" менталітетом. Всі ми були винні в кодуванні мавпи-бачити-мавпи-робити, де ви просто щось робите, бо саме так ви бачили це. Ми винні в тому ж, що з інтерфейсами.

Коли я дивлюся на інтерфейс, призначений для класу в бібліотеці, я думав: о, ці хлопці - професіонали. Це повинен бути правильним способом зробити інтерфейс. Мені не вдалося зрозуміти, що межа бібліотеки має власні потреби та проблеми. З одного боку, бібліотека зовсім не знає дизайну своїх клієнтів. Не кожна межа однакова. І іноді навіть одна і та ж межа має різні способи її перетину.

Ось два простих способи розглянути дизайн інтерфейсу:

  • Інтерфейс, що належить сервісу. Деякі люди розробляють кожен інтерфейс, щоб викрити все, що може зробити послуга. Ви навіть можете знайти параметри рефакторингу в IDE, які будуть писати інтерфейс для вас, використовуючи будь-який клас, яким ви його подаєте.

  • Клієнтський інтерфейс. ISP, здається, стверджує, що це правильно, а служба, що належить, неправильна. Ви повинні мати на увазі кожен інтерфейс із потребами клієнтів. Оскільки клієнт має інтерфейс, він повинен його визначити.

То хто прав?

Розгляньте плагіни:

введіть тут опис зображення

Кому належать тут інтерфейси? Клієнти? Послуги?

Виходить і те, і інше.

Кольори тут - шари. Червоний шар (праворуч) не повинен нічого знати про зелений шар (зліва). Зелений шар можна змінити або замінити, не торкаючись червоного шару. Таким чином будь-який зелений шар може бути підключений до червоного шару.

Мені подобається знати, що слід знати про те, а що не слід знати. Для мене "що знає про що?" - єдине найважливіше архітектурне питання.

Зробимо зрозумілий лексику:

[Client] --> [Interface] <|-- [Service]

----- Flow ----- of ----- control ---->

Клієнт - це те, що використовує.

Послуга - це те, що використовується.

Interactor трапляється і те, і інше.

ISP каже, що розбиваються інтерфейси для клієнтів. Добре, давайте застосувати це тут:

  • Presenter(послуга) не повинна диктувати Output Port <I>інтерфейс. Інтерфейс повинен бути звужений до того, що Interactor(тут виступаючи як клієнт) потрібно. Це означає, що інтерфейс ЗНАЄ про Interactorта, щоб слідкувати за ISP, повинен змінюватися разом із ним. І це прекрасно.

  • Interactor(тут виступає в ролі служби) не повинно диктувати Input Port <I>інтерфейс. Інтерфейс повинен бути звужений до того, що потрібно Controller(клієнту). Це означає, що інтерфейс ЗНАЄ про Controllerта, щоб слідкувати за ISP, повинен змінюватися разом із ним. І це не добре.

Другий - не добре, оскільки червоний шар не повинен знати про зелений шар. Так помиляється ISP? Добре начебто. Жоден принцип не є абсолютним. Це випадок, коли кози, яким подобається інтерфейс, щоб показати все, що може зробити служба, виявляються правильними.

Принаймні, вони мають рацію, якщо Interactorне роблять нічого іншого, ніж це потребує випадку використання. Якщо це Interactorробиться для інших випадків використання, немає ніяких причин, це Input Port <I>повинно знати про них. Не впевнений, чому Interactorне можна просто зосередитись на одному випадку використання, тому це не проблема, але все відбувається.

Але input port <I>інтерфейс просто не може вести себе Controllerклієнту, і це може бути справжнім плагіном. Це межа "бібліотеки". Зовсім інший магазин програмування може писати зелений шар через роки після публікації червоного шару.

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

Один із способів зняти це - адаптер. Розмістіть його між клієнтами типу Controlerта Input Port <I>інтерфейсом. Адаптер приймає Interactorяк Input Port <I>і передає свою роботу йому. Однак він відкриває лише те, що Controllerпотрібно клієнтам через рольовий інтерфейс або інтерфейси, що належать зеленому шару. Адаптер не відповідає ISP, але дозволяє більш складним класам, як Controllerнасолоджуватися ISP. Це корисно, якщо є менше адаптерів, ніж такі Controller, як користуються клієнти , і коли ви перебуваєте в незвичній ситуації, коли ви перетинаєте межу бібліотеки, і, незважаючи на публікацію, бібліотека не перестане змінюватися. Дивлячись на тебе Firefox. Тепер ці зміни розбивають лише ваші адаптери.

То що це означає? Це означає, чесно, ви не надали мені достатньо інформації, щоб сказати, що вам робити. Я не знаю, якщо недотримання ISP викликає у вас проблему. Я не знаю, якщо дотримання цього не призведе до більшої кількості проблем.

Я знаю, ви шукаєте простий керівний принцип. ISP намагається бути таким. Але це залишає багато не сказаного. Я вірю в це. Так, будь ласка, не змушуйте клієнтів залежати від методів, якими вони не користуються, без поважних причин!

Якщо у вас є вагомі причини, як-от розробка дизайну для прийняття плагінів, знайте про проблеми, які не слідкують за причинами провайдера (важко змінити, не порушуючи клієнтів), і способи їх пом'якшення (зберегти Interactorабо принаймні Input Port <I>зосередитись на одному стабільному випадок використання).


Дякуємо за вклад. У мене є модуль надання послуг, який має декілька клієнтів. Її простір імен має логічно узгоджені межі, але клієнт повинен перетинати ці логічні межі. Тим самим поділ простору імен на основі логічних меж не допомагає провайдеру. Тому я розділив простір імен на основі потреб клієнта, як показано на схемі у запитанні. Але це робить його залежним від клієнтів і поганим способом приєднання клієнтів до послуги, оскільки клієнтів можна додавати / видаляти порівняно часто, але зміни в сервісі будуть мінімальними.
work.bin

Зараз я схиляюся до сервісу, що надає жирний інтерфейс, як у його повному просторі імен, і клієнт має доступ до цих послуг через адаптери, призначені для клієнта. З точки зору, це буде файл функції обгортки, що належить клієнту. Зміни в сервісі примусять перекомпіляцію адаптера, але не обов'язково клієнта. .. <
contd

<contd> .. Це, безумовно, буде тримати час збирання мінімальним і тримати зв'язок між клієнтом і сервісом 'вільним' за рахунок часу виконання (виклик посередницької функції обгортки), збільшує кодовий простір, збільшує використання стека та, ймовірно, більше простору розуму (програміст) у підтримці адаптерів.
work.bin

Моє поточне рішення задовольняє мої потреби зараз, новий підхід потребує більше зусиль і, можливо, може порушити YAGNI. Мені доведеться зважити плюси і мінуси кожного методу і вирішити, яким шляхом іти сюди.
work.bin

1

Отже, цей момент:

existent clients are unaffected by the addition (or deletion) of more clients.

Відмовляється від того, що ви порушуєте інший важливий принцип, який є ЯГНІ. Я б переймався цим, коли маю сотні клієнтів. Якщо подумати про щось наперед, то виявиться, що у вас немає додаткових клієнтів для цього коду, що досягає мети.

Друге

 partitioning depends on the nature of clients

Чому ваш код не використовує DI, інверсію залежності, нічого, нічого у вашій бібліотеці не повинно залежати від характеру вашого клієнта.

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

для будь-якого клієнта видно лише ті дані та API, які йому потрібні; решта простору імен модуля прихована від клієнта, тобто дотримуйтесь принципу розбиття інтерфейсу.

так

декларація не повторюється у кількох файлах заголовків, тобто не порушує DRY. модуль M не має ніяких залежностей від своїх клієнтів.

так

клієнт не впливає на зміни, внесені в частини модуля M, які не використовуються.

так

існуючі клієнти не впливають на додавання (або видалення) більшої кількості клієнтів.

так


1

Ті самі відомості, що наведені в декларації, завжди повторюються у визначенні. Це просто так, як ця мова працює. Крім того, повторення декларації у кількох файлах заголовків не порушує DRY . Це досить поширена методика (принаймні у стандартній бібліотеці).

Повторення документації або реалізація порушить DRY .

Я б не переймався цим, якщо код клієнта не написаний мною.


0

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

Структура зразка

M.h      // fat header
 - P1    // Partition 1
 - P2    // ... 2
   - P21 // ... 2 section 1
 - P3    // ... 3
C1.c     // Client 1 (Needs to include P1, P3)
C2.c     // ... 2 (Needs to include P2)
C3.c     // ... 3 (Needs to include P1, P21, P3)

Mh

#ifdef P1
#define _PREF_ P1_             // Define Prefix ("PREF") = P1_
 void _PREF_init();            // Some partition specific function
#endif /* P1 */

#ifdef P2
#define _PREF_ P2_
 void _PREF_init();
#endif /* P2 */

#if defined(P21) || defined (P2) // Part 2.1
#define _PREF_ P2_1_
 void _PREF_oddone();
#endif /* P21 */

#ifdef P3
#define _PREF_ P3_
 void _PREF_init();
#endif /* P3 */

Мак

У файлі Mc вам фактично не доведеться використовувати #ifdefs, оскільки те, що ви помістите у файл .c, не впливає на клієнтські файли до тих пір, поки визначені функції, які використовуються файлами клієнта.

#include "M.h"
#define _PREF_ P1_        
void _PREF_init() { ... };

#define _PREF_ P2_
void _PREF_init() { ... }

#define _PREF_ P2_1_
void _PREF_oddone() { ... }

#define _PREF_ P3_
void _PREF_init() { ... }

C1.c

#define P1     // "invite" P1
#define P3     // "invite" P3
#include "M.h" // Open the door, but only the invited come in.

void main()
{
    P1_init();
    //P2_init();
    //P2_1_oddone();
    P3_init();
}

C2.c

#define P2
#include "M.h

void main()
{
    //P1_init();
    P2_init();
    P2_1_oddone();
    //P3_init();
}

C3.c

#define P1
#define P21
#define P3  
#include "M.h" 

void main()
{
    P1_init();
    //P2_init();
    P2_1_oddone();
    P3_init();
}

Знову ж таки, я не впевнений, чи це ви просите. Тому візьміть його з зерном солі.


Як виглядає Мак? Ви визначаєте P1_init() і P2_init() ?
work.bin

@ work.bin Я припускаю, що Mc виглядатиме як простий .c файл, за винятком визначення простору імен між функціями.
Sanchke Dellowar

Якщо припустити, що і C1, і C2 існують - що робить P1_init()і P2_init()посилається на це?
work.bin

У файлі Mh / Mc препроцесор замінить _PREF_тим, що було останньо визначено. Так _PREF_init()буде і P1_init()через останню #define заяву. Тоді наступний оператор визначення встановить PREF рівним P2_, таким чином, генеруючи P2_init().
Sanchke Dellowar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.