Como aplicar o princípio de Segregação de interface em C?

15

Eu tenho um módulo, digamos 'M', que tem alguns clientes, digamos 'C1', 'C2', 'C3'. Quero distribuir o espaço de nome do módulo M, ou seja, as declarações das APIs e os dados que ele expõe, nos arquivos de cabeçalho, de forma que -

  1. para qualquer cliente, apenas os dados e APIs necessários são visíveis; O restante do espaço para nome do módulo está oculto no cliente, ou seja, adere ao princípio de Segregação da interface .
  2. uma declaração não é repetida em vários arquivos de cabeçalho, ou seja, não viola DRY .
  3. O módulo M não possui dependências em seus clientes.
  4. um cliente não é afetado pelas alterações feitas em partes do módulo M que não são usadas por ele.
  5. os clientes existentes não são afetados pela adição (ou exclusão) de mais clientes.

Atualmente, eu lido com isso dividindo o espaço de nome do módulo, dependendo dos requisitos de seus clientes. Por exemplo, na imagem abaixo, são mostradas as diferentes partes do namespace do módulo exigidas por seus três clientes. Os requisitos do cliente se sobrepõem. O espaço para nome do módulo é dividido em 4 arquivos de cabeçalho separados - 'a', '1', '2' e '3' .

Particionamento de espaço de nome do módulo

No entanto, isso viola alguns dos requisitos acima mencionados, ou seja, R3 e R5. O requisito 3 é violado porque esse particionamento depende da natureza dos clientes; também na adição de um novo cliente, esse particionamento muda e viola o requisito 5. Como pode ser visto no lado direito da imagem acima, com a adição de um novo cliente, o espaço para nome do módulo agora é dividido em 7 arquivos de cabeçalho - 'a ',' b ',' c ',' 1 ',' 2 * ',' 3 * 'e' 4 ' . Os arquivos de cabeçalho destinados a 2 dos clientes mais antigos são alterados, acionando sua reconstrução.

Existe uma maneira de alcançar a segregação de interface em C de maneira não artificial?
Se sim, como você lidaria com o exemplo acima?

Uma solução hipotética irreal que eu imagino seria -
O módulo possui um arquivo de cabeçalho gordo cobrindo todo o espaço para nome. Esse arquivo de cabeçalho é dividido em seções e subseções endereçáveis, como uma página da Wikipedia. Cada cliente possui um arquivo de cabeçalho específico, adaptado para ele. Os arquivos de cabeçalho específicos do cliente são apenas uma lista de hiperlinks para as seções / subseções do arquivo de cabeçalho gordo. E o sistema de construção deve reconhecer um arquivo de cabeçalho específico do cliente como 'modificado' se qualquer uma das seções apontadas no cabeçalho do módulo for modificada.

work.bin
fonte
1
Por que esse problema é específico para C? É porque C não tem herança?
Robert Harvey
Além disso, a violação do ISP torna seu design melhor?
Robert Harvey
2
C realmente não suporta intrinsecamente conceitos de POO (como interfaces ou herança). Nós nos contentamos com hacks brutos (mas criativos). Procurando por um hack para simular interfaces. Normalmente, o arquivo de cabeçalho inteiro é a interface para um módulo.
work.bin
1
structé o que você usa em C quando deseja uma interface. É verdade que os métodos são um pouco difíceis. Você pode achar isso interessante: cs.rit.edu/~ats/books/ooc.pdf
Robert Harvey
Não consegui criar uma interface equivalente usando structe function pointers.
work.bin

Respostas:

5

A segregação de interface, em geral, não deve se basear nos requisitos do cliente. Você deve alterar toda a abordagem para alcançá-la. Eu diria, modularize a interface agrupando os recursos em grupos coerentes . Esse agrupamento é baseado na coerência dos próprios recursos, não nos requisitos do cliente. Nesse caso, você terá um conjunto de interfaces, I1, I2, ... etc. O cliente C1 pode usar I2 sozinho. O cliente C2 pode usar I1 e I5 etc. Observe que, se um cliente usa mais de um Ii, não há problema. Se você decompôs a interface em módulos coerentes, é aí que está o cerne da questão.

