Explicação sobre como "Tell, Don't Ask" é considerado bom OO

47

Esta postagem do blog foi postada no Hacker News com vários upvotes. Vindo do C ++, a maioria desses exemplos parece ir contra o que me foi ensinado.

Como no exemplo 2:

Ruim:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

versus bom:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

O conselho em C ++ é que você deve preferir funções livres em vez de funções membro, pois elas aumentam o encapsulamento. Ambos são idênticos semanticamente, então por que preferir a escolha que tem acesso a mais estado?

Exemplo 4:

Ruim:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

versus bom:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

Por que é de responsabilidade do User formatar uma string de erro não relacionada? E se eu quiser fazer algo além de imprimir 'No street name on file' se não tiver rua? E se a rua tiver o mesmo nome?

Alguém poderia me esclarecer sobre as vantagens e justificativas de "Conte, não pergunte"? Eu não estou procurando o que é melhor, mas tentando entender o ponto de vista do autor.

    
por Pubby 20.07.2012 / 02:30
fonte

9 respostas

78

Perguntar ao objeto sobre seu estado e, em seguida, chamar métodos nesse objeto com base em decisões tomadas fora do objeto significa que o objeto agora é uma abstração com vazamento; alguns de seus comportamentos estão localizados fora do objeto, e o estado interno é exposto (talvez desnecessariamente) ao mundo externo.

You should endeavor to tell objects what you want them to do; do not ask them questions about their state, make a decision, and then tell them what to do.

The problem is that, as the caller, you should not be making decisions based on the state of the called object that result in you then changing the state of the object. The logic you are implementing is probably the called object’s responsibility, not yours. For you to make decisions outside the object violates its encapsulation.

Sure, you may say, that’s obvious. I’d never write code like that. Still, it’s very easy to get lulled into examining some referenced object and then calling different methods based on the results. But that may not be the best way to go about doing it. Tell the object what you want. Let it figure out how to do it. Think declaratively instead of procedurally!

It is easier to stay out of this trap if you start by designing classes based on their responsibilities; you can then progress naturally to specifying commands that the class may execute, as opposed to queries that inform you as to the state of the object.

link

    
por 20.07.2012 / 02:48
fonte
16

Geralmente, o artigo sugere que você não deve expor o estado do membro para os outros raciocinar, se você puder raciocinar sobre ele mesmo .

No entanto, o que não está claramente indicado é que esta lei se enquadra em limites óbvios quando o raciocínio ultrapassa a responsabilidade de uma classe específica. Por exemplo, toda classe cujo trabalho é manter algum valor ou fornecer algum valor - especialmente os genéricos, ou onde a classe fornece um comportamento que deve ser estendido.

Por exemplo, se o sistema fornecer o temperature como uma consulta, amanhã, o cliente poderá check_for_underheating sem precisar alterar SystemMonitor . Este não é o caso quando o SystemMonitor implementa check_for_overheating em si. Assim, uma classe SystemMonitor , cujo trabalho é gerar um alarme quando a temperatura está muito alta, segue isso, mas uma classe SystemMonitor cujo trabalho é permitir que outro trecho de código leia a temperatura para que ela possa controlar, digamos , TurboBoost ou algo assim, não deveria.

Observe também que o segundo exemplo usa inutilmente o Anti-padrão de objeto nulo.

    
por 20.07.2012 / 03:42
fonte
9

O problema real com o seu exemplo de superaquecimento é que as regras para o que qualifica como superaquecimento não são facilmente variadas para sistemas diferentes. Suponha que o Sistema A esteja como você tem (temp > 100 está superaquecendo) mas o Sistema B é mais delicado (temp > 93 está superaquecendo). Você muda sua função de controle para verificar o tipo de sistema e, em seguida, aplica o valor correto?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

Ou você tem cada tipo de sistema definindo sua capacidade de aquecimento?

EDITAR:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

