Jak zastosować zasadę segregacji interfejsów w C?

15

Mam moduł, powiedz „M”, który ma kilku klientów, powiedz „C1”, „C2”, „C3”. Chcę podzielić przestrzeń nazw modułu M, tj. Deklaracje interfejsów API i danych, które udostępnia, do plików nagłówkowych w taki sposób, aby -

  1. dla każdego klienta widoczne są tylko wymagane przez niego dane i interfejsy API; reszta przestrzeni nazw modułu jest ukryta przed klientem, tj. przestrzegaj zasady segregacji interfejsu .
  2. deklaracja nie jest powtarzana w wielu plikach nagłówkowych, tj. nie narusza DRY .
  3. moduł M nie ma żadnych zależności od swoich klientów.
  4. na klienta nie mają wpływu zmiany dokonane w częściach modułu M, które nie są przez niego używane.
  5. na istniejących klientów nie ma wpływu dodanie (lub usunięcie) większej liczby klientów.

Obecnie radzę sobie z tym, dzieląc przestrzeń nazw modułu w zależności od wymagań jego klientów. Na przykład na poniższym obrazku pokazano różne części przestrzeni nazw modułu wymaganej przez 3 klientów. Wymagania klienta pokrywają się. Przestrzeń nazw modułu podzielona jest na 4 osobne pliki nagłówkowe - „a”, „1”, „2” i „3” .

Partycjonowanie przestrzeni nazw modułów

Jednak narusza to niektóre z wyżej wymienionych wymagań, tj. R3 i R5. Wymaganie 3 zostało naruszone, ponieważ partycjonowanie zależy od charakteru klientów; także po dodaniu nowego klienta podział ten zmienia się i narusza wymaganie 5. Jak widać po prawej stronie powyższego obrazu, przestrzeń robocza modułu jest teraz podzielona na 7 plików nagłówkowych po dodaniu nowego klienta - „a ”,„ b ”,„ c ”,„ 1 ”,„ 2 * ”,„ 3 * ”i„ 4 ” . Pliki nagłówkowe przeznaczone dla 2 starszych zmian klientów, powodując w ten sposób ich odbudowę.

Czy istnieje sposób na osiągnięcie segregacji interfejsu w C w nieskomplikowany sposób?
Jeśli tak, jak poradziłbyś sobie z powyższym przykładem?

Nierealne hipotetyczne rozwiązanie, które, jak sobie wyobrażam, byłoby -
Moduł ma 1 gruby plik nagłówka obejmujący całą przestrzeń nazw. Ten plik nagłówka jest podzielony na adresowalne sekcje i podsekcje, takie jak strona Wikipedii. Każdy klient ma następnie dostosowany do niego określony plik nagłówka. Pliki nagłówkowe specyficzne dla klienta to tylko lista hiperłączy do sekcji / podsekcji grubego pliku nagłówkowego. System kompilacji musi rozpoznać specyficzny dla klienta plik nagłówka jako „zmodyfikowany”, jeśli jakakolwiek sekcja wskazana w nagłówku modułu jest zmodyfikowana.

work.bin
źródło
1
Dlaczego ten problem jest specyficzny dla C? Czy to dlatego, że C nie ma dziedzictwa?
Robert Harvey
Ponadto, czy pogwałcenie ISP sprawia, że ​​Twój projekt działa lepiej?
Robert Harvey
2
C tak naprawdę nie obsługuje pojęć związanych z OOP (takich jak interfejsy lub dziedziczenie). Robimy zgrubne (ale kreatywne) hacki. Szukasz hacka do symulacji interfejsów. Zazwyczaj cały plik nagłówka stanowi interfejs modułu.
work.bin
1
structjest to, czego używasz w C, gdy potrzebujesz interfejsu. To prawda, że ​​metody są trochę trudne. Może Cię to zainteresować: cs.rit.edu/~ats/books/ooc.pdf
Robert Harvey
Nie mogłem wymyślić interfejsu równoważnego przy użyciu structi function pointers.
work.bin

Odpowiedzi:

5

Segregacja interfejsu zasadniczo nie powinna opierać się na wymaganiach klienta. Powinieneś zmienić całe podejście, aby to osiągnąć. Powiedziałbym, że można zmodularyzować interfejs, grupując funkcje w spójne grupy. To znaczy, że grupowanie opiera się na spójności samych funkcji, a nie na wymaganiach klienta. W takim przypadku będziesz mieć zestaw interfejsów, I1, I2, ... itd. Klient C1 może używać samego I2. Klient C2 może korzystać z I1 i I5 itd. Pamiętaj, że jeśli klient używa więcej niż jednego Ii, nie stanowi problemu. Jeśli rozłożyłeś interfejs na spójne moduły, to właśnie w tym tkwi sedno sprawy.