Novamente, o ISP não é baseado no cliente. Trata-se de decompor a interface em módulos menores. Se isso for feito corretamente, também garantirá que os clientes sejam expostos a poucos recursos necessários.

Com essa abordagem, seus clientes podem aumentar para qualquer número, mas você M não é afetado. Cada cliente usará uma ou alguma combinação das interfaces com base em suas necessidades. Haverá casos em que um cliente, C, precisará incluir, digamos, I1 e I3, mas não usar todos os recursos dessas interfaces? Sim, isso não é um problema. Ele apenas usa o menor número de interfaces.

Nazar Merza
fonte
Você certamente quis dizer grupos disjuntos ou sem sobreposição , presumo?
Doc Brown
Sim, disjunta e sem sobreposição.
Nazar Merza
3

O Princípio de Segregação de Interface diz:

Nenhum cliente deve ser forçado a depender dos métodos que não usa. O ISP divide as interfaces muito grandes em menores e mais específicas, para que os clientes precisem apenas conhecer os métodos que lhes interessam.

Existem algumas perguntas não respondidas aqui. Um é:

Quão pequeno?

Você diz:

Atualmente, eu lido com isso dividindo o espaço de nome do módulo, dependendo dos requisitos de seus clientes.

Eu chamo isso de digitação manual de pato . Você cria interfaces que expõem apenas o que um cliente precisa. O princípio de segregação de interface não é simplesmente digitação manual do pato.

Mas o ISP também não é simplesmente um pedido de interfaces de função "coerentes" que podem ser reutilizadas. Nenhum design de interface de função "coerente" pode se proteger perfeitamente contra a adição de um novo cliente com suas próprias necessidades de função.

O ISP é uma maneira de isolar os clientes do impacto das alterações no serviço. O objetivo era tornar a construção mais rápida à medida que você faz alterações. Claro que tem outros benefícios, como não quebrar clientes, mas esse era o ponto principal. Se eu estiver alterando a count()assinatura da função de serviços , é bom que os clientes que não usam count()não precisem ser editados e recompilados.

É por isso que me importo com o Princípio de Segregação de Interface. Não é algo que considero importante como fé. Resolve um problema real.

Portanto, a maneira como deve ser aplicada deve resolver um problema para você. Não existe uma maneira mecânica de aplicar ISP que não pode ser derrotada com o exemplo certo de uma mudança necessária. Você deve observar como o sistema está mudando e fazer escolhas que permitirão que as coisas se acalmem. Vamos explorar as opções.