O jeito antigo faz sua função de controle ficar feia quando você começa a lidar com mais sistemas. O último permite que a função de controle seja estável com o passar do tempo.

    
por 20.07.2012 / 19:16
fonte
6
Primeiro, eu sinto que devo fazer uma exceção à sua caracterização dos exemplos como "ruim" e "bom". O artigo usa os termos "Não tão bom" e "Melhor", acho que esses termos foram escolhidos por uma razão: são diretrizes e, dependendo das circunstâncias, a abordagem "Não tão boa" pode ser apropriada ou, na verdade, a única solução.

Quando tiver uma opção, você deverá dar preferência para incluir qualquer funcionalidade que dependa somente da classe na classe em vez de fora dela - o motivo é por causa de encapsulamento, e o fato de que facilita a evolução da aula ao longo do tempo. A classe também faz um trabalho melhor de anunciar seus recursos do que um monte de funções gratuitas.

Às vezes você precisa contar, porque a decisão depende de algo fora da classe ou porque é simplesmente algo que você não deseja que a maioria dos usuários da classe faça. Às vezes você quer contar, porque o comportamento é contra-intuitivo para a classe e você não quer confundir a maioria dos usuários da classe.

Por exemplo, você reclama que o endereço da rua retorna uma mensagem de erro, não é, o que está fazendo é fornecer um valor padrão. Mas às vezes um valor padrão não é apropriado. Se esse for o estado ou a cidade, talvez você queira um padrão ao atribuir um registro a um vendedor ou a um pesquisador, para que todos os desconhecidos sejam direcionados a uma pessoa específica. Por outro lado, se você estiver imprimindo envelopes, talvez prefira uma exceção ou proteção que evite que você desperdice papel em cartas que não podem ser entregues.

Portanto, pode haver casos em que "Não tão bom" é o caminho a percorrer, mas geralmente, "Melhor" é, bem, melhor.

    
por 20.07.2012 / 05:18
fonte
3

Anti-simetria de dados / objetos

Como outros salientaram, o Tell-Dont-Ask é especificamente para casos em que você altera o estado do objeto depois que você pergunta (veja, por exemplo, o texto do Pragprog publicado em outro lugar nesta página). Este nem sempre é o caso, e. o objeto 'usuário' não é alterado depois de ser solicitado seu endereço de usuário. É, portanto, discutível se este for um caso apropriado para aplicar Tell-Dont-Ask.

Tell-Dont-Ask está preocupado com a responsabilidade, sem tirar a lógica de uma classe que deveria estar justificadamente dentro dela. Mas nem toda lógica que lida com objetos é necessariamente lógica desses objetos. Isso está insinuando em um nível mais profundo, mesmo além do Tell-Dont-Ask, e eu quero acrescentar uma pequena observação sobre isso.

Como uma questão de design arquitetônico, você pode querer ter objetos que são apenas recipientes para propriedades, talvez até mesmo imutáveis, e então executar várias funções sobre coleções de tais objetos, avaliando, filtrando ou transformando-as em vez de enviar comandos (que é mais o domínio de Tell-Dont-Ask).

A decisão que é mais apropriada para o seu problema depende se você espera ter dados estáveis (os objetos declarativos), mas com mudança / adição no lado da função. Ou se você espera ter um conjunto estável e limitado de tais funções, mas esperar mais fluxo no nível dos objetos, por exemplo, adicionando novos tipos. Na primeira situação você preferiria funções livres, nos segundos métodos de objetos.

Bob Martin, em seu livro "Código Limpo", chama isso de "Anti-Simetria de Objetos / Dados" (p.95ff), outras comunidades podem se referir a ele como " problema de expressão ".

    
por 22.07.2014 / 15:12
fonte
3