Ponownie, ISP nie jest oparty na kliencie. Chodzi o rozkład interfejsu na mniejsze moduły. Jeśli zostanie to zrobione poprawnie, zapewni również, że klienci będą narażeni na tak mało funkcji, jak potrzebują.

Dzięki takiemu podejściu Twoi klienci mogą powiększać się do dowolnej liczby, ale nie wpływa to na Ciebie. Każdy klient użyje jednej lub kilku kombinacji interfejsów w zależności od potrzeb. Czy zdarzają się przypadki, że klient C musi dołączyć powiedzmy I1 i I3, ale nie będzie korzystać ze wszystkich funkcji tych interfejsów? Tak, to nie jest problem. Po prostu używa najmniejszej liczby interfejsów.

Nazar Merza
źródło
Z pewnością chodziło ci o rozłączne lub nie nakładające się grupy, zakładam?
Doc Brown
Tak, rozłączny i nienakładający się.
Nazar Merza
3

Interfejs Segregacja Zasada mówi:

Żaden klient nie powinien być zmuszany do polegania na metodach, których nie używa. ISP dzieli bardzo duże interfejsy na mniejsze i bardziej szczegółowe, tak aby klienci musieli wiedzieć tylko o interesujących ich metodach.

Tutaj jest kilka pytań bez odpowiedzi. Jeden jest:

Jaki mały?

Mówisz:

Obecnie radzę sobie z tym, dzieląc przestrzeń nazw modułu w zależności od wymagań jego klientów.

Nazywam to ręcznym pisaniem kaczek . Budujesz interfejsy, które ujawniają tylko to, czego potrzebuje klient. Zasadą segregacji interfejsów nie jest po prostu ręczne pisanie kaczek.

Ale dostawca usług internetowych to nie tylko wezwanie do stworzenia „spójnych” interfejsów ról, które można ponownie wykorzystać. Żaden „spójny” interfejs interfejsu roli nie może doskonale zabezpieczyć się przed dodaniem nowego klienta z własnymi potrzebami.

ISP to sposób na odizolowanie klientów od wpływu zmian w usłudze. Jego celem było przyspieszenie kompilacji podczas wprowadzania zmian. Pewnie, że ma inne zalety, takie jak nie łamanie klientów, ale to był główny punkt. Jeśli zmieniam count()podpis funkcji usług , dobrze jest, jeśli nieużywani klienci count()nie muszą być edytowani i rekompilowani.

DLACZEGO zależy mi na zasadzie segregacji interfejsu. Nie uważam tego za tak ważne. Rozwiązuje prawdziwy problem.

Sposób, w jaki powinien być zastosowany, powinien rozwiązać problem. Nie ma rutynowego sposobu na zastosowanie ISP, którego nie można pokonać tylko odpowiednim przykładem potrzebnej zmiany. Powinieneś przyjrzeć się zmianom systemu i dokonać wyborów, które pozwolą ci się uspokoić. Sprawdźmy opcje.

