Trilhos: Lei da Confusão Deméter


13

Estou lendo um livro chamado Rails AntiPatterns e eles falam sobre o uso de delegação para evitar violar a Lei de Demeter. Aqui está o seu exemplo principal:

Eles acreditam que chamar algo assim no controlador é ruim (e eu concordo)

@street = @invoice.customer.address.street

A solução proposta é fazer o seguinte:

class Customer

    has_one :address
    belongs_to :invoice

    def street
        address.street
    end
end

class Invoice

    has_one :customer

    def customer_street
        customer.street
    end
end

@street = @invoice.customer_street

Eles estão afirmando que, como você usa apenas um ponto, não está violando a Lei de Deméter aqui. Acho que isso está incorreto, porque você ainda está passando pelo cliente para passar pelo endereço e obter a rua da fatura. Eu obtive essa ideia principalmente de uma postagem de blog que li:

http://www.dan-manges.com/blog/37

Na postagem do blog, o principal exemplo é

class Wallet
  attr_accessor :cash
end
class Customer
  has_one :wallet

  # attribute delegation
  def cash
    @wallet.cash
  end
end

class Paperboy
  def collect_money(customer, due_amount)
    if customer.cash < due_ammount
      raise InsufficientFundsError
    else
      customer.cash -= due_amount
      @collected_amount += due_amount
    end
  end
end

Os estados postagem no blog que apenas um, embora haja pontilham customer.cashem vez de customer.wallet.cash, este código ainda viola a Lei de Demeter.

Agora, no método Paperboy collect_money, não temos dois pontos, apenas um em "customer.cash". Esta delegação resolveu o nosso problema? De modo nenhum. Se observarmos o comportamento, um entregador de jornal ainda está chegando diretamente na carteira de um cliente para sacar dinheiro.

EDITAR

Entendo e concordo completamente que isso ainda é uma violação e preciso criar um método Walletchamado de retirada que lide com o pagamento e que eu deva chamar esse método dentro da Customerclasse. O que não entendo é que, de acordo com esse processo, meu primeiro exemplo ainda viola a Lei de Deméter, porque Invoiceainda está alcançando diretamente Customera rua.

Alguém pode me ajudar a esclarecer a confusão. Eu tenho procurado nos últimos 2 dias tentando deixar este tópico afundar, mas ainda é confuso.


2
pergunta semelhante aqui
thorsten müller

Eu não acho que o segundo exemplo (o jornaleiro) do blog viole a Lei de Demeter. Pode ser um design ruim (você está assumindo que o cliente pagará com dinheiro), mas isso NÃO é uma violação da Lei de Deméter. Nem todos os erros de design são causados ​​por violar esta lei. O autor está confuso IMO.
Andres F.

Respostas:


24

Seu primeiro exemplo não viola a lei de Demeter. Sim, com o código como está, dizer @invoice.customer_streetque obtém o mesmo valor que um hipotético @invoice.customer.address.streetteria, mas a cada passo da travessia, o valor retornado é decidido pelo objeto que está sendo solicitado - não é que "o entregador chegue ao carteira do cliente ", é que" o entregador pede dinheiro ao cliente e o cliente recebe o dinheiro da carteira ".

Quando você diz @invoice.customer.address.street, está assumindo o conhecimento dos clientes e endereços internos - isso é ruim. Quando você diz @invoice.customer_street, está perguntando invoice: "Ei, eu gostaria da rua do cliente, você decide como obtê-la ". O cliente então diz para o seu endereço: "ei, eu gostaria da sua rua, você decide como obtê-la ".

O impulso de Deméter não é 'você nunca pode conhecer valores de objetos distantes no gráfico' "; é sim 'você mesmo não deve percorrer muito longe o gráfico de objetos para obter valores'.

Concordo que isso possa parecer uma distinção sutil, mas considere o seguinte: no código compatível com Demeter, quanto código precisa ser alterado quando a representação interna de uma addressalteração? E o código não compatível com Demeter?


Este é exatamente o tipo de explicação que eu estava procurando! Obrigado.
user2158382

Muito boa explicação. Tenho uma pergunta: 1) Se o objeto de fatura deseja retornar um objeto de cliente para o cliente de fatura que não significa necessariamente que é o mesmo objeto de cliente que ele contém internamente. Pode ser simplesmente um objeto criado, dinamicamente, com o objetivo de retornar ao cliente um bom conjunto de dados empacotados com vários valores. Usando a lógica que você apresenta, você está dizendo que a fatura não pode ter um campo que represente mais de um dado. Ou eu estou esquecendo de alguma coisa.
Zumalifeguard 13/10

2

O primeiro exemplo e o segundo não são realmente muito iguais. Enquanto o primeiro fala sobre regras gerais de "um ponto", o segundo fala mais sobre outras coisas no design de OO, especialmente " Diga, Não pergunte "

A delegação é uma técnica eficaz para evitar violações da Lei de Deméter, mas apenas por comportamento, não por atributos. - A partir do segundo exemplo, o blog de Dan

Novamente, " apenas por comportamento, não por atributos "

