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 -
- 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 .
- 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.
- um cliente não é afetado pelas alterações feitas em partes do módulo M que não são usadas por ele.
- 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' .
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.
fonte
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.pdfstruct
efunction pointers
.Respostas:
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.
fonte
O Princípio de Segregação de Interface diz:
Existem algumas perguntas não respondidas aqui. Um é:
Quão pequeno?
Você diz:
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 usamcount()
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.
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.
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.
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.
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:
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:
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 aOutput Port <I>
interface. A interface deve ser reduzida ao queInteractor
(aqui atuando como cliente) precisa. Isso significa que a interface SABE sobre oeInteractor
, para seguir o ISP, deve mudar com ele. E isso é bom.Interactor
(aqui atuando como um serviço) não deve ditar aInput Port <I>
interface. A interface deve ser restrita ao queController
(um cliente) precisa. Isso significa que a interface SABE sobre oeController
, 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
Interactor
não fizerem nada além do que esse caso de uso precisa. Se oInteractor
fizer para outros casos de uso, não há motivoInput Port <I>
para saber sobre eles. Não sei porInteractor
que 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 noController
cliente 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
Controler
e aInput Port <I>
interface. O adaptador aceitaInteractor
comoInput Port <I>
e delega seu trabalho. No entanto, ele expõe apenas o que os clientesController
precisam 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 complexasController
gostem do ISP. Isso é útil se houver menos adaptadores do que clientes comoController
esses 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
Interactor
ou pelo menos seInput Port <I>
concentre em um estável caso de uso).fonte
Então, este ponto:
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
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:
sim
sim
sim
sim
fonte
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.
fonte
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
M
têm um relacionamento exclusivo muitos para muitos com todos e quaisquer clientes.Estrutura da amostra
Mh
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.
C1.c
C2.c
C3.c
Mais uma vez, não tenho certeza se é isso que você está perguntando. Então, tome-o com um grão de sal.
fonte
P1_init()
eP2_init()
?P1_init()
e seP2_init()
vincula?_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 assimP2_init()
.