Najpierw zadaj sobie pytanie: czy utrudnianie obecnie zmian w interfejsie usługi? Jeśli nie, wyjdź na zewnątrz i baw się, aż się uspokoisz. To nie jest ćwiczenie intelektualne. Upewnij się, że lekarstwo nie jest gorsze niż choroba.

  1. Jeśli wielu klientów korzysta z tego samego podzbioru funkcji, przemawia to za „spójnymi” interfejsami wielokrotnego użytku. Podzbiór prawdopodobnie skupia się na jednym pomyśle, który możemy uznać za rolę, jaką usługa zapewnia klientowi. Miło, gdy to działa. To nie zawsze działa.

  2.  

    1. Jeśli wielu klientów korzysta z różnych podzbiorów funkcji, możliwe, że klient faktycznie korzysta z usługi przez wiele ról. Zgadza się, ale utrudnia dostrzeżenie ról. Znajdź ich i spróbuj się z nimi drażnić. To może nas z powrotem przenieść na przypadek 1. Klient po prostu korzysta z usługi za pośrednictwem więcej niż jednego interfejsu. Nie zaczynaj przesyłania usługi. Jeśli cokolwiek oznaczałoby to przekazanie usługi klientowi więcej niż jeden raz. To działa, ale zastanawiam się, czy usługa nie jest wielką kulą błota, którą należy rozbić.

    2. Jeśli wielu klientów korzysta z różnych podzbiorów, ale nie widzisz ról, nawet pozwalając klientom korzystać z więcej niż jednego, to nie masz nic lepszego niż pisanie w klawiaturze do projektowania interfejsów. Ten sposób projektowania interfejsów zapewnia, że ​​klient nie jest narażony na działanie nawet jednej funkcji, z której nie korzysta, ale prawie gwarantuje, że dodanie nowego klienta zawsze będzie wymagało dodania nowego interfejsu, którego wdrożenie usługi nie musi wiedzieć o tym będzie interfejs, który agreguje interfejsy ról. Po prostu wymieniliśmy jeden ból na inny.

  3. Jeśli wielu klientów korzysta z różnych podzbiorów, nakładają się, oczekuje się, że zostaną dodani nowi klienci, którzy będą potrzebować nieprzewidzianych podzbiorów, a Ty nie chcesz przerywać usługi, a następnie rozważ bardziej funkcjonalne rozwiązanie. Ponieważ dwie pierwsze opcje nie zadziałały i naprawdę jesteś w złym miejscu, w którym nic nie jest zgodne z wzorcem i nadchodzą kolejne zmiany, zastanów się nad udostępnieniem każdej funkcji własnego interfejsu. Skończenie tutaj nie oznacza, że ​​ISP zawiódł. Jeśli coś zawiodło, był to obiektowy paradygmat. Interfejsy z pojedynczą metodą w skrajnym przypadku podążają za ISP. To trochę pisanie na klawiaturze, ale może się nagle okazać, że interfejsy mogą być ponownie używane. Ponownie upewnij się, że nie ma

Okazuje się, że mogą stać się bardzo małe.

Podjąłem to pytanie jako wyzwanie do zastosowania ISP w najbardziej ekstremalnych przypadkach. Pamiętaj jednak, że najlepiej unikać skrajności. W dobrze przemyślanym projekcie stosującym inne zasady SOLID te problemy zwykle nie występują ani nie mają znaczenia, prawie w takim samym stopniu.


Kolejne pytanie bez odpowiedzi:

Kto jest właścicielem tych interfejsów?

W kółko widzę interfejsy zaprojektowane zgodnie z czymś, co nazywam mentalnością „biblioteki”. Wszyscy jesteśmy winni kodowania małpa-patrz-małpa-do, w którym po prostu coś robisz, ponieważ tak właśnie to widziałeś. To samo jesteśmy winni interfejsom.

Kiedy patrzę na interfejs zaprojektowany dla klasy w bibliotece, pomyślałem: o, ci faceci są zawodowcami. To musi być właściwy sposób wykonania interfejsu. Nie rozumiałem, że granica biblioteki ma swoje potrzeby i problemy. Po pierwsze, biblioteka całkowicie nie zna projektu swoich klientów. Nie każda granica jest taka sama. A czasami nawet ta sama granica ma różne sposoby jej przekroczenia.

Oto dwa proste sposoby spojrzenia na projekt interfejsu:

  • Interfejs serwisowy. Niektórzy ludzie projektują każdy interfejs, aby pokazać wszystko, co usługa może zrobić. Możesz nawet znaleźć opcje refaktoryzacji w IDE, które napiszą dla ciebie interfejs, używając dowolnej klasy, którą go karmisz.

  • Interfejs należący do klienta. Wydaje się, że ISP twierdzi, że jest to słuszne, a własność usługi jest błędna. Powinieneś rozbić każdy interfejs z myślą o potrzebach klientów. Ponieważ klient jest właścicielem interfejsu, powinien go zdefiniować.

Więc kto ma rację?

Rozważ wtyczki:

wprowadź opis zdjęcia tutaj

Kto jest właścicielem interfejsów tutaj? Klienci? Usługi?

Okazuje się jedno i drugie.

Kolory tutaj są warstwami. Czerwona warstwa (po prawej) nie powinna nic wiedzieć o zielonej warstwie (po lewej). Zieloną warstwę można zmienić lub zastąpić bez dotykania czerwonej warstwy. W ten sposób każda zielona warstwa może zostać podłączona do czerwonej warstwy.

Lubię wiedzieć, co powinno wiedzieć o czym, a czego nie powinno wiedzieć. Dla mnie „co wie o czym?” To najważniejsze pytanie architektoniczne.

Wyjaśnijmy trochę słownictwa:

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

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

Klient jest czymś, co wykorzystuje.

Usługa jest czymś, co jest używane.

Interactor tak się składa, że ​​jest to jedno i drugie.

