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 User
formatar 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.
fonte
Respostas:
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.
http://pragprog.com/articles/tell-dont-ask
fonte
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
temperature
consulta, amanhã, o cliente poderá,check_for_underheating
sem precisar alterarSystemMonitor
. Este não é o caso quando o próprioSystemMonitor
implementacheck_for_overheating
. Portanto, umaSystemMonitor
classe cujo trabalho é acionar um alarme quando a temperatura está muito alta segue isso - mas umaSystemMonitor
classe 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.
fonte
check_for_underheating
sem precisar alterarSystemMonitor
". Como o cliente é diferenteSystemMonitor
daquele 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.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?
Ou você tem cada tipo de sistema que define sua capacidade de aquecimento?
EDITAR:
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.
fonte
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.
fonte
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 ".
fonte
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, usefoo->doSomething();
uma chamada de wrapper em torno da funcionalidade da barra e implementado da maneira mais simplesreturn bar->doSomething();
- sefoo
for responsável pelo gerenciamentobar
, deixe-o fazer isso!fonte
Além das outras boas respostas sobre "diga, não pergunte", alguns comentários sobre seus exemplos específicos que podem ajudar:
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 é 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.
fonte
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:
Isso significa que é melhor você ter algo parecido com isto:
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.
fonte
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.
fonte