Explicação sobre como “Diga, Não Pergunte” é considerado bom OO

49

Este post do blog foi publicado no Hacker News com vários upvotes. Vindo do C ++, a maioria desses exemplos parece ir contra o que aprendi.

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 aumentam o encapsulamento. Ambos são idênticos semanticamente, então por que preferir a escolha que tem acesso a mais estados?

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 é da responsabilidade de Userformatar uma sequência de erros 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 a lógica "Diga, não pergunte"? Não estou procurando o que é melhor, mas tentando entender o ponto de vista do autor.

Pubby
fonte
Exemplos de código podem ser Ruby e não Python, não sei.
Pubby
2
Eu sempre me pergunto se algo como o primeiro exemplo não é mais uma violação do SRP?
Stijn
11
Você pode ler que: pragprog.com/articles/tell-dont-ask
Mik378
Rubi. @ é uma abreviação, por exemplo, e o Python encerra seus blocos implicitamente com espaço em branco.
Erik Reppen
3
"O conselho em C ++ é que você deve preferir funções livres em vez de funções membro, pois aumentam o encapsulamento." Não sei quem te disse isso, mas não é verdade. Funções livres podem ser usadas para aumentar o encapsulamento, mas não necessariamente aumentam o encapsulamento.
Rob K

Respostas:

81

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; parte de seu comportamento está localizado fora do objeto, e o estado interno é exposto (talvez desnecessariamente) ao mundo externo.

Você deve tentar dizer aos objetos o que você quer que eles façam; não faça perguntas sobre seu estado, tome uma decisão e depois diga a eles o que fazer.

O problema é que, como responsável pela chamada, você não deve tomar decisões com base no estado do objeto chamado que resulta na alteração do estado do objeto. A lógica que você está implementando provavelmente é de responsabilidade do objeto chamado, não sua. Para você tomar decisões fora do objeto, viola seu encapsulamento.

Claro, você pode dizer, isso é óbvio. Eu nunca escreveria um código assim. Mesmo assim, é muito fácil examinar o objeto referenciado e chamar métodos diferentes com base nos resultados. Mas essa pode não ser a melhor maneira de fazê-lo. Diga ao objeto o que você deseja. Deixe-o descobrir como fazê-lo. Pense declarativamente em vez de processualmente!

É mais fácil ficar fora dessa armadilha se você começar criando classes com base em suas responsabilidades; então você pode progredir naturalmente para especificar comandos que a classe pode executar, em vez de consultas que o informam sobre o estado do objeto.

http://pragprog.com/articles/tell-dont-ask

Robert Harvey
fonte
4
O texto de exemplo não permite muitas coisas que são claramente boas práticas.
DeadMG
13
O @DeadMG faz o que você diz apenas para os que são escravizados, que ignoram cegamente "pragmático" no nome do site e no pensamento principal dos autores do site, que foi claramente indicado em seu livro-chave: "não existe a melhor solução ... "
gnat
2
Nunca leia o livro. Nem eu gostaria. Eu li apenas o texto de exemplo, que é completamente justo.
DeadMG
3
@DeadMG não se preocupe. Agora que você sabe o ponto principal que coloca esse exemplo (e qualquer outro da pragprog) no contexto pretendido ("não existe a melhor solução ..."), não há problema em não ler o livro
gnat
11
Ainda não tenho certeza do que o Tell, Don't Ask deve explicar para você sem contexto, mas esse é realmente um bom conselho de OOP.
precisa
16

Geralmente, a peça sugere que você não deve expor o estado membro para que outros possam raciocinar, se você puder raciocinar a respeito .

No entanto, o que não está claramente indicado é que essa lei cai em limites muito óbvios quando o raciocínio está acima da 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 oferece um comportamento que deve ser estendido.

Por exemplo, se o sistema fornecer a temperatureconsulta, amanhã, o cliente poderá, check_for_underheatingsem precisar alterar SystemMonitor. Este não é o caso quando o próprio SystemMonitorimplementa check_for_overheating. Portanto, uma SystemMonitorclasse cujo trabalho é acionar um alarme quando a temperatura está muito alta segue isso - mas uma SystemMonitorclasse cujo trabalho é permitir que outro trecho de código leia a temperatura para que ele possa controlar, digamos, o TurboBoost ou algo parecido , não deveria.

Observe também que o segundo exemplo usa inutilmente o Antipadrão de Objeto Nulo.