ISP twierdzi, że zrywają interfejsy dla klientów. Dobrze, zastosujmy to tutaj:

  • Presenter(usługa) nie powinna dyktować Output Port <I>interfejsu. Interfejs powinien zostać zawężony do Interactorpotrzeb (tutaj działających jako klient). Oznacza to, że interfejs WIE o Interactori, aby podążać za ISP, musi się z nim zmienić. I w porządku.

  • Interactor(tutaj działający jako usługa) nie powinien dyktować Input Port <I>interfejsu. Interfejs powinien zostać zawężony do Controllerpotrzeb (klienta). Oznacza to, że interfejs WIE o Controlleri, aby podążać za ISP, musi się z nim zmienić. I to nie jest w porządku.

Drugi nie jest w porządku, ponieważ czerwona warstwa nie powinna wiedzieć o zielonej warstwie. Czy zatem dostawca usług internetowych się myli? A więc, coś w tym stylu, tak. Żadna zasada nie jest absolutna. Jest to przypadek, w którym głupcy, którzy lubią interfejs, aby pokazać wszystko, co usługa może zrobić, okazują się słuszni.

Przynajmniej mają rację, jeśli Interactornie robi nic poza potrzebami tego przypadku użycia. Jeśli Interactorrobi to dla innych przypadków użycia, nie ma powodu Input Port <I>, aby to wiedzieć. Nie jestem pewien, dlaczego Interactornie można skupić się tylko na jednym przypadku użycia, więc nie stanowi to problemu, ale coś się dzieje.

Ale input port <I>interfejs po prostu nie może podporządkować się Controllerklientowi i sprawić, że będzie to prawdziwa wtyczka. To granica „biblioteki”. Zupełnie inny sklep programistyczny mógłby pisać zieloną warstwę wiele lat po opublikowaniu czerwonej warstwy.

Jeśli przekraczasz granicę „biblioteki” i odczuwasz potrzebę zastosowania ISP, nawet jeśli nie jesteś właścicielem interfejsu po drugiej stronie, będziesz musiał znaleźć sposób na zawężenie interfejsu bez jego zmiany.

Jednym ze sposobów na wyciągnięcie tego jest adapter. Umieść go między klientami podobnymi Controlera Input Port <I>interfejsem. Adapter akceptuje Interactorjako Input Port <I>i przekazuje mu swoją pracę. Odsłania jednak tylko to, czego Controllerpotrzebują klienci za pośrednictwem interfejsu roli lub interfejsów należących do zielonej warstwy. Adapter sam nie podąża za ISP, ale umożliwia bardziej złożoną klasę, taką jak Controllerkorzystanie z ISP. Jest to przydatne, jeśli jest mniej adapterów niż klienci Controllerużywający ich, a gdy znajdujesz się w nietypowej sytuacji, gdy przekraczasz granicę biblioteki i pomimo opublikowania biblioteka nie przestaje się zmieniać. Patrzę na ciebie Firefox. Teraz te zmiany tylko psują twoje adaptery.

Co to znaczy? Oznacza to, że szczerze mówiąc, nie dostarczyłeś mi wystarczających informacji, aby powiedzieć ci, co powinieneś zrobić. Nie wiem, czy niestosowanie się do usługodawcy internetowego powoduje problem. Nie wiem, czy przestrzeganie go nie spowoduje więcej problemów.

Wiem, że szukasz prostej zasady przewodniej. ISP stara się być tym. Ale pozostawia wiele niedopowiedzeń. Wierzę w to. Tak, nie zmuszaj klientów do polegania na metodach, których nie używają, bez ważnego powodu!

Jeśli masz dobry powód, np. Projektujesz coś, co akceptuje wtyczki, pamiętaj o problemach, które nie podążają za przyczynami dostawcy usług internetowych (trudno je zmienić bez łamania klientów) i sposobach na ich złagodzenie (utrzymaj Interactorlub przynajmniej Input Port <I>skoncentruj się na jednym stabilnym przypadek użycia).

