Como escolher entre Tell Don't Ask e Command Query Separation?

25

O princípio Tell Don't Ask diz:

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.

Um exemplo simples de "Diga, não pergunte" é

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

e a versão Tell é ...

Widget w = ...;
w.removeFromParent();

Mas e se eu precisar saber o resultado do método removeFromParent? Minha primeira reação foi apenas alterar o removeFromParent para retornar um valor booleano se o pai foi removido ou não.

Mas então me deparei com Command Query Separation Pattern que diz NÃO para fazer isso.

Ele afirma que todo método deve ser um comando que executa uma ação ou uma consulta que retorna dados ao chamador, mas não os dois. Em outras palavras, fazer uma pergunta não deve alterar a resposta. Mais formalmente, os métodos devem retornar um valor somente se forem referencialmente transparentes e, portanto, não apresentarem efeitos colaterais.

Esses dois estão realmente em conflito um com o outro e como faço para escolher entre os dois? Eu vou com o programador pragmático ou Bertrand Meyer sobre isso?

Dakotah North
fonte
1
o que você faria com o booleano?
David
1
Parece que você está mergulhando muito longe em padrões de codificação sem equilibrar usabilidade
Izkata
Re booleano ... este é um exemplo fácil de fazer backup, mas semelhante à operação de gravação abaixo, o objetivo é que seja um status da operação.
Dakotah North
2
O que quero dizer é que você não deve se concentrar em "devolver algo", mas mais no que deseja fazer com ele. Como eu disse em outro comentário, se você se concentrar na falha, use a exceção, se quiser fazer alguma coisa quando a operação estiver concluída, use retorno de chamada ou eventos, se desejar registrar o que aconteceu, é responsabilidade do Widget ... Isso é responsabilidade por que estou pedindo suas necessidades. Se você não conseguir encontrar um exemplo concreto em que precise devolver algo, talvez isso signifique que você não precisará escolher entre os dois.
David
1
A Separação de Consulta de Comando é um princípio, não um padrão. Padrões são algo que você pode usar para resolver um problema. Os princípios são os que você segue para não criar problemas.
Candied_orange #

Respostas:

25

Na verdade, seu exemplo de problema já ilustra a falta de decomposição.

Vamos mudar um pouco:

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

Isso não é realmente diferente, mas torna a falha mais aparente: por que o livro saberia sobre sua prateleira? Simplificando, não deveria. Introduz uma dependência de livros nas prateleiras (o que não faz sentido) e cria referências circulares. Está tudo ruim.

Da mesma forma, não é necessário que um widget conheça seu pai. Você dirá: "Ok, mas o widget precisa de seu pai para se organizar adequadamente etc." e, sob o capô, o widget conhece seu pai e solicita que suas métricas calculem suas próprias métricas com base nelas etc. Segundo dizer, não pergunte se isso está errado. O pai deve dizer a todos os seus filhos para renderizar, passando todas as informações necessárias como argumentos. Dessa forma, pode-se facilmente ter o mesmo widget em dois pais (mesmo que isso faça sentido ou não).

Para voltar ao exemplo do livro - digite o bibliotecário:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

Por favor, entenda que não estamos pedindo mais no sentido de dizer, não pergunte .

Na primeira versão, a prateleira era uma propriedade do livro e você solicitou. Na segunda versão, não fazemos nenhuma suposição sobre o livro. Tudo o que sabemos é que o bibliotecário pode nos dizer uma prateleira que contém o livro. Presumivelmente, ele confia em algum tipo de tabela de pesquisa para fazer isso, mas não sabemos ao certo (ele também pode simplesmente percorrer todos os livros em todas as prateleiras) e, na verdade , não queremos saber .
Não confiamos em se ou como a resposta dos bibliotecários está acoplada ao seu estado ou a de outros objetos dos quais possa depender. Dizemos ao bibliotecário para encontrar a prateleira.
No primeiro cenário, codificamos diretamente o relacionamento entre um livro e uma prateleira como parte do estado do livro e o recuperamos diretamente. Isso é muito frágil, porque também assumimos que a estante devolvida pelo livro contém o livro (essa é uma restrição que devemos garantir, caso contrário, talvez não removeconsigamos tirá-lo da estante).

