Estou lendo sobre cheiros de códigos comuns no livro de Refatoração de Martin Fowler . Nesse contexto, eu estava pensando em um padrão que estou vendo em uma base de código, e se alguém poderia objetivamente considerá-lo um anti-padrão.
O padrão é aquele em que um objeto é passado como argumento para um ou mais métodos, todos os quais alteram o estado do objeto, mas nenhum deles retorna o objeto. Portanto, ele conta com o passe por natureza de referência (neste caso) C # / .NET.
var something = new Thing();
// ...
Foo(something);
int result = Bar(something, 42);
Baz(something);
Descobri que (especialmente quando os métodos não são nomeados adequadamente), preciso investigar esses métodos para entender se o estado do objeto foi alterado. Isso torna a compreensão do código mais complexa, pois preciso rastrear vários níveis da pilha de chamadas.
Eu gostaria de propor a melhoria desse código para retornar outro objeto (clonado) com o novo estado ou qualquer coisa necessária para alterar o objeto no site de chamada.
var something1 = new Thing();
// ...
// Let's return a new instance of Thing
var something2 = Foo(something1);
// Let's use out param to 'return' other info about the operation
int result;
var something3 = Bar(something2, out result);
// If necessary, let's capture and make explicit complex changes
var changes = Baz(something3)
something3.Apply(changes);
Para mim, parece que o primeiro padrão é escolhido nas suposições
- que é menos trabalhoso ou requer menos linhas de código
- que nos permite mudar o objeto e retornar alguma outra informação
- que é mais eficiente, pois temos menos instâncias de
Thing
.
Ilustro uma alternativa, mas para propor, é preciso ter argumentos contra a solução original. Quais argumentos, se houver, podem ser apresentados para afirmar que a solução original é um antipadrão?
E o que há de errado com minha solução alternativa?
fonte
Respostas:
Sim, a solução original é um antipadrão pelos motivos que você descreve: dificulta o raciocínio sobre o que está acontecendo, o objeto não é responsável por seu próprio estado / implementação (quebra do encapsulamento). Eu também acrescentaria que todas essas mudanças de estado são contratos implícitos do método, tornando esse método frágil diante das mudanças nos requisitos.
Dito isto, sua solução tem algumas desvantagens, a mais óbvia delas é que a clonagem de objetos não é boa. Pode ser lento para objetos grandes. Isso pode levar a erros onde outras partes do código se apegam às referências antigas (o que provavelmente ocorre na base de código que você descreve). Tornar esses objetos explicitamente imutáveis resolve pelo menos alguns desses problemas, mas é uma mudança mais drástica.
A menos que os objetos sejam pequenos e de certa forma transitórios (o que os torna bons candidatos à imutabilidade), eu estaria inclinado a simplesmente mover mais da transição de estado para os próprios objetos. Isso permite ocultar os detalhes de implementação dessas transições e definir requisitos mais fortes sobre quem / o quê / onde essas transições de estado ocorrem.
fonte
Bar(something)
(e modificar o estado desomething
), façaBar
um membro dosomething
tipo.something.Bar(42)
é mais provável que sofra mutaçãosomething
, além de permitir que você use ferramentas OO (estado privado, interfaces, etc.) para protegersomething
o estadoNa verdade, esse é o verdadeiro cheiro do código. Se você tiver um objeto mutável, ele fornece métodos para alterar seu estado. Se você tem uma chamada para esse método incorporado em uma tarefa de mais algumas instruções, não há problema em refatorar essa tarefa para um método próprio - o que o deixa exatamente na situação descrita. Mas se você não tem nomes de métodos como
Foo
eBar
, mas métodos que deixam claro que eles alteram o objeto, não vejo problema aqui. Imagineou
ou
ou
ou algo assim - não vejo aqui nenhuma razão para retornar um objeto clonado para esses métodos, e também não há razão para analisar sua implementação para entender que eles mudarão o estado do objeto passado.
Se você não quiser efeitos colaterais, torne seus objetos imutáveis, ele aplicará métodos como os anteriores para retornar um objeto alterado (clonado) sem alterar o original.
fonte
Sim, consulte http://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/ para obter um dos muitos exemplos de pessoas apontando que efeitos colaterais inesperados são ruins.
Em geral, o princípio fundamental é que o software é construído em camadas, e cada camada deve apresentar a abstração mais limpa possível para a próxima. E uma abstração limpa é aquela em que você deve ter o mínimo possível em mente para usá-la. Isso se chama modularidade e se aplica a tudo, desde funções únicas a protocolos em rede.
fonte
ForEach<T>
faz.Antes de tudo, isso não depende da "natureza de referência por", depende dos objetos serem tipos de referência mutáveis. Em linguagens não funcionais, esse quase sempre será o caso.
Em segundo lugar, se isso é um problema ou não, depende tanto do objeto quanto da rigidez das alterações nos diferentes procedimentos - se você não conseguir fazer uma alteração no Foo e causar uma falha na barra, será um problema. Não é necessariamente um cheiro de código, mas é um problema com Foo ou Bar ou Something (provavelmente Bar como deveria estar verificando sua entrada, mas poderia ser Algo sendo colocado em um estado inválido que deveria estar impedindo).
Eu não diria que sobe para o nível de um antipadrão, mas algo para estar ciente.
fonte
Eu diria que há pouca diferença entre
A.Do(Something)
modificarsomething
esomething.Do()
modificarsomething
. Em qualquer um dos casos, deve ficar claro o nome do método invocado quesomething
será modificado. Se não estiver claro o nome do método, independentemente desomething
ser um parâmetrothis
ou parte do ambiente, ele não deverá ser modificado.fonte
Eu acho que é bom mudar o estado do objeto em alguns cenários. Por exemplo, tenho uma lista de usuários e desejo aplicar filtros diferentes à lista antes de devolvê-la ao cliente.
E sim, você pode tornar isso bonito movendo a filtragem para outro método, para acabar com algo nas linhas de:
Onde
Filter(users)
seria executado acima dos filtros.Não me lembro exatamente onde me deparei com isso antes, mas acho que isso foi chamado de pipeline de filtragem.
fonte
Não tenho certeza se a nova solução proposta (de copiar objetos) é um padrão. O problema, como você apontou, é a má nomenclatura de funções.
Digamos que eu escreva uma operação matemática complexa como uma função f () . I documentar que f () é uma função que mapeia
NXN
paraN
, eo algoritmo por trás dele. Se a função for nomeada inadequadamente, e não documentada, e não tiver casos de teste acompanhantes, você precisará entender o código; nesse caso, o código será inútil.Sobre sua solução, algumas observações:
X
tornou-seY
depoisf()
, masX
na verdade éY
) e possivelmente inconsistência temporal.O problema que você está tentando resolver é válido; no entanto, mesmo com a enorme superengenharia, o problema é contornado, não resolvido.
fonte