Entendo o motivo por trás do princípio do mínimo conhecimento , mas encontro algumas desvantagens se tentar aplicá-lo em meu design.
Um dos exemplos desse princípio (na verdade, como não usá-lo), que encontrei no livro Head First Design Patterns, especifica que é errado chamar um método em objetos que foram retornados ao chamar outros métodos, em termos desse princípio. .
Mas parece que às vezes é muito necessário usar essa capacidade.
Por exemplo: Eu tenho várias classes: classe de captura de vídeo, classe de codificador, classe de streamer, e todas elas usam alguma outra classe básica, VideoFrame, e como elas interagem umas com as outras, elas podem fazer, por exemplo, algo como isto:
streamer
código da classe
...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....
Como você pode ver, esse princípio não é aplicado aqui. Esse princípio pode ser aplicado aqui ou é que esse princípio nem sempre pode ser aplicado em um design como este?
fonte
Respostas:
O princípio do qual você está falando (mais conhecido como Lei de Demeter ) para funções pode ser aplicado adicionando outro método auxiliar à sua classe de streamer, como
Agora, cada função apenas "conversa com amigos", não com "amigos de amigos".
IMHO é uma diretriz grosseira que pode ajudar a criar métodos que seguem mais rigorosamente o princípio da responsabilidade única. Em um caso simples como o descrito acima, provavelmente é muito opinativo se isso realmente vale a pena e se o código resultante é realmente "mais limpo" ou se apenas expandirá seu código formalmente sem nenhum ganho notável.
fonte
O Princípio do mínimo de conhecimento ou a Lei de Deméter é um aviso contra emaranhar sua classe com detalhes de outras classes que atravessam camada após camada. Diz a você que é melhor conversar apenas com seus "amigos" e não com "amigos de amigos".
Imagine que você foi convidado a soldar um escudo na estátua de um cavaleiro com uma armadura brilhante de placas. Você posiciona cuidadosamente o escudo no braço esquerdo para que pareça natural. Você percebe que há três pequenos lugares no antebraço, no cotovelo e no braço, onde o escudo toca a armadura. Você solda todos os três locais porque deseja ter certeza de que a conexão é forte. Agora imagine que seu chefe está louco, porque ele não pode mover o cotovelo de sua armadura. Você supôs que a armadura nunca iria se mover e criou uma conexão imóvel entre o antebraço e o braço. O escudo só deve se conectar ao seu amigo, antebraço. Não para antebraços amigos. Mesmo se você precisar adicionar um pedaço de metal para fazê-los tocar.
As metáforas são boas, mas o que realmente queremos dizer com amigo? Qualquer coisa que um objeto sabe como criar ou encontrar é um amigo. Como alternativa, um objeto pode apenas pedir para receber outros objetos , dos quais conhece apenas a interface. Eles não contam como amigos, porque nenhuma expectativa de como obtê-los é imposta. Se o objeto não sabe de onde eles vieram porque algo o passou / injetou, então não é um amigo de um amigo, nem mesmo um amigo. É algo que o objeto só sabe usar. É uma coisa boa.
Ao tentar aplicar princípios como esse, é importante entender que eles nunca o proíbem de realizar algo. Eles são um aviso de que você pode estar deixando de fazer mais trabalho para obter um design melhor que realize a mesma coisa.
Ninguém quer fazer o trabalho sem motivo, por isso é importante entender o que você está recebendo com isso. Nesse caso, ele mantém seu código flexível. Você pode fazer alterações e ter menos outras classes afetadas pelas alterações com que se preocupar. Parece bom, mas não ajuda a decidir o que fazer, a menos que você a tome como algum tipo de doutrina religiosa.
Em vez de seguir cegamente esse princípio, adote uma versão simples desse problema. Escreva uma solução que não siga o princípio e uma que siga. Agora que você tem duas soluções, pode comparar como cada uma delas é receptiva às mudanças, tentando transformá-las em ambas.
Se você NÃO PODE resolver um problema enquanto segue esse princípio, provavelmente há outra habilidade que você está perdendo.
Uma solução para o seu problema específico é injetar
frame
algo (uma classe ou método) que sabe conversar com quadros, para que você não precise espalhar todos os detalhes de bate-papo de quadros em sua classe, que agora só sabem como e quando obtenha um quadro.Na verdade, isso segue outro princípio: separe o uso da construção.
Ao usar esse código, você assumiu a responsabilidade de adquirir um
Frame
. Você ainda não assumiu nenhuma responsabilidade por conversar com aFrame
.Agora você precisa saber como conversar com a
Frame
, mas substitua por:E agora você só precisa saber como conversar com seu amigo, FrameHandler.
Existem muitas maneiras de conseguir isso e talvez essa não seja a melhor, mas mostra como seguir o princípio não torna o problema insolúvel. Só está exigindo mais trabalho de você.
Toda boa regra tem uma exceção. Os melhores exemplos que conheço são os idiomas específicos do domínio interno . Um DSL s cadeia de métodosParece abusar da Lei de Demeter o tempo todo, porque você está constantemente chamando métodos que retornam tipos diferentes e os usa diretamente. Por que isso está bem? Como em uma DSL, tudo o que é retornado é um amigo cuidadosamente projetado com o qual você deve conversar diretamente. Por design, você recebeu o direito de esperar que a cadeia de métodos de uma DSL não seja alterada. Você não tem esse direito se apenas mergulhar aleatoriamente na base de código encadeando o que encontrar. As melhores DSLs são representações ou interfaces muito finas para outros objetos dos quais você provavelmente não deve se aprofundar. Só mencionei isso porque descobri que compreendi muito melhor a lei do demeter depois que aprendi por que as DSLs são um bom design. Alguns chegam ao ponto de dizer que as DSLs nem mesmo violam a verdadeira lei do demeter.
Outra solução é deixar algo mais injetar
frame
em você. Se vierframe
de um levantador ou, de preferência, de um construtor, você não assumirá nenhuma responsabilidade pela construção ou aquisição de um quadro. Isso significa que seu papel aqui é muito mais parecidoFrameHandlers
com o que seria. Em vez disso, agora é você quem está conversandoFrame
e fazendo outra coisa descobrir como obter um.Frame
De certa forma, essa é a mesma solução com uma simples mudança de perspectiva.Os princípios do SOLID são os grandes que tento seguir. Dois sendo respeitados aqui são os princípios de responsabilidade única e inversão de dependência. É realmente difícil respeitar esses dois e ainda acaba violando a Lei de Demeter.
A mentalidade que entra em violar Demeter é como comer em um restaurante buffet, onde você simplesmente pega o que quiser. Com um pouco de trabalho adiantado, você pode fornecer um menu e um servidor que lhe trarão o que quiser. Sente-se, relaxe e incline bem.
fonte
O design funcional é melhor que o design orientado a objetos? Depende.
MVVM é melhor que MVC? Depende.
Amos e Andy ou Martin e Lewis? Depende.
Do que isso depende? As escolhas que você faz dependem de quão bem cada técnica ou tecnologia atende aos requisitos funcionais e não funcionais do seu software, enquanto satisfaz adequadamente seus objetivos de design, desempenho e capacidade de manutenção.
Ao ler isso em um livro ou blog, avalie a afirmação com base em seu mérito; ou seja, pergunte o porquê. Não há técnica certa ou errada no desenvolvimento de software; há apenas "quão bem essa técnica atende aos meus objetivos? É eficaz ou ineficaz? Ela resolve um problema, mas cria um novo? Pode ser bem compreendida por toda a comunidade?" equipe de desenvolvimento, ou é muito obscuro? "
Nesse caso em particular - o ato de chamar um método em um objeto retornado por outro método - como existe um padrão de design real que codifica essa prática (Factory), é difícil imaginar como alguém poderia fazer a afirmação de que é categoricamente errado.
O motivo pelo qual é chamado "Princípio do Menos Conhecimento" é que "Baixo Acoplamento" é a qualidade desejável de um sistema. Objetos que não estão fortemente ligados um ao outro funcionam de forma mais independente e, portanto, são mais fáceis de manter e modificar individualmente. Mas, como mostra o seu exemplo, há momentos em que o acoplamento alto é mais desejável, para que os objetos possam coordenar com mais eficiência seus esforços.
fonte
A resposta de Doc Brown mostra uma implementação clássica de livros didáticos da Lei de Demeter - e o incômodo / desorganização do código de adicionar dezenas de métodos dessa maneira é provavelmente o motivo pelo qual os programadores, inclusive eu, geralmente não se incomodam em fazê-lo, mesmo que devessem.
Existe uma maneira alternativa de dissociar a hierarquia de objetos:
No caso de Pôster original (OP),
encoder->WaitEncoderFrame()
retornaria um emIEncoderFrame
vez de aFrame
e definiria quais operações são permitidas.SOLUÇÃO 1
No caso mais fácil,
Frame
e asEncoder
classes estão sob seu controle,IEncoderFrame
é um subconjunto de métodos que o Frame já expõe publicamente e aEncoder
classe não se importa com o que você faz com esse objeto. Em seguida, a implementação é trivial ( código em c # ):SOLUÇÃO 2
Em um caso intermediário, em que a
Frame
definição não está sob seu controle ou não seria apropriado adicionarIEncoderFrame
os métodos deFrame
, então uma boa solução é um Adaptador . É isso que a resposta de CandiedOrange discute, comonew FrameHandler( frame )
. IMPORTANTE: Se você fizer isso, será mais flexível se você o expuser como uma interface , não como uma classe .Encoder
precisa conhecerclass FrameHandler
, mas os clientes precisam apenas saberinterface IFrameHandler
. Ou como eu o nomeei,interface IEncoderFrame
- para indicar que é especificamente Frame, como visto no POV do codificador :CUSTO: Alocação e GC de um novo objeto, o EncoderFrameWrapper, toda vez que
encoder.TheFrame
é chamado. (Você pode armazenar em cache esse invólucro, mas isso adiciona mais código. E só é fácil codificar com segurança se o campo de quadro do codificador não puder ser substituído por um novo quadro.)SOLUÇÃO 3
No caso mais difícil, o novo wrapper precisaria saber sobre ambos
Encoder
eFrame
. Esse objeto violaria o LoD - ele está manipulando um relacionamento entre o Codificador e o Quadro que deveria ser de responsabilidade do Codificador - e provavelmente seria um problema para acertar. Aqui está o que pode acontecer se você começar por esse caminho:Isso ficou feio. Há uma implementação menos complicada, quando o wrapper precisa tocar nos detalhes de seu criador / proprietário (Encoder):
É verdade que, se soubesse que terminaria aqui, talvez não o fizesse. Poderia simplesmente escrever os métodos LoD e terminar com ele. Não há necessidade de definir uma interface. Por outro lado, gosto que a interface agrupe métodos relacionados. Eu gosto de como é fazer as "operações tipo quadro" com o que parece um quadro.
COMENTÁRIOS FINAIS
Considere o seguinte: se o implementador
Encoder
considerou que a exposiçãoFrame frame
era apropriada à sua arquitetura geral ou "era muito mais fácil do que implementar o LoD", seria muito mais seguro se eles fizessem o primeiro trecho que eu mostro - exponha um subconjunto limitado de Quadro, como uma interface. Na minha experiência, isso geralmente é uma solução completamente viável. Basta adicionar métodos à interface, conforme necessário. (Estou falando de um cenário em que "sabemos" que o Frame já possui os métodos necessários, ou eles seriam fáceis e não controversos de serem adicionados. O trabalho de "implementação" para cada método está adicionando uma linha à definição da interface.) E sabemos que, mesmo no pior cenário futuro, é possível manter a API funcionando - aqui,IEncoderFrame
Frame
Encoder
.Observe também que se você não tem permissão para adicionar
IEncoderFrame
aFrame
, ou os métodos necessários não se encaixam bem com o generalFrame
classe e solução # 2 não combina com você, talvez por causa do objeto extra de criação e destruição, A solução 3 pode ser vista simplesmente como uma maneira de organizar os métodos deEncoder
realizar LoD. Não basta passar por dezenas de métodos. Envolva-os em uma Interface e use "implementação explícita da interface" (se você estiver em c #), para que eles só possam ser acessados quando o objeto for visualizado através dessa interface.Outro ponto que quero enfatizar é que a decisão de expor a funcionalidade como uma interface lidou com todas as três situações descritas acima. No primeiro,
IEncoderFrame
é simplesmente um subconjunto daFrame
funcionalidade. No segundo,IEncoderFrame
é um adaptador. No terceiro,IEncoderFrame
é uma partição paraEncoder
a funcionalidade s. Não importa se suas necessidades mudam entre essas três situações: a API permanece a mesma.fonte