Com a introdução do bibliotecário, modelamos esse relacionamento separadamente, conseguindo assim uma separação de preocupações.

back2dos
fonte
Eu acho que esse é um ponto muito válido, mas não responde à pergunta. Na sua versão, a pergunta é: o método remove (Book b) na classe Shelf deve ter um valor de retorno?
scarfridge
1
@scarfridge: No final das contas, diga, não pergunte é um corolário da separação adequada das preocupações. Se você pensar bem, eu respondo à pergunta. Pelo menos eu acho que sim;)
back2dos
1
@scarfridge Em vez de um valor de retorno, pode-se passar uma função / delegado que é chamada em falha. Se for bem sucedido, você está pronto, certo?
Bless Yahu
2
@BlessYahu Isso parece demais para mim. Pessoalmente, sinto que a separação entre comandos e consultas é um ideal no mundo real (multiencadeado). IMHO, não há problema em um método com efeitos colaterais retornar um valor, desde que o nome do método indique claramente, que ele alterará o estado do objeto. Considere o método pop () de uma pilha. Mas um método com um nome de consulta não deve ter efeitos colaterais.
21712 scarfridge
13

Se você precisa saber o resultado, então você sabe; essa é sua exigência.

A frase "métodos deve retornar um valor somente se forem referencialmente transparentes e, portanto, não apresentarem efeitos colaterais" é uma boa diretriz a seguir (especialmente se você estiver escrevendo seu programa em um estilo funcional , por simultaneidade ou por outros motivos), mas é não é absoluto.

Por exemplo, pode ser necessário saber o resultado de uma operação de gravação de arquivo (verdadeira ou falsa). Esse é um exemplo de método que retorna um valor, mas sempre produz efeitos colaterais; não há maneira de contornar isso.

Para cumprir a Separação de Comando / Consulta, você teria que executar a operação de arquivo com um método e, em seguida, verificar seu resultado com outro método, uma técnica indesejável, porque desacopla o resultado do método que causou o resultado. E se algo acontecer com o estado do seu objeto entre a chamada do arquivo e a verificação do status?

O ponto principal é o seguinte: se você estiver usando o resultado de uma chamada de método para tomar decisões em outras partes do programa, não estará violando o Tell Don't Ask. Se, por outro lado, você estiver tomando decisões para um objeto com base em uma chamada de método para esse objeto, mova essas decisões para o próprio objeto para preservar o encapsulamento.

Isso não é de todo contraditório com a Separação de Consulta de Comando; de fato, reforça ainda mais, porque não é mais necessário expor um método externo para fins de status.

Robert Harvey
fonte
1
Alguém poderia argumentar que, no seu exemplo, se a operação "write" falhar, uma exceção deve ser lançada, portanto não há necessidade de um método "get status".
David
@ David: Depois, volte ao exemplo do OP, que retornaria verdadeiro se uma alteração realmente ocorresse, falso se não ocorresse. Duvido que você queira lançar uma exceção lá.
9788 Robert
Mesmo o exemplo do OP não parece certo para mim. Você quer ser notado se a remoção não for possível; nesse caso, você usaria uma exceção ou quando um widget é realmente removido; nesse caso, você poderia usar o mecanismo de evento ou de retorno de chamada. Eu simplesmente não consigo ver um exemplo real onde você não gostaria de respeitar "Pattern Separação Comando Query"
David
@ David: Eu editei minha resposta para esclarecer.
Robert Harvey
Para não deixar bem claro, mas toda a idéia por trás da separação entre comando e consulta é que quem emite o comando para gravar o arquivo não se importa com o resultado - pelo menos não em um sentido específico (um usuário pode mais tarde puxe uma lista de todas as operações recentes de arquivo, mas que não tem correlação com o comando inicial). Se você precisar executar ações após a conclusão de um comando, independentemente de se importar ou não com o resultado, a maneira correta de fazer isso é com um modelo de programação assíncrono usando eventos que (podem) conter detalhes sobre o comando original e / ou seus resultado.
Aaronaught 8/11/11
2