Esse paradigma às vezes é chamado de "Diga, não pergunte" , o que significa dizer ao objeto o que fazer, não pergunte sobre seu estado; e às vezes como "strong", "pergunte, não diga", ou seja, peça ao objeto para fazer algo por você, não diga qual deve ser seu estado. De qualquer forma, a melhor prática é a mesma - a maneira como um objeto deve executar uma ação é a preocupação desse objeto, não o objeto do chamador. As interfaces devem evitar expor seu estado (por exemplo, através de acessores ou propriedades públicas) e, em vez disso, expor métodos de "execução" cuja implementação é opaca. Outros cobriram isso com os links para o programador pragmático.

Esta regra está relacionada com a regra de evitar o código "double-dot" ou "double arrow", frequentemente referido como "Apenas falar com amigos imediatos", o que indica que foo->getBar()->doSomething() é ruim, em vez disso use foo->doSomething(); é uma chamada de wrapper em torno da funcionalidade da barra e implementada como simplesmente return bar->doSomething(); - se foo for responsável pelo gerenciamento de bar , deixe-o em seguida!

    
por 24.02.2016 / 15:38
fonte
1

Além das outras boas respostas sobre "diga, não pergunte", alguns comentários sobre seus exemplos específicos podem ajudar:

The advice in C++ is that you should prefer free functions instead of member functions as they increase encapsulation. Both of these are identical semantically, so why prefer the choice that has access to more state?

Essa escolha não tem acesso a mais estado. Ambos usam a mesma quantidade de estado para realizar seus trabalhos, mas o exemplo "ruim" requer que o estado da classe seja público para fazer seu trabalho. Além disso, o comportamento dessa classe no exemplo 'ruim' está espalhado para a função livre, tornando mais difícil de encontrar e mais difícil de refatorar.

Why is it the responsibility of User to format an unrelated error string? What if I want to do something besides print 'No street name on file' if it has no street? What if the street is named the same thing?

Por que é da responsabilidade de 'street_name' fazer os dois 'get street name' e 'provide error message'? Pelo menos na versão "boa", cada peça tem uma responsabilidade. Ainda assim, não é um ótimo exemplo.

    
por 20.07.2012 / 03:37
fonte
1

Essas respostas são muito boas, mas aqui está outro exemplo para enfatizar: observe que geralmente é uma maneira de evitar duplicação. Por exemplo, digamos que você tenha VÁRIOS lugares com um código como:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

Isso significa que é melhor você ter algo assim:

Product product = productMgr.get(productUuid, currentUser)

Como essa duplicação significa que a maioria dos clientes da sua interface usaria o novo método, em vez de repetir a mesma lógica aqui e ali. Você dá ao seu delegado o trabalho que deseja, em vez de pedir as informações necessárias para fazer você mesmo.

    
por 13.01.2016 / 17:35
fonte
0

Acredito que isso seja mais verdadeiro ao escrever objetos de alto nível, mas menos verdadeiro ao passar para o nível mais profundo, por exemplo, biblioteca de classes, pois é impossível escrever todos os métodos para satisfazer todos os consumidores de classe.

Por exemplo, # 2, acho que é simplificado demais. Se realmente implementássemos isso, o SystemMonitor acabaria tendo o código para acesso de hardware de baixo nível e lógica para abstração de alto nível incorporada na mesma classe. Infelizmente, se estamos tentando separar isso em duas classes, violaríamos o "Tell, Don't ask" em si.

O exemplo 4 é mais ou menos o mesmo - está incorporando a lógica da interface do usuário à camada de dados. Agora, se vamos consertar o que o usuário quer ver em caso de nenhum endereço, temos que corrigir o objeto na camada de dados, e se dois projetos usam esse mesmo objeto, mas precisam usar texto diferente para endereço nulo?

Concordo que, se pudermos implementar "Diga, não peça" por tudo, seria muito útil - eu mesmo ficaria feliz se pudesse apenas contar em vez de perguntar (e fazer eu mesmo) na vida real ! No entanto, da mesma forma que na vida real, a viabilidade da solução é muito limitada a classes de alto nível.

    
por 20.07.2012 / 19:38
fonte