Se você pedir atributos, você deve perguntar . "Ei, cara, quanto dinheiro você tem no bolso? Mostre-me, vou avaliar se você pode pagar isso." Isso é errado, nenhum balconista de compras se comportará assim. Em vez disso, eles dirão "Por favor, pague"

customer.pay(due_amount)

Será dever do cliente avaliar se ele deve pagar e se ele pode pagar. E a tarefa do funcionário é concluída depois de pedir ao cliente para pagar.

Então, o segundo exemplo prova que o primeiro está errado?

Na minha opinião. Não , desde que:

1. Você faz isso com autocontrole.

Embora você possa acessar todos os atributos do cliente @invoicepor delegação, raramente é necessário em casos normais.

Pense em uma página mostrando uma fatura em um aplicativo Rails. Haverá uma seção no topo para mostrar os detalhes do cliente. Então, no modelo de fatura, você codificará assim?

#customer-info
  = @invoice.customer_name
  = @invoice.customer_address
  ....

Isso é errado e ineficiente. Uma abordagem melhor é

#customer-info
  = render partial: 'invoice_header_customer', 
           locals: {customer: @invoice.customer}

Em seguida, deixe o cliente parcial para processar todos os atributos pertencentes ao cliente.

Então geralmente você não precisa disso. Mas você pode ter uma página de lista mostrando todas as faturas recentes, há um campo de informações em cada um liexibindo o nome do cliente. Nesse caso, você precisa mostrar o atributo do cliente e é totalmente legítimo codificar o modelo como

= @invoice.customer_name

2. Não há nenhuma ação adicional, dependendo desta chamada de método.

No caso acima da página da lista, a fatura perguntou o atributo de nome do cliente, mas seu objetivo real é " mostre seu nome "; portanto, ainda é basicamente um comportamento, mas não um atributo . Não há mais avaliação e ação com base nesse atributo, se o seu nome for "Mike", eu gostarei de você e lhe darei 30 dias a mais de crédito. Não, a fatura apenas diz "mostre seu nome", não mais. Portanto, isso é totalmente aceitável, de acordo com a regra "Tell Don't Ask" no exemplo 2.


0

Leia mais no segundo artigo e acho que a idéia ficará mais clara. A idéia é apenas oferecer ao cliente a capacidade de pagar e ocultar completamente onde o caso é mantido. É um campo, um membro de uma carteira ou outra coisa? O chamador não sabe, não precisa saber e não muda se esse detalhe da implementação mudar.

class Wallet
  attr_accessor :cash
  def withdraw(amount)
     raise InsufficientFundsError if amount > cash
     cash -= amount
     amount
  end
end
class Customer
  has_one :wallet
  # behavior delegation
  def pay(amount)
    @wallet.withdraw(amount)
  end
end
class Paperboy
  def collect_money(customer, due_amount)
    @collected_amount += customer.pay(due_amount)
  end
end

Acho que sua segunda referência está dando uma recomendação mais útil.

A única idéia de "um ponto" é um sucesso parcial, pois esconde alguns detalhes profundos, mas ainda aumenta o acoplamento entre componentes separados.


Desculpe, talvez não tenha sido claro, mas entendo perfeitamente o segundo exemplo e entendo que você precisa fazer a abstração que publicou, mas o que não entendo é o meu primeiro exemplo. De acordo com a postagem do blog, meu primeiro exemplo está incorreto #
user2158382

0

Parece que Dan derrotou seu exemplo neste artigo: The Paperboy, The Wallet e The Law Of Demeter

Lei de Demeter Um método de um objeto deve invocar apenas os métodos dos seguintes tipos de objetos:

  1. em si
  2. seus parâmetros
  3. quaisquer objetos que ele cria / instancia
  4. seus objetos componentes diretos

Quando e como aplicar a lei de Demeter

Portanto, agora você tem um bom entendimento da lei e de seus benefícios, mas ainda não discutimos como identificar lugares no código existente onde podemos aplicá-lo (e igualmente importante, onde NÃO o aplicar ...)

  1. Instruções 'get' encadeadas - O primeiro e mais óbvio lugar para aplicar a Lei de Demeter são os locais de código que possuem get() instruções repetidas ,

    value = object.getX().getY().getTheValue();

    como se nossa pessoa canônica desse exemplo fosse detida pelo policial, poderíamos ver:

    license = person.getWallet().getDriversLicense();

  2. muitos objetos 'temporários' - o exemplo de licença acima não seria melhor se o código parecesse,

    Wallet tempWallet = person.getWallet(); license = tempWallet.getDriversLicense();

    é equivalente, mas mais difícil de detectar.

  3. Importando muitas classes - No projeto Java em que trabalho, temos uma regra que importa apenas as classes que realmente usamos; você nunca vê algo como

    import java.awt.*;

    no nosso código fonte. Com essa regra em vigor, não é incomum ver uma dúzia de instruções de importação, todas provenientes do mesmo pacote. Se isso estiver acontecendo no seu código, pode ser um bom lugar para procurar exemplos obscuros de violações. Se você precisar importá-lo, estará acoplado a ele. Se mudar, talvez você precise também. Ao importar explicitamente as classes, você começará a ver como suas classes realmente são acopladas.

Entendo que seu exemplo está em Ruby, mas isso deve se aplicar a todos os idiomas OOP.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.