Comment appliquer le principe de séparation d'interface en C?

15

J'ai un module, disons «M», qui a quelques clients, disons «C1», «C2», «C3». Je veux répartir l'espace de noms du module M, c'est-à-dire les déclarations des API et des données qu'il expose, en fichier (s) d'en-tête de telle manière que -

  1. pour tout client, seules les données et les API dont il a besoin sont visibles; le reste de l'espace de noms du module est caché au client, c'est-à-dire qu'il adhère au principe de séparation d'interface .
  2. une déclaration n'est pas répétée dans plusieurs fichiers d'en-tête, c'est-à-dire qu'elle ne viole pas DRY .
  3. le module M n'a pas de dépendances sur ses clients.
  4. un client n'est pas affecté par les modifications apportées dans les parties du module M qui ne sont pas utilisées par lui.
  5. les clients existants ne sont pas affectés par l'ajout (ou la suppression) de plus de clients.

Actuellement, je traite cela en divisant l'espace de noms du module en fonction des besoins de ses clients. Par exemple, dans l'image ci-dessous, les différentes parties de l'espace de noms du module requises par ses 3 clients sont affichées. Les exigences des clients se chevauchent. L'espace de noms du module est divisé en 4 fichiers d'en-tête distincts - «a», «1», «2» et «3» .

Partitionnement de l'espace de noms des modules

Cependant, cela viole certaines des exigences susmentionnées, à savoir R3 et R5. L'exigence 3 est violée car ce partitionnement dépend de la nature des clients; également sur l'ajout d'un nouveau client, ce partitionnement change et viole l'exigence 5. Comme on peut le voir sur le côté droit de l'image ci-dessus, avec l'ajout d'un nouveau client, l'espace de noms du module est maintenant divisé en 7 fichiers d'en-tête - 'a »,« b »,« c »,« 1 »,« 2 * »,« 3 * »et« 4 » . Les fichiers d'en-tête destinés à 2 des anciens clients changent, déclenchant ainsi leur reconstruction.

Existe-t-il un moyen de réaliser la séparation d'interfaces en C de manière non artificielle?
Si oui, comment traiteriez-vous l'exemple ci-dessus?

Une solution hypothétique irréelle que j'imagine serait -
Le module a 1 gros fichier d'en-tête couvrant tout son espace de noms. Ce fichier d'en-tête est divisé en sections et sous-sections adressables comme une page Wikipedia. Chaque client dispose alors d'un fichier d'en-tête spécifique adapté à ses besoins. Les fichiers d'en-tête spécifiques au client ne sont qu'une liste d'hyperliens vers les sections / sous-sections du fichier d'en-tête gras. Et le système de construction doit reconnaître un fichier d'en-tête spécifique au client comme «modifié» si l'une des sections vers lesquelles il pointe dans l'en-tête du module est modifiée.