Primeiro, pergunte a si mesmo: está dificultando as alterações na interface de serviço agora? Caso contrário, saia e brinque até se acalmar. Este não é um exercício intelectual. Por favor, verifique se a cura não é pior que a doença.

  1. Se muitos clientes usam o mesmo subconjunto de funções, isso significa interfaces reutilizáveis ​​"coerentes". O subconjunto provavelmente se concentra em torno de uma idéia que podemos considerar como a função que o serviço está fornecendo ao cliente. É bom quando isso funciona. Isso nem sempre funciona.

  2.  

    1. Se muitos clientes usam diferentes subconjuntos de funções, é possível que o cliente esteja realmente usando o serviço através de várias funções. Tudo bem, mas dificulta a visualização dos papéis. Encontre-os e tente separá-los. Isso pode nos colocar de volta no caso 1. O cliente simplesmente usa o serviço através de mais de uma interface. Por favor, não comece a transmitir o serviço. Se alguma coisa significaria passar o serviço para o cliente mais de uma vez. Isso funciona, mas me faz questionar se o serviço não é uma grande bola de lama que precisa ser quebrada.

    2. Se muitos clientes usam subconjuntos diferentes, mas você não vê funções, mesmo permitindo que os clientes usem mais de um, então você não tem nada melhor do que digitar duck para projetar suas interfaces. Essa maneira de projetar as interfaces garante que o cliente não seja exposto a uma única função que não esteja usando, mas quase garante que a adição de um novo cliente sempre envolverá a adição de uma nova interface que, embora a implementação do serviço não precise saber sobre isso a interface que agrega as interfaces de função. Simplesmente trocamos uma dor por outra.

  3. Se muitos clientes usam subconjuntos diferentes, se sobrepõem, espera-se que novos clientes sejam adicionados, que precisarão de subconjuntos imprevisíveis e você não estiver disposto a interromper o serviço, considere uma solução mais funcional. Como as duas primeiras opções não funcionaram e você está realmente em um lugar ruim, onde nada está seguindo um padrão e mais mudanças estão chegando, considere fornecer a cada função sua própria interface. Terminar aqui não significa que o ISP falhou. Se alguma coisa falhou, foi o paradigma orientado a objetos. As interfaces de método único seguem o ISP ao extremo. É bastante digitado no teclado, mas você pode achar que isso repentinamente torna as interfaces reutilizáveis. Mais uma vez, verifique se não há

Acontece que eles podem ficar muito pequenos.

Fiz essa pergunta como um desafio para aplicar o ISP nos casos mais extremos. Mas tenha em mente que é melhor evitar extremos. Em um design bem pensado que aplica outros princípios do SOLID, esses problemas geralmente não ocorrem ou importam, quase o mesmo.


Outra pergunta sem resposta é:

Quem possui essas interfaces?

Repetidas vezes, vejo interfaces projetadas com o que chamo de mentalidade de "biblioteca". Todos nós somos culpados pela codificação macaco-ver-macaco-do, onde você está apenas fazendo algo porque é assim que você vê isso. Somos culpados da mesma coisa com interfaces.

Quando olho para uma interface projetada para uma aula em uma biblioteca, eu pensava: ah, esses caras são profissionais. Esse deve ser o caminho certo para fazer uma interface. O que eu estava deixando de entender é que o limite de uma biblioteca tem suas próprias necessidades e problemas. Por um lado, uma biblioteca é completamente ignorante do design de seus clientes. Nem todo limite é o mesmo. E às vezes até o mesmo limite tem maneiras diferentes de atravessá-lo.

Aqui estão duas maneiras simples de analisar o design da interface:

  • Interface de propriedade do serviço. Algumas pessoas projetam todas as interfaces para expor tudo o que um serviço pode fazer. Você pode até encontrar opções de refatoração nos IDE que escreverão uma interface para você usando qualquer classe que for alimentada.

  • Interface de propriedade do cliente. O ISP parece argumentar que isso está certo e o serviço de propriedade está errado. Você deve dividir todas as interfaces com as necessidades dos clientes. Como o cliente possui a interface, ele deve defini-la.

Então quem está certo?

Considere plugins:

insira a descrição da imagem aqui

Quem possui as interfaces aqui? Os clientes? Os serviços?

Acontece que ambos.

As cores aqui são camadas. A camada vermelha (direita) não deve saber nada sobre a camada verde (esquerda). A camada verde pode ser alterada ou substituída sem tocar na camada vermelha. Dessa forma, qualquer camada verde pode ser conectada à camada vermelha.

Eu gosto de saber o que deveria saber sobre o que e o que não deveria saber. Para mim, "o que sabe sobre o quê?", É a questão arquitetônica mais importante.

Vamos esclarecer um pouco o vocabulário:

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

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

Um cliente é algo que usa.

Um serviço é algo que é usado.

Interactor passa a ser ambos.

