O princípio do menor conhecimento

32

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?

resgate
fonte
possível duplicata de Método encadeamento vs encapsulamento
mosquito
4
Provavelmente é uma duplicata mais próxima.
Robert Harvey
1
Relacionado: Código como este é um "acidente de trem" (em violação da Lei de Demeter)? (divulgação completa: essa é minha própria pergunta)
um CVn

Respostas:

21

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

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

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.

Doc Brown
fonte
Essa abordagem tem a vantagem de facilitar muito o teste de unidade, a depuração e o raciocínio sobre seu código.
Gntskn 25/05
+1. Porém, há uma boa razão para não fazer essa alteração de código: a interface da classe pode ficar inchada com métodos cuja tarefa é apenas fazer uma ou algumas chamadas para vários outros métodos (e sem lógica adicional). Em alguns tipos de ambiente de programação, como C ++ COM (principalmente WIC e DirectX), há um alto custo associado à adição de cada método único a uma interface COM, em comparação com outros idiomas.
Rwong 25/05
1
No design de interface C ++ COM, decompor grandes classes em classes pequenas (o que significa que você tem mais chances de ter que conversar com vários objetos) e minimizar o número de métodos de interface são dois objetivos de design com benefícios reais (reduções de custo) com base em um entendimento profundo da mecânica interna (tabelas virtuais, reutilização de código, muitas outras coisas). Portanto, os programadores de C ++ COM normalmente devem ignorar o LoD.
Rwong 25/05
10
+1: A Lei de Deméter deve realmente ser chamada de Sugestão de Deméter: YMMV
Preocupante Binário
A lei do demeter é um conselho para fazer escolhas arquitetônicas, enquanto você sugere uma mudança cosmética, para que pareça estar em conformidade com essa 'lei'. Isso seria um equívoco, porque uma mudança sintática não significa que de repente tudo está bem. Até onde eu sei, a lei do demeter basicamente significava: se você deseja fazer OOP, pare de escrever código prodecural com funções getter em todos os lugares.
user2180613
39

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 framealgo (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.

frame = encoder->WaitEncoderFrame()

Ao usar esse código, você assumiu a responsabilidade de adquirir um Frame. Você ainda não assumiu nenhuma responsabilidade por conversar com a Frame.

frame->DoOrGetSomething(); 

Agora você precisa saber como conversar com a Frame, mas substitua por:

new FrameHandler(frame)->DoOrGetSomething();

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 frameem você. Se vier framede 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 parecido FrameHandlerscom o que seria. Em vez disso, agora é você quem está conversando Framee 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.

candied_orange
fonte
2
"Diz a você que é melhor conversar apenas com seus" amigos "e não com" amigos de amigos "" "+ +
RubberDuck
1
O segundo parágrafo, é roubado de algum lugar, ou você inventou? Isso me fez entender completamente o conceito . Tornou flagrante o que "amigos de amigos" são e quais as desvantagens de envolver muitas classes (articulações). Citação A +.
die maus 23/01
2
@diemaus Se eu roubei esse parágrafo do escudo de algum lugar, a fonte vazou da minha cabeça. Naquele momento, meu cérebro pensou que estava sendo inteligente. O que me lembro é que o adicionei após muitas das votações, por isso é bom vê-lo validado. Ainda bem que ajudou.
candied_orange
19

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.

[Algum livro] diz que [alguma coisa] está errada.

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.

Robert Harvey
fonte
2

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:

Exponha interfacetipos, em vez de classtipos, através de seus métodos e propriedades.

No caso de Pôster original (OP), encoder->WaitEncoderFrame()retornaria um em IEncoderFramevez de a Framee definiria quais operações são permitidas.


SOLUÇÃO 1

No caso mais fácil, Framee as Encoderclasses estão sob seu controle, IEncoderFrameé um subconjunto de métodos que o Frame já expõe publicamente e a Encoderclasse não se importa com o que você faz com esse objeto. Em seguida, a implementação é trivial ( código em c # ):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

SOLUÇÃO 2

Em um caso intermediário, em que a Framedefinição não está sob seu controle ou não seria apropriado adicionar IEncoderFrameos métodos de Frame, então uma boa solução é um Adaptador . É isso que a resposta de CandiedOrange discute, como new FrameHandler( frame ). IMPORTANTE: Se você fizer isso, será mais flexível se você o expuser como uma interface , não como uma classe . Encoderprecisa conhecer class FrameHandler, mas os clientes precisam apenas saber interface IFrameHandler. Ou como eu o nomeei, interface IEncoderFrame- para indicar que é especificamente Frame, como visto no POV do codificador :

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

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 Encodere Frame. 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:

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

Isso ficou feio. Há uma implementação menos complicada, quando o wrapper precisa tocar nos detalhes de seu criador / proprietário (Encoder):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

É 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 Encoderconsiderou que a exposição Frame frameera 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,IEncoderFrameFrameEncoder.

Observe também que se você não tem permissão para adicionar IEncoderFramea Frame, ou os métodos necessários não se encaixam bem com o general Frameclasse 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 de Encoderrealizar 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 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 da Framefuncionalidade. No segundo, IEncoderFrameé um adaptador. No terceiro, IEncoderFrameé uma partição para Encodera funcionalidade s. Não importa se suas necessidades mudam entre essas três situações: a API permanece a mesma.

Fabricante de ferramentas
fonte
Acoplar sua classe a uma interface retornada por um de seus colaboradores, em vez de uma classe concreta, enquanto uma melhoria, ainda é uma fonte de acoplamento. Se a interface precisar mudar, significa que sua classe precisará mudar. O ponto importante a tirar é que o acoplamento não é inerentemente ruim , desde que você evite o acoplamento desnecessário a objetos instáveis ​​ou estruturas internas . É por isso que a Lei de Deméter precisa ser tomada com uma grande pitada de sal; instrui você a sempre evitar algo que pode ou não ser um problema, dependendo das circunstâncias.
Periata Breatta
@PeriataBreatta - Eu certamente não posso e não discordo disso. Mas eu gostaria de salientar uma coisa: uma interface , por definição, representa o que precisa ser conhecido na fronteira entre as duas classes. Se "precisar mudar", isso é fundamental - nenhuma abordagem alternativa poderia de alguma forma evitar magicamente a codificação necessária. Compare isso ao executar qualquer uma das três situações que eu descrevo, mas sem usar uma interface - em vez disso, retornar uma classe concreta. Em 1, Quadro, em 2, EncoderFrameWrapper, em 3, Codificador. Você se trava nessa abordagem. A interface pode se adaptar a todos eles.
Home
@PeriataBreatta ... que demonstra o benefício de defini-lo explicitamente como uma interface. Espero, eventualmente, aprimorar um IDE para torná-lo tão conveniente, que as interfaces sejam usadas com muito mais força. A maioria dos acessos em vários níveis seria através de alguma interface, portanto, seria muito mais fácil gerenciar as alterações. (Se isso é "exagero" em alguns casos, a análise de código, combinada com anotações sobre onde estamos dispostos a correr o risco de não ter uma interface, em troca do pequeno aumento de desempenho, pode "compilar" - substituindo-a por uma daquelas 3 classes de betão a partir das soluções "3").
ToolmakerSteve
@PeriataBreatta - e quando digo "a interface pode se adaptar a todos", não estou dizendo "ahhhh, é maravilhoso que esse recurso de um idioma possa cobrir esses casos diferentes". Estou dizendo que a definição de interfaces minimiza as alterações que você pode precisar fazer. Na melhor das hipóteses, o design pode mudar desde o caso mais simples (solução 1) até o caso mais difícil (solução 3), sem que a interface seja alterada - apenas as necessidades internas do produtor se tornaram mais complexas. E mesmo quando são necessárias mudanças, elas tendem a ser menos difundidas, IMHO.
Home