DeadMG
fonte
19
“Objeto nulo” não é o que eu chamaria de antipadrão, então me pergunto qual é o seu motivo para isso.
Konrad Rudolph
4
Certeza de que ninguém possui nenhum método especificado como "Não faz nada". Isso faz com que chamá-los seja inútil. Isso significa que qualquer objeto que implementa Objeto Nulo quebra, no mínimo, o LSP e se descreve como operações de implementação que, de fato, não o faz. O usuário espera um valor de volta. A correção do programa deles depende disso. Você simplesmente traz mais problemas fingindo que é um valor quando não é. Você já tentou depurar métodos com falha silenciosa? É impossível e ninguém deveria sofrer com isso.
DeadMG
4
Eu diria que isso depende inteiramente do domínio do problema.
21412 Konrad Rudolph
5
@DeadMG Concordo que o exemplo acima é um mau uso do padrão de objeto nulo, mas não é um mérito para usá-lo. Algumas vezes eu usei uma implementação 'no-op' de alguma interface ou outra para evitar a verificação nula ou a verdadeira 'nula' permeando o sistema.
Max
6
Não tenho certeza de que entendi seu ponto de vista com "o cliente pode check_for_underheatingsem precisar alterar SystemMonitor". Como o cliente é diferente SystemMonitordaquele momento? Agora você não está dissipando sua lógica de monitoramento em várias classes? Também não vejo o problema com uma classe de monitor que fornece informações de sensor para outras classes enquanto reserva funções de alarme para si. O controlador de reforço deve controlar o reforço sem ter que se preocupar em disparar um alarme se a temperatura ficar muito alta.
TMN
9

O problema real com o seu exemplo de superaquecimento é que as regras para o que se qualifica como superaquecimento não são facilmente variadas para diferentes sistemas. Suponha que o Sistema A seja o que você tem (temperatura> 100 está superaquecendo), mas o Sistema B é mais delicado (temperatura> 93 está superaquecendo). Você altera sua função de controle para verificar o tipo de sistema e 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 que define 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

A maneira anterior faz com que sua função de controle fique feia quando você começa a lidar com mais sistemas. Este último permite que a função de controle seja estável com o passar do tempo.

Matthew Flynn
fonte
11
Por que não fazer com que cada sistema se registre no monitor. Durante o registro, eles podem indicar quando ocorre superaquecimento.
Martin York
@LokiAstari - Você poderia, mas poderia se deparar com um novo sistema que também é sensível à umidade ou à pressão atmosférica. O princípio é o resumo que varia - neste caso, é a susceptibilidade ao superaquecimento
Matthew Flynn
11
É exatamente por isso que você deve ter um modelo tell. Você informa ao sistema as condições atuais e informa se está fora das condições normais de trabalho. Dessa forma, você nunca precisará modificar o SystemMoniter. Isso é um encapsulamento para você.
Martin York
@LokiAstari - Acho que estamos falando de propósitos diferentes aqui - eu estava realmente pensando em criar sistemas diferentes, em vez de monitores diferentes. O problema é que o sistema deve saber quando está em um estado que aciona um alarme, em oposição a alguma função externa do controlador. O SystemA deve ter seus critérios, o SystemB deve ter seus próprios. O controlador deve apenas poder perguntar (em intervalos regulares) se o sistema está bom ou não.
Matthew Flynn
6

Primeiramente, acho 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: estas 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 for feita uma escolha, você deve preferir incluir qualquer funcionalidade que dependa exclusivamente da classe na classe, e não fora dela - o motivo é o encapsulamento e o fato de facilitar a evolução da classe ao longo do tempo. A turma também faz um trabalho melhor de anunciar suas capacidades do que várias funções gratuitas.

Às vezes, você precisa dizer, 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 dizer, porque o comportamento é contra-intuitivo para a classe e não quer confundir a maioria dos usuários da classe.

Por exemplo, você reclama do endereço que 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 for Estado ou cidade, você pode desejar um padrão ao atribuir um registro a um vendedor ou a um pesquisador, para que todas as incógnitas sejam direcionadas a uma pessoa específica. Por outro lado, se você estava imprimindo envelopes, pode preferir uma exceção ou proteção que evite desperdiçar 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.

jmoreno
fonte
3

Anti-simetria de dados / objetos