O ISP diz que interrompe interfaces para clientes. Tudo bem, vamos aplicar isso aqui:

  • Presenter(um serviço) não deve ditar a Output Port <I>interface. A interface deve ser reduzida ao que Interactor(aqui atuando como cliente) precisa. Isso significa que a interface SABE sobre oe Interactor, para seguir o ISP, deve mudar com ele. E isso é bom.

  • Interactor(aqui atuando como um serviço) não deve ditar a Input Port <I>interface. A interface deve ser restrita ao que Controller(um cliente) precisa. Isso significa que a interface SABE sobre oe Controller, para seguir o ISP, deve mudar com ele. E isso não está bem.

O segundo não é bom porque a camada vermelha não deve saber sobre a camada verde. Então, o ISP está errado? Bem, mais ou menos. Nenhum princípio é absoluto. Este é um caso em que as bobagens que gostam da interface para mostrar tudo o que o serviço pode fazer estão corretas.

Pelo menos, eles estão certos se Interactornão fizerem nada além do que esse caso de uso precisa. Se o Interactorfizer para outros casos de uso, não há motivo Input Port <I>para saber sobre eles. Não sei por Interactorque não posso focar apenas em um Caso de Uso; portanto, esse não é um problema, mas acontece tudo.

Mas a input port <I>interface simplesmente não pode se escravizar no Controllercliente e fazer com que esse seja um verdadeiro plug-in. Este é um limite de 'biblioteca'. Uma loja de programação completamente diferente poderia escrever a camada verde anos após a publicação da camada vermelha.

Se você estiver cruzando um limite de 'biblioteca' e sentir a necessidade de aplicar o ISP, mesmo que não seja o proprietário da interface do outro lado, precisará encontrar uma maneira de restringir a interface sem alterá-la.

Uma maneira de conseguir isso é um adaptador. Coloque entre clientes como Controlere a Input Port <I>interface. O adaptador aceita Interactorcomo Input Port <I>e delega seu trabalho. No entanto, ele expõe apenas o que os clientes Controllerprecisam através de uma interface de função ou interfaces pertencentes à camada verde. O adaptador não segue o ISP, mas permite que classes mais complexas Controllergostem do ISP. Isso é útil se houver menos adaptadores do que clientes como Controlleresses e quando você estiver na situação incomum em que está ultrapassando um limite de biblioteca e, apesar de publicada, a biblioteca não para de mudar. Olhando para você Firefox. Agora essas alterações quebram apenas seus adaptadores.

Então o que isso quer dizer? Sinceramente, você não forneceu informações suficientes para eu lhe dizer o que deve fazer. Não sei se não seguir o ISP está causando um problema. Não sei se segui-lo não acabaria causando mais problemas.

Sei que você está procurando um princípio orientador simples. ISP tenta ser isso. Mas deixa muito por dizer. Eu acredito nisso. Sim, por favor, não force os clientes a depender de métodos que eles não usam, sem uma boa razão!

Se você tiver um bom motivo, como projetar algo para aceitar plug-ins, esteja ciente dos problemas que não seguem as causas do ISP (é difícil mudar sem quebrar os clientes) e as maneiras de mitigá-los (mantenha Interactorou pelo menos se Input Port <I>concentre em um estável caso de uso).

candied_orange
fonte
Obrigado pela contribuição. Eu tenho um módulo de prestação de serviços que possui vários clientes. Seu espaço para nome possui limites logicamente coerentes, mas as necessidades do cliente ultrapassam esses limites lógicos. Assim, dividir o espaço para nome com base em limites lógicos não ajuda no ISP. Portanto, dividi o namespace com base nas necessidades do cliente, conforme mostrado no diagrama da pergunta. Mas isso o torna dependente dos clientes e uma maneira ruim de acoplar clientes ao serviço, já que os clientes podem ser adicionados / removidos com relativa frequência, mas as alterações no serviço serão mínimas.
work.bin
Agora, estou inclinado para o serviço que fornece uma interface complexa, como em seu espaço de nome completo, e cabe ao cliente acessar esses serviços por meio de adaptadores específicos do cliente. Em termos de C, seria um arquivo de wrappers de função pertencentes ao cliente. Alterações no serviço forçariam a recompilação do adaptador, mas não necessariamente do cliente. .. <
contd
<contd> .. Isso definitivamente manterá o tempo de compilação mínimo e manterá o acoplamento entre o cliente e o serviço 'frouxo' ao custo do tempo de execução (chamando uma função intermediária de wrapper), aumenta o espaço do código, aumenta o uso da pilha e provavelmente mais espaço na mente (programador) na manutenção dos adaptadores.
work.bin
Minha solução atual atende às minhas necessidades agora, a nova abordagem exigirá mais esforço e poderá estar violando a YAGNI. Vou ter que pesar os prós e contras de cada método e decidir qual caminho seguir aqui.
work.bin
1