candied_orange
źródło
Dzięki za wkład. Mam moduł świadczący usługi, który ma wielu klientów. Jego przestrzeń nazw ma logicznie spójne granice, ale klient musi przeciąć te logiczne granice. W ten sposób podział przestrzeni nazw na podstawie granic logicznych nie pomaga dostawcy usług internetowych. Dlatego podzieliłem przestrzeń nazw na podstawie potrzeb klienta, jak pokazano na schemacie w pytaniu. Zależy to jednak od klientów i złego sposobu łączenia klientów z usługą, ponieważ klienci mogą być dodawani / usuwani stosunkowo często, ale zmiany w usłudze będą minimalne.
work.bin
Teraz skłaniam się ku usłudze zapewniającej gruby interfejs, jak w jej pełnej przestrzeni nazw, i to klient ma dostęp do tych usług za pośrednictwem adapterów specyficznych dla klienta. W języku C byłby to plik opakowań funkcji należących do klienta. Zmiany w usłudze wymusiłyby rekompilację adaptera, ale niekoniecznie klienta. .. <
contd
<contd> .. To z pewnością ograniczy czas kompilacji i utrzyma luźne połączenie klienta z usługą kosztem środowiska wykonawczego (wywołanie funkcji pośredniego opakowania), zwiększy przestrzeń kodu, zwiększy użycie stosu i prawdopodobnie więcej przestrzeni myśli (programista) w utrzymaniu adapterów.
work.bin
Moje obecne rozwiązanie zaspokaja teraz moje potrzeby, nowe podejście będzie wymagało więcej wysiłku i może naruszać YAGNI. Będę musiał rozważyć zalety i wady każdej metody i zdecydować, w którą stronę pójść.
work.bin
1

Więc ten punkt:

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

Zrezygnuj z tego, że naruszasz inną ważną zasadę, jaką jest YAGNI. Zależy mi na tym, gdy mam setki klientów. Myślenie o czymś z góry, a wtedy okaże się, że nie masz żadnych dodatkowych klientów dla tego kodu, jest lepsze niż cel.

druga

 partitioning depends on the nature of clients

Dlaczego twój kod nie używa DI, inwersji zależności, nic, nic w bibliotece nie powinno zależeć od natury twojego klienta.

W końcu wygląda na to, że potrzebujesz dodatkowej warstwy pod swoim kodem, aby zaspokoić potrzeby nakładania się rzeczy (DI, więc twój kod frontowy zależy tylko od tej dodatkowej warstwy, a klienci zależą tylko od interfejsu frontowego) w ten sposób pokonujesz DRY.
Naprawdę byś to zrobił. Więc robisz te same rzeczy, których używasz w warstwie modułów poniżej innego modułu. W ten sposób, mając warstwę poniżej, osiągniesz:

dla każdego klienta widoczne są tylko wymagane przez niego dane i interfejsy API; reszta przestrzeni nazw modułu jest ukryta przed klientem, tj. przestrzegaj zasady segregacji interfejsu.

tak

deklaracja nie jest powtarzana w wielu plikach nagłówkowych, tj. nie narusza DRY. moduł M nie ma żadnych zależności od swoich klientów.

tak

na klienta nie mają wpływu zmiany dokonane w częściach modułu M, które nie są przez niego używane.

tak

na istniejących klientów nie ma wpływu dodanie (lub usunięcie) większej liczby klientów.

tak

Mateusz
źródło
1

Te same informacje, które podano w deklaracji, są zawsze powtarzane w definicji. Tak właśnie działa ten język. Ponadto, powtarzając deklarację w wielu plikach nagłówkowych nie narusza DRY . Jest to dość powszechnie stosowana technika (przynajmniej w standardowej bibliotece).

Powtórzenie dokumentacji lub implementacji naruszy DRY .

Nie zawracałbym sobie tym głowy, chyba że kod klienta nie jest napisany przeze mnie.

Maciej Chałapuk
źródło
0

Wykluczam moje zamieszanie. Jednak twój praktyczny przykład rysuje rozwiązanie w mojej głowie. Jeśli potrafię wyrazić własnymi słowami: wszystkie partycje w module Mmają wiele do wielu wyłącznych relacji z dowolnym klientem.

Przykładowa struktura

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

W pliku Mc tak naprawdę nie musisz używać #ifdefs, ponieważ to, co umieścisz w pliku .c, nie wpływa na pliki klienta, o ile zdefiniowane są funkcje plików klienta.

#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();
}

Znów nie jestem pewien, czy o to pytasz. Więc weź to z odrobiną soli.

Sanchke Dellowar
źródło
Jak wygląda Mc? Czy definiujesz P1_init() i P2_init() ?
work.bin
@ work.bin Zakładam, że Mc wyglądałby jak zwykły plik .c, z wyjątkiem definiowania przestrzeni nazw między funkcjami.
Sanchke Dellowar
Zakładając, że istnieją zarówno C1, jak i C2 - z czym się łączy P1_init()i co P2_init()łączy?
work.bin
W pliku Mh / Mc Preprocesor zastąpi _PREF_to, co zostało ostatnio zdefiniowane. Tak _PREF_init()będzie z P1_init()powodu ostatniej instrukcji #define. Następnie następna instrukcja definiuje PREF równą P2_, generując w ten sposób P2_init().
Sanchke Dellowar