Eu acho que você deveria ir com seu instinto inicial. Às vezes, o design deve ser intencionalmente perigoso, para introduzir complexidade imediatamente quando você se comunica com o objeto, para que seja forçado a manipulá-lo corretamente imediatamente, em vez de tentar manipulá-lo no próprio objeto, ocultar todos os detalhes sangrentos e dificultar a limpeza. alterar as premissas subjacentes.

Você deve ter uma sensação de afundamento se um objeto oferecer equivalentes opene closecomportamentos implementados e começar imediatamente a entender com o que está lidando quando vir um valor de retorno booleano para algo que pensou ser uma tarefa atômica simples.

Como você lida mais tarde é o seu problema. Você pode criar uma abstração acima dela, com um tipo de widget removeFromParent(), mas sempre deve ter um retorno de baixo nível, caso contrário você provavelmente estava fazendo suposições prematuras.

Não tente simplificar tudo. Não há nada tão decepcionante quando você confia em algo que parece elegante e inocente, apenas para perceber o verdadeiro horror mais tarde, no pior dos momentos.

Filip Dupanović
fonte
2

O que você descreve é ​​uma "exceção" conhecida ao princípio de Separação de Consulta de Comando.

Neste artigo, Martin Fowler explica como ele ameaçaria essa exceção.

Meyer gosta de usar a separação de comando e consulta absolutamente, mas há exceções. O popping de uma pilha é um bom exemplo de uma consulta que modifica o estado. Meyer diz corretamente que você pode evitar esse método, mas é um idioma útil. Então, eu prefiro seguir esse princípio quando posso, mas estou preparado para quebrá-lo para conseguir meu pop.

No seu exemplo, eu consideraria a mesma exceção.

elviejo79
fonte
0

A separação entre comando e consulta é incrivelmente fácil de entender mal.

Se eu disser no meu sistema, existe um comando que também retorna um valor de uma consulta e você diz "Ha! Você está violando!" você está pulando a arma.

Não, não é isso que é proibido.

O que é proibido é quando esse comando é a ÚNICA maneira de fazer essa consulta. Não. Eu não deveria ter que mudar de estado para fazer perguntas. Isso não significa que tenho que fechar os olhos toda vez que mudo de estado.

Não importa se sim, já que vou abri-las novamente de qualquer maneira. O único benefício para essa reação exagerada foi garantir que os designers não fiquem preguiçosos e se esqueça de incluir a consulta que não muda de estado.

O significado real é tão sutil que é mais fácil para as pessoas preguiçosas dizerem que os levantadores devem retornar nulos. Isso facilita ter certeza de que você não está violando a regra real, mas é uma reação exagerada que pode se tornar uma dor real.

Interfaces fluentes e iDSLs "violam" essa reação exagerada o tempo todo. Há muita energia sendo ignorada se você reagir demais.

Você pode argumentar que um comando deve fazer apenas uma coisa e uma consulta deve fazer apenas uma coisa. E, em muitos casos, esse é um bom argumento. Quem pode dizer que existe apenas uma consulta que deve seguir um comando? Tudo bem, mas isso não é separação de comando-consulta. Esse é o princípio da responsabilidade única.

Olhado desta maneira e um pop de pilha não é uma exceção bizarra. É uma responsabilidade única. O pop só viola a separação da consulta de comando se você esquecer de fornecer uma espiada.

candied_orange
fonte