work.bin
la source
1
Pourquoi ce problème est-il spécifique à C? Est-ce parce que C n'a pas d'héritage?
Robert Harvey
En outre, la violation du FAI améliore-t-elle le fonctionnement de votre conception?
Robert Harvey
2
C ne prend pas vraiment en charge les concepts de POO (tels que les interfaces ou l'héritage). Nous nous contentons de hacks grossiers (mais créatifs). Vous cherchez un hack pour simuler des interfaces. En règle générale, l'ensemble du fichier d'en-tête est l'interface avec un module.
work.bin
1
structest ce que vous utilisez en C lorsque vous voulez une interface. Certes, les méthodes sont un peu difficiles. Vous pourriez trouver cela intéressant: cs.rit.edu/~ats/books/ooc.pdf
Robert Harvey
Je n'ai pas pu trouver une interface équivalente en utilisant structet function pointers.
work.bin

Réponses:

5

La séparation d'interface, en général, ne doit pas être basée sur les exigences du client. Vous devez changer toute l'approche pour y parvenir. Je dirais, modulariser l'interface en regroupant les fonctionnalités en groupes cohérents . Le regroupement est basé sur la cohérence des fonctionnalités elles-mêmes, et non sur les exigences du client. Dans ce cas, vous aurez un ensemble d'interfaces, I1, I2, ... etc. Le client C1 peut utiliser I2 seul. Le client C2 peut utiliser I1 et I5, etc. Notez que si un client utilise plusieurs Ii, ce n'est pas un problème. Si vous avez décomposé l'interface en modules cohérents, c'est là que réside le cœur du problème.

Encore une fois, le FAI n'est pas basé sur le client. Il s'agit de décomposer l'interface en modules plus petits. Si cela est fait correctement, cela garantira également que les clients sont exposés à aussi peu de fonctionnalités qu'ils en ont besoin.

Avec cette approche, vos clients peuvent augmenter à n'importe quel nombre, mais vous M n'est pas affecté. Chaque client utilisera une ou une combinaison des interfaces en fonction de ses besoins. Y aura-t-il des cas où un client, C, devra inclure disons I1 et I3, mais n'utilisera pas toutes les fonctionnalités de ces interfaces? Oui, ce n'est pas un problème. Il utilise juste le moins d'interfaces.

Nazar Merza
la source
Vous vouliez sûrement parler de groupes disjoints ou ne se chevauchant pas , je suppose?
Doc Brown
Oui, disjoint et sans chevauchement.
Nazar Merza
3

Le principe de ségrégation d'interface dit:

Aucun client ne doit être contraint de dépendre de méthodes qu'il n'utilise pas. Le FAI divise les interfaces très grandes en interfaces plus petites et plus spécifiques afin que les clients n'aient à connaître que les méthodes qui les intéressent.

Il y a quelques questions sans réponse ici. L'un est:

Comme c'est petit?

Vous dites:

Actuellement, je traite cela en divisant l'espace de noms du module en fonction des besoins de ses clients.

J'appelle cette saisie manuelle de canard . Vous créez des interfaces qui exposent uniquement les besoins d'un client. Le principe de ségrégation d'interface n'est pas simplement une saisie manuelle de canard.

Mais le FAI n'est pas simplement un appel à des interfaces de rôle "cohérentes" qui peuvent être réutilisées. Aucune conception d'interface de rôle "cohérente" ne peut parfaitement se prémunir contre l'ajout d'un nouveau client avec ses propres besoins de rôle.

Le FAI est un moyen d'isoler les clients de l'impact des modifications apportées au service. Il était destiné à accélérer la génération lorsque vous apportez des modifications. Bien sûr, cela a d'autres avantages, comme ne pas casser les clients, mais c'était le point principal. Si je modifie la count()signature de la fonction services , c'est bien si les clients qui n'utilisent pas count()n'ont pas besoin d'être modifiés et recompilés.

C'est pourquoi je me soucie du principe de ségrégation d'interface. Ce n'est pas quelque chose que je considère comme important. Cela résout un vrai problème.

Ainsi, la façon dont il doit être appliqué devrait résoudre un problème pour vous. Il n'y a pas de moyen par cœur mort pour appliquer un FAI qui ne peut pas être vaincu avec juste le bon exemple de changement nécessaire. Vous êtes censé voir comment le système évolue et faire des choix qui calmeront les choses. Explorons les options.

Demandez-vous d'abord: est-il difficile de modifier l'interface de service en ce moment? Sinon, allez dehors et jouez jusqu'à ce que vous vous calmiez. Ce n'est pas un exercice intellectuel. Veuillez vous assurer que le remède n'est pas pire que la maladie.

  1. Si de nombreux clients utilisent le même sous-ensemble de fonctions, cela plaide pour des interfaces réutilisables "cohérentes". Le sous-ensemble se concentre probablement autour d'une idée que nous pouvons considérer comme le rôle que le service fournit au client. C'est sympa quand ça marche. Cela ne fonctionne pas toujours.

  2.  

    1. Si de nombreux clients utilisent différents sous-ensembles de fonctions, il est possible que le client utilise réellement le service via plusieurs rôles. C'est OK mais cela rend les rôles difficiles à voir. Trouvez-les et essayez de les taquiner. Cela peut nous remettre dans le cas 1. Le client utilise simplement le service via plusieurs interfaces. Veuillez ne pas commencer à diffuser le service. Si quoi que ce soit, cela signifierait passer le service au client plus d'une fois. Cela fonctionne mais cela me fait me demander si le service n'est pas une grosse boule de boue qui doit être brisée.

    2. Si de nombreux clients utilisent des sous-ensembles différents mais que vous ne voyez même pas de rôles autorisant les clients à en utiliser plusieurs, vous n'avez rien de mieux que de taper du canard pour concevoir vos interfaces. Cette façon de concevoir les interfaces garantit que le client n'est pas exposé à une seule fonction qu'il n'utilise pas, mais il garantit presque que l'ajout d'un nouveau client impliquera toujours l'ajout d'une nouvelle interface qui, même si la mise en œuvre du service n'a pas besoin de savoir à ce sujet l'interface qui agrège les interfaces de rôle sera. Nous avons simplement échangé une douleur contre une autre.

  3. Si de nombreux clients utilisent des sous-ensembles différents, se chevauchent, de nouveaux clients devraient s'ajouter qui auront besoin de sous-ensembles imprévisibles, et vous ne souhaitez pas interrompre le service, alors envisagez une solution plus fonctionnelle. Étant donné que les deux premières options n'ont pas fonctionné et que vous êtes vraiment dans un mauvais endroit où rien ne suit un modèle et que d'autres changements arrivent, envisagez de fournir à chaque fonction sa propre interface. Terminer ici ne signifie pas que le FAI a échoué. Si quelque chose échouait, c'était le paradigme orienté objet. Les interfaces à méthode unique suivent l'extrême FAI. C'est un peu de frappe au clavier, mais vous pouvez trouver que cela rend soudainement les interfaces réutilisables. Encore une fois, assurez-vous qu'il n'y a pas

Il s'avère donc qu'ils peuvent devenir très petits.

J'ai pris cette question comme un défi pour appliquer le FAI dans les cas les plus extrêmes. Mais gardez à l'esprit qu'il vaut mieux éviter les extrêmes. Dans une conception bien pensée qui applique d'autres principes SOLIDES, ces problèmes ne se produisent généralement pas ou importent presque autant.


Une autre question sans réponse est:

À qui appartiennent ces interfaces?

Je vois encore et encore des interfaces conçues avec ce que j'appelle une mentalité de «bibliothèque». Nous avons tous été coupables du codage singe-voir-singe-faire où vous faites juste quelque chose parce que c'est ainsi que vous l'avez vu. Nous sommes coupables de la même chose avec les interfaces.

Quand je regarde une interface conçue pour une classe dans une bibliothèque, je pensais: oh, ces gars sont des pros. Ce doit être la bonne façon de faire une interface. Ce que je n'arrivais pas à comprendre, c'est qu'une limite de bibliothèque a ses propres besoins et problèmes. D'une part, une bibliothèque ignore complètement la conception de ses clients. Toutes les frontières ne sont pas identiques. Et parfois, même la même frontière a différentes façons de la traverser.

Voici deux façons simples d'examiner la conception d'interface:

  • Interface appartenant au service. Certaines personnes conçoivent chaque interface pour exposer tout ce qu'un service peut faire. Vous pouvez même trouver des options de refactoring dans les IDE qui écriront une interface pour vous en utilisant la classe que vous l'alimenterez.

  • Interface appartenant au client. Le FAI semble affirmer que c'est juste et que le service appartenant est faux. Vous devez briser chaque interface en fonction des besoins des clients. Puisque le client possède l'interface, il doit la définir.

Alors qui a raison?

Considérez les plugins:

entrez la description de l'image ici

À qui appartiennent les interfaces ici? Les clients? Les services?

S'avère à la fois.

Les couleurs ici sont des couches. Le calque rouge (à droite) n'est pas censé rien savoir du calque vert (à gauche). La couche verte peut être modifiée ou remplacée sans toucher la couche rouge. De cette façon, n'importe quelle couche verte peut être connectée à la couche rouge.

J'aime savoir ce qui est censé savoir quoi et ce qui n'est pas censé savoir. Pour moi, "qu'est-ce qui sait quoi?", Est la question architecturale la plus importante.

Rendons le vocabulaire clair:

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

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

Un client est quelque chose qui utilise.

Un service est quelque chose qui est utilisé.

Interactor se trouve être les deux.

Le FAI dit de briser les interfaces pour les clients. Très bien, appliquons cela ici:

  • Presenter(un service) ne doit pas dicter à l' Output Port <I>interface. L'interface doit être limitée à ce dont Interactor(ici, en tant que client) a besoin. Cela signifie que l'interface CONNAIT le Interactoret, pour suivre le FAI, doit changer avec lui. Et c'est très bien.

  • Interactor(ici en tant que service) ne doit pas dicter à l' Input Port <I>interface. L'interface doit être limitée aux Controllerbesoins (d'un client). Cela signifie que l'interface CONNAIT le Controlleret, pour suivre le FAI, doit changer avec lui. Et ce n'est pas bien.

Le second n'est pas bien car la couche rouge n'est pas censée connaître la couche verte. Le FAI a-t-il donc tort? Enfin un peu. Aucun principe n'est absolu. Il s'agit d'un cas où les gaffes qui aiment que l'interface montre tout ce que le service peut faire se révèlent corrects.

Au moins, ils ont raison si le Interactorne fait rien d'autre que ce dont le cas d'utilisation a besoin. Si le Interactorfait des choses pour d'autres cas d'utilisation, il n'y a aucune raison que cela les Input Port <I>connaisse. Je ne sais pas pourquoi Interactorne peut pas se concentrer uniquement sur un cas d'utilisation, donc ce n'est pas un problème, mais des choses se produisent.

Mais l' input port <I>interface ne peut tout simplement pas s'asservir au Controllerclient et avoir un véritable plugin. Il s'agit d'une limite de «bibliothèque». Un magasin de programmation complètement différent pourrait écrire la couche verte des années après la publication de la couche rouge.

Si vous traversez une frontière de `` bibliothèque '' et que vous ressentez le besoin d'appliquer ISP même si vous ne possédez pas l'interface de l'autre côté, vous devrez trouver un moyen de restreindre l'interface sans la changer.

Un moyen de retirer cela est un adaptateur. Mettez-le entre les clients comme Controleret l' Input Port <I>interface. L'adaptateur accepte en Interactortant que Input Port <I>et délègue son travail à lui. Cependant, il expose uniquement les Controllerbesoins des clients via une ou plusieurs interfaces de rôle appartenant à la couche verte. L'adaptateur ne suit pas le FAI lui-même mais permet à une classe plus complexe Controllerde profiter du FAI. Cela est utile s'il y a moins d'adaptateurs que les clients Controllerqui les utilisent et lorsque vous vous trouvez dans une situation inhabituelle où vous traversez les limites d'une bibliothèque et, malgré sa publication, la bibliothèque ne cessera de changer. En vous regardant Firefox. Maintenant, ces changements ne font que casser vos adaptateurs.

Qu'est-ce que cela signifie? Cela signifie honnêtement que vous n'avez pas fourni suffisamment d'informations pour que je puisse vous dire ce que vous devez faire. Je ne sais pas si le fait de ne pas suivre le FAI vous pose un problème. Je ne sais pas si le suivre ne finirait pas par vous causer plus de problèmes.

Je sais que vous cherchez un principe directeur simple. Le FAI essaie d'être ça. Mais cela laisse beaucoup de choses à dire. J'y crois. Oui, veuillez ne pas forcer les clients à dépendre de méthodes qu'ils n'utilisent pas, sans bonne raison!

Si vous avez une bonne raison, telle que la conception de quelque chose pour accepter les plugins, alors soyez conscient des problèmes qui ne suivent pas les causes des FAI (il est difficile de changer sans casser les clients) et des moyens de les atténuer (garder Interactorou au moins se Input Port <I>concentrer sur une seule écurie) cas d'utilisation).

candied_orange
la source
Merci pour la contribution. J'ai un module de prestation de services qui a plusieurs clients. Son espace de noms a des limites logiquement cohérentes, mais les besoins du client dépassent ces limites logiques. De ce fait, diviser l'espace de noms sur la base de limites logiques n'aide pas le FAI. J'ai donc divisé l'espace de noms en fonction des besoins des clients, comme indiqué dans le diagramme de la question. Mais cela le rend dépendant des clients et une mauvaise façon de coupler les clients au service, car les clients pourraient être ajoutés / supprimés relativement fréquemment, mais les changements dans le service seront minimes.
work.bin
Je penche maintenant vers le service fournissant une interface grasse, comme dans son espace de noms complet et il appartient au client d'accéder à ces services via des adaptateurs spécifiques au client. En termes de C, il s'agirait d'un fichier d'encapsuleurs de fonctions appartenant au client. Les modifications du service forceraient la recompilation de l'adaptateur mais pas nécessairement du client. .. <
contd
<contd> .. Cela gardera certainement les temps de construction minimaux et gardera le couplage entre le client et le service «lâche» au prix de l'exécution (appel d'une fonction wrapper intermédiaire), augmentera l'espace de code, augmentera l'utilisation de la pile et probablement plus d'espace mental (programmeur) dans la maintenance des adaptateurs.
work.bin
Ma solution actuelle répond maintenant à mes besoins, la nouvelle approche demandera plus d'efforts et pourrait bien violer YAGNI. Je devrai peser le pour et le contre de chaque méthode et décider de la voie à suivre ici.
work.bin
1

Donc, ce point:

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

Abandonne que vous violez un autre principe important qui est YAGNI. Je m'en soucierais quand j'aurais des centaines de clients. En pensant à quelque chose à l'avance, il se révélera que vous n'avez pas de clients supplémentaires pour ce code bat le but.

Seconde

 partitioning depends on the nature of clients

Pourquoi votre code n'utilise pas DI, l'inversion de dépendance, rien, rien dans votre bibliothèque ne devrait dépendre de la nature de votre client.

Finalement, il semble que vous ayez besoin d'une couche supplémentaire sous votre code pour répondre aux besoins de chevauchement (DI, donc votre code frontal ne dépend que de cette couche supplémentaire et vos clients ne dépendent que de votre interface frontale) de cette façon, vous battez le DRY.
Cela, vous l'auriez vraiment. Vous faites donc les mêmes choses que vous utilisez dans votre couche de module sous un autre module. De cette façon, avoir une couche en dessous de vous permet:

pour tout client, seules les données et les API dont il a besoin sont visibles; le reste de l'espace de noms du module est caché au client, c'est-à-dire qu'il adhère au principe de séparation d'interface.

Oui

une déclaration n'est pas répétée dans plusieurs fichiers d'en-tête, c'est-à-dire qu'elle ne viole pas DRY. le module M n'a pas de dépendances sur ses clients.

Oui

un client n'est pas affecté par les modifications apportées dans les parties du module M qui ne sont pas utilisées par lui.

Oui

les clients existants ne sont pas affectés par l'ajout (ou la suppression) de plus de clients.

Oui

Mateusz
la source
1

Les mêmes informations que celles fournies dans la déclaration sont toujours répétées dans la définition. C'est juste la façon dont cette langue fonctionne. En outre, la répétition d'une déclaration dans plusieurs fichiers d'en-tête ne viole pas DRY . C'est une technique assez courante (au moins dans la bibliothèque standard).

La répétition de la documentation ou de la mise en œuvre violerait DRY .

Je ne me dérangerais pas avec cela à moins que le code client ne soit pas écrit par moi.

Maciej Chałapuk
la source
0

Je rejette ma confusion. Cependant, votre exemple pratique tire une solution dans ma tête. Si je peux dire avec mes propres mots: toutes les partitions du module Mont plusieurs à plusieurs relation exclusive avec tous les clients.

Exemple de structure

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

Dans le fichier Mc, vous n'auriez pas réellement besoin d'utiliser les #ifdefs car ce que vous mettez dans le fichier .c n'affecte pas les fichiers clients tant que les fonctions utilisées par les fichiers clients sont définies.

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

Encore une fois, je ne sais pas si c'est ce que vous demandez. Alors prenez-le avec un grain de sel.

Sanchke Dellowar
la source
À quoi ressemble Mc? Définissez-vous P1_init() et P2_init() ?
work.bin
@ work.bin Je suppose que Mc ressemblerait à un simple fichier .c à l'exception de la définition de l'espace de noms entre les fonctions.
Sanchke Dellowar
En supposant que les deux existent C1 et C2 - qu'est-ce P1_init()et P2_init()lien?
work.bin
Dans le fichier Mh / Mc, le préprocesseur sera remplacé _PREF_par ce à quoi il a été défini en dernier. Il en _PREF_init()sera de même à P1_init()cause de la dernière instruction #define. Ensuite, la prochaine instruction define définira PREF égal à P2_, générant ainsi P2_init().
Sanchke Dellowar