Então, este ponto:

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

Desiste de que você está violando outro princípio importante que é YAGNI. Eu me importaria com isso quando tenho centenas de clientes. Pensando em algo antecipadamente e, em seguida, verificamos que você não tem mais clientes para esse código é melhor do que isso.

Segundo

 partitioning depends on the nature of clients

Por que o seu código não está usando DI, inversão de dependência, nada, nada na sua biblioteca deve depender da natureza do seu cliente.

Eventualmente, parece que você precisa de uma camada adicional no seu código para atender às necessidades de coisas sobrepostas (DI, portanto, o código da frente depende apenas dessa camada adicional e os clientes dependem apenas da interface da frente) dessa maneira, você vence o DRY.
Isso você odiaria de verdade. Então você faz as mesmas coisas que usa na camada do módulo abaixo de outro módulo. Dessa forma, tendo a camada abaixo, você consegue:

para qualquer cliente, apenas os dados e APIs necessários são visíveis; O restante do espaço para nome do módulo está oculto no cliente, ou seja, adere ao princípio de Segregação da interface.

sim

uma declaração não é repetida em vários arquivos de cabeçalho, ou seja, não viola DRY. O módulo M não possui dependências em seus clientes.

sim

um cliente não é afetado pelas alterações feitas em partes do módulo M que não são usadas por ele.

sim

os clientes existentes não são afetados pela adição (ou exclusão) de mais clientes.

sim

Mateusz
fonte
1

As mesmas informações fornecidas na declaração são sempre repetidas na definição. É assim que essa linguagem funciona. Além disso, repetir uma declaração em vários arquivos de cabeçalho não viola o DRY . É uma técnica bastante usada (pelo menos na biblioteca padrão).

Repetir a documentação ou a implementação violaria o DRY .

Eu não me incomodaria com isso, a menos que o código do cliente não seja escrito por mim.

Maciej Chałapuk
fonte
0

Eu nego minha confusão. No entanto, seu exemplo prático desenha uma solução na minha cabeça. Se eu puder colocar minhas próprias palavras: todas as partições no módulo Mtêm um relacionamento exclusivo muitos para muitos com todos e quaisquer clientes.

Estrutura da amostra

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

No arquivo Mc, você não precisaria realmente usar o #ifdefs porque o que você coloca no arquivo .c não afeta os arquivos do cliente, desde que as funções usadas pelos arquivos do cliente estejam definidas.

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

Mais uma vez, não tenho certeza se é isso que você está perguntando. Então, tome-o com um grão de sal.

Sanchke Dellowar
fonte
Como é o Mc? Você define P1_init() e P2_init() ?
precisa saber é o seguinte
@ work.bin Presumo que Mc se pareça com um arquivo .c simples, com a exceção de definir o namespace entre as funções.
Sanchke Dellowar
Supondo que C1 e C2 existam - o que faz P1_init()e se P2_init()vincula?
precisa saber é o seguinte
No arquivo Mh / Mc, o pré-processador será substituído _PREF_pelo que foi definido pela última vez. Assim _PREF_init()será P1_init()por causa da última declaração #define. Em seguida, a próxima instrução define definir PREF igual a P2_, gerando assim P2_init().
Sanchke Dellowar