Como outros salientaram, o Tell-Dont-Ask é especificamente para os casos em que você altera o estado do objeto após a solicitação (consulte, por exemplo, o texto do Pragprog publicado em outro lugar nesta página). Nem sempre é esse o caso, por exemplo, o objeto 'usuário' não é alterado depois que foi solicitado o seu user.address. Portanto, é discutível se este é um caso apropriado para aplicar o Tell-Dont-Ask.

O Tell-Dont-Ask está preocupado com a responsabilidade, em não retirar 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á sugerindo um nível mais profundo, mesmo além do Tell-Dont-Ask, e quero acrescentar uma breve observação sobre isso.

Por uma questão de design de arquitetura, convém ter objetos que sejam realmente apenas contêineres para propriedades, talvez até imutáveis, e depois executar várias funções nas coleções desses objetos, avaliando, filtrando ou transformando-os em vez de enviar comandos (o que é mais o domínio do Tell-Dont-Ask).

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

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

ThomasH
fonte
3

Esse paradigma às vezes é chamado de 'Diga, não pergunte' , ou seja, diga ao objeto o que fazer, não pergunte sobre seu estado; e às vezes como 'pergunte, não conte' , ou seja, peça ao objeto que faça algo por você, não diga qual deve ser seu estado. De qualquer maneira, a melhor prática é a mesma - a maneira como um objeto deve executar uma ação é a preocupação desse objeto, não a do objeto que está chamando. As interfaces devem evitar expor seu estado (por exemplo, através de acessadores ou propriedades públicas) e, em vez disso, expor métodos 'doing' cuja implementação é opaca. Outros abordaram isso com os links para programadores pragmáticos.

Essa regra está relacionada à regra sobre como evitar o código de "ponto duplo" ou "seta dupla", geralmente chamado de "Fale apenas com amigos imediatos", que afirma foo->getBar()->doSomething()estar incorreto. Em vez disso, use foo->doSomething();uma chamada de wrapper em torno da funcionalidade da barra e implementado da maneira mais simples return bar->doSomething();- se foofor responsável pelo gerenciamento bar, deixe-o fazer isso!

Nicholas Shanks
fonte
1

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

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

Essa escolha não tem acesso a mais estados. Ambos usam a mesma quantidade de estado para realizar seus trabalhos, mas o exemplo 'ruim' exige que o estado de classe seja público para realizar seu trabalho. Além disso, o comportamento dessa classe no exemplo "ruim" se espalha para a função livre, dificultando a localização e refatorando.

Por que é responsabilidade do Usuário formatar uma sequência de erros não relacionada? E se eu quiser fazer algo além de imprimir 'Sem nome de rua registrado' se não tiver rua? E se a rua tiver o mesmo nome?

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

Telastyn
fonte
2
Isso não é verdade. Você presume que verificar se há superaquecimento é a única coisa sensata a fazer com a temperatura. E se a classe pretende ser um dos vários monitores de temperatura e um sistema deve executar ações diferentes, dependendo de muitos de seus resultados, por exemplo? Quando esse comportamento puder ser limitado ao comportamento predefinido de uma única instância, com certeza. Senão, obviamente não pode ser aplicado.
DeadMG
Claro, ou se o termostato e os alarmes existiam em classes diferentes (como provavelmente deveriam).
Telastyn
11
@DeadMG: O conselho geral é tornar as coisas privadas / protegidas até que você precise acessá-las. Embora este exemplo em particular seja meh, isso não contesta a prática padrão.
Guvante
Um exemplo em um artigo sobre a prática de ser 'meh' questiona isso. Se essa prática é padrão por causa de seus grandes benefícios, por que o problema de encontrar um exemplo adequado?
Stijn de Witt
1

Essas respostas são muito boas, mas aqui está outro exemplo apenas 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 parecido com isto:

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 realizar, em vez de pedir as informações necessárias para fazer você mesmo.

antonio.fornie
fonte
0

Eu acredito que isso é mais verdadeiro ao escrever objetos de alto nível, mas menos verdadeiro ao descer 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 a 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 "Diga, não pergunte".

O exemplo 4 é mais ou menos o mesmo - ele está incorporando a lógica da interface do usuário na camada de dados. Agora, se vamos corrigir o que o usuário deseja ver no caso de nenhum endereço, precisamos corrigir o objeto na camada de dados, e se dois projetos usarem o mesmo objeto, mas precisar usar texto diferente para o endereço nulo?

Concordo que se pudermos implementar tudo "Diga, não pergunte", seria muito útil - eu ficaria feliz se pudesse apenas dizer, em vez de perguntar (e fazer sozinho) 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.

tia
fonte