Quando um método de uma classe deve retornar a mesma instância após se modificar?

9

Eu tenho uma classe que tem três métodos A(), B()e C(). Esses métodos modificam a própria instância.

Enquanto os métodos precisam retornar uma instância quando a instância é uma cópia separada (assim como Clone()), eu tenho uma escolha livre para retornar voidou a mesma instância ( return this;) ao modificar a mesma instância no método e não retornar nenhum outro valor.

Ao decidir retornar a mesma instância modificada, eu posso fazer cadeias de métodos simples obj.A().B().C();.

Essa seria a única razão para fazê-lo?

É correto modificar a própria instância e devolvê-la também? Ou deveria apenas retornar uma cópia e deixar o objeto original como antes? Porque, ao retornar a mesma instância modificada, o usuário talvez suponha que o valor retornado seja uma cópia, caso contrário, não seria retornado? Se estiver tudo bem, qual é a melhor maneira de esclarecer essas coisas sobre o método?

Martin Braun
fonte

Respostas:

7

Quando usar encadeamento

O encadeamento de funções é popular principalmente em idiomas em que um IDE com preenchimento automático é um local comum. Por exemplo, quase todos os desenvolvedores de C # usam o Visual Studio. Portanto, se você estiver desenvolvendo com C #, adicionar encadeamento a seus métodos pode economizar tempo para os usuários dessa classe, pois o Visual Studio o ajudará na criação da cadeia.

Por outro lado, linguagens como PHP, que são altamente dinâmicas por natureza e geralmente não têm suporte de preenchimento automático em IDEs, terão menos classes que suportam encadeamento. O encadeamento somente será apropriado quando phpDocs corretos forem empregados para expor os métodos encadeados.

O que é encadeamento?

Dada uma classe denominada, Fooos dois métodos a seguir são encadeados.

function what() { return this; }
function when() { return new Foo(this); }

O fato de ser uma referência à instância atual e criar uma nova instância não altera que esses sejam métodos encadeados.

Não há regra de ouro de que um método encadeado possa fazer referência apenas ao objeto atual. Os métodos encadeados e de fato podem ser divididos em duas classes diferentes. Por exemplo;

class B { function When() { return true; } };
class A { function What() { return new B(); } };

var a = new A();
var x = a.What().When();

Não há referência a thisnenhum dos exemplos acima. O código a.What().When()é um exemplo de encadeamento. O interessante é que o tipo de classe Bnunca é atribuído a uma variável.

Um método é encadeado quando seu valor de retorno é usado como o próximo componente de uma expressão.

Aqui estão mais alguns exemplos

 // return value never assigned.
 myFile.Open("something.txt").Write("stuff").Close();

// two chains used in expression
int x = a.X().Y() * b.X().Y();

// a chain that creates new strings
string name = str.Substring(1,10).Trim().ToUpperCase();

Quando usar thisenew(this)

As strings na maioria dos idiomas são imutáveis. Portanto, as chamadas de método de encadeamento sempre resultam na criação de novas strings. Onde como um objeto como StringBuilder pode ser modificado.

Consistência é a melhor prática.

Se você possui métodos que modificam o estado de um objeto e retornam this, não misture métodos que retornam novas instâncias. Em vez disso, crie um método específico chamado Clone()que fará isso explicitamente.

 var x  = a.Foo().Boo().Clone().Foo();

Isso é muito mais claro quanto ao que está acontecendo lá dentro a.

A etapa fora e truque traseiro

Eu chamo isso de truque passo a passo , porque resolve muitos problemas comuns relacionados ao encadeamento. Basicamente, significa que você sai da classe original para uma nova classe temporária e depois volta para a classe original.

A classe temporária existe apenas para fornecer recursos especiais à classe original, mas somente sob condições especiais.

Muitas vezes, uma cadeia precisa mudar de estado , mas a classe Anão pode representar todos esses estados possíveis . Portanto, durante uma cadeia, é introduzida uma nova classe que contém uma referência para A. Isso permite que o programador entre em um estado e volte para A.

Aqui está o meu exemplo, que o estado especial seja conhecido como B.

class A {
    function Foo() { return this; }
    function Boo() { return this; }
    function Change() return new B(this); }
}

class B {
    var _a;
    function (A) { _a = A; }
    function What() { return this; }
    function When() { return this; }
    function End() { return _a; }
}

var a = new A();
a.Foo().Change().What().When().End().Boo();

Agora esse é um exemplo muito simples. Se você quiser ter mais controle, Bpoderá retornar a um novo supertipo Aque possui métodos diferentes.

Reactgular
fonte
4
Realmente não entendo por que você recomenda o encadeamento de métodos, dependendo apenas do suporte ao preenchimento automático do tipo Intellisense. Cadeias de métodos ou interfaces fluentes são um padrão de design de API para qualquer linguagem OOP; a única coisa que o preenchimento automático faz é impedir erros de digitação e erros de tipo.
amon
@ amon você interpretou mal o que eu disse. Eu disse que é mais popular quando o intellisense é comum para um idioma. Eu nunca disse que dependia do recurso.
Reactgular
3
Embora essa resposta me ajude bastante a entender as possibilidades do encadeamento, ainda não tenho a decisão de usá-lo. Claro, a consistência é a melhor . No entanto, estou me referindo ao caso em que modifico meu objeto na função e return this. Como posso ajudar os usuários de minhas bibliotecas a lidar e entender essa situação? (Permitindo-lhes métodos cadeia, mesmo quando não é necessário, se isso realmente ficar bem Ou deveria eu só ficar com uma forma, exigem mudança em tudo?)
Martin Braun
@modiX se minha resposta estiver correta, aceite-a como resposta. As outras coisas que você procura são opiniões sobre como usar o encadeamento, e não há resposta certa / errada para isso. Isso realmente depende de você decidir. Talvez você esteja se preocupando demais com pequenos detalhes?
Reactgular
3

Esses métodos modificam a própria instância.

Dependendo do idioma, ter métodos que retornem void/ unite modifiquem sua instância ou parâmetros não é idiomático. Mesmo nas linguagens que costumavam fazer isso mais (C #, C ++), está saindo de moda com a mudança para uma programação de estilos mais funcionais (objetos imutáveis, funções puras). Mas vamos assumir que há uma boa razão por enquanto.

Essa seria a única razão para fazê-lo?

Para certos comportamentos (pense x++), espera-se que a operação retorne o resultado, mesmo que modifique a variável. Mas essa é a única razão para fazê-lo por conta própria.

É correto modificar a própria instância e devolvê-la também? Ou deveria apenas retornar uma cópia e deixar o objeto original como antes? Como quando retorna a mesma instância modificada, o usuário talvez admita que o valor retornado é uma cópia, caso contrário não seria retornado? Se estiver tudo bem, qual é a melhor maneira de esclarecer essas coisas sobre o método?

Depende.

Nos idiomas em que copiar / novo e retornar são comuns (C # LINQ e seqüências de caracteres), retornar a mesma referência seria confuso. Nas linguagens em que modificar e retornar é comum (algumas bibliotecas C ++), a cópia seria confusa.

Tornar a assinatura inequívoca (retornando voidou usando construções de linguagem como propriedades) seria a melhor maneira de esclarecer. Depois disso, deixar claro o nome SetFoopara mostrar que você está modificando a instância existente é bom. Mas a chave é manter os idiomas da linguagem / biblioteca com a qual você está trabalhando.

Telastyn
fonte
Obrigado pela sua resposta. Estou trabalhando principalmente com a estrutura .NET e, pela sua resposta, ela é cheia de coisas não-idiomáticas. Enquanto coisas como métodos LINQ retornam uma nova cópia do original após anexar operações, etc. coisas como o Clear()ou Add()de qualquer tipo de coleção modificam a mesma instância e retornam void. Em ambos os casos você diz que seria confuso ...
Martin Braun
@modix - O LINQ não retorna uma cópia ao anexar operações.
Telastyn 19/08/2014
Por que eu posso fazer obj = myCollection.Where(x => x.MyProperty > 0).OrderBy(x => x.MyProperty);então? Where()retorna um novo objeto de coleção. OrderBy()até retorna um objeto de coleção diferente, porque o conteúdo é ordenado.
Martin Braun
11
@modix - Eles retornam novos objetos que implementam IEnumerable, mas para ser claro, eles não são cópias, e eles não são coleções (exceto ToList, ToArray, etc.).
Telastyn
Isso mesmo, eles não são cópias, além disso, são novos objetos. Também dizendo coleção, eu estava me referindo a objetos com os quais você pode contar (objetos que contêm mais de um objeto em qualquer tipo de matriz), não a classes que herdam ICollection. Foi mal.
Martin Braun
0

(Eu assumi C ++ como sua linguagem de programação)

Para mim, esse é principalmente um aspecto de legibilidade. Se A, B, C são modificadores, especialmente se este é um objeto temporário passado como parâmetro para alguma função, por exemplo

do_something(
      CPerson('name').age(24).height(160).weight(70)
      );

comparado com

{
   CPerson person('name');
   person.set_age(24);
   person.set_height(160);
   person.set_weight(70);
   do_something(person);
}

Regenerando se está correto retornar uma referência à instância modificada, eu diria que sim e aponto, por exemplo, para os operadores de fluxo '>>' e '<<' ( http://www.cprogramming.com/tutorial/ operator_overloading.html )

AdrianI
fonte
11
Você assumiu errado, é c #. No entanto, quero que minha pergunta seja mais comum.
Martin Braun
Claro, poderia ter sido Java também, mas esse foi um ponto menor apenas para colocar meu exemplo com os operadores no contexto. A essência é que tudo se resume à legibilidade, que é influenciada pelas expectativas e antecedentes do leitor, que são influenciados pelas práticas usuais na linguagem / estruturas específicas com as quais estamos lidando - como as outras respostas também apontaram.
Adriani
0

Você também pode executar o encadeamento do método com retorno de cópia em vez de modificar o retorno.

Um bom exemplo de C # é string.Replace (a, b), que não altera a string na qual foi invocada, mas retorna uma nova string que permite que você se encaminhe alegremente.

Peter Wone
fonte
Sim eu conheço. Mas eu não queria me referir a esse caso, pois tenho que usá-lo quando não quero editar a própria instância. No entanto, obrigado por apontar isso.
Martin Braun
Por outras respostas, a consistência da semântica é importante. Como você pode ser consistente com o retorno de cópia e não pode ser consistente com o retorno de modificação (porque você não pode fazê-lo com objetos imutáveis), eu diria que a resposta para sua pergunta é não usar o retorno de cópia.
Peter Wone