¿Cuándo debe un método de una clase devolver la misma instancia después de modificarse?

9

Tengo una clase que tiene tres métodos A(), B()y C(). Esos métodos modifican la propia instancia.

Si bien los métodos tienen que devolver una instancia cuando la instancia es una copia separada (igual que Clone()), tengo la opción libre de devolver voido la misma instancia ( return this;) al modificar la misma instancia en el método y no devolver ningún otro valor.

Al decidir devolver la misma instancia modificada, puedo hacer cadenas de métodos ordenadas como obj.A().B().C();.

¿Sería esta la única razón para hacerlo?

¿Está bien modificar la propia instancia y devolverla también? ¿O solo debería devolver una copia y dejar el objeto original como antes? Porque al devolver la misma instancia modificada, el usuario podría suponer que el valor devuelto es una copia, de lo contrario, ¿no se devolvería? Si está bien, ¿cuál es la mejor manera de aclarar esas cosas en el método?

Martin Braun
fuente

Respuestas:

7

Cuando usar el encadenamiento

El encadenamiento de funciones es muy popular en los idiomas en los que un IDE con autocompletar es un lugar común. Por ejemplo, casi todos los desarrolladores de C # usan Visual Studio. Por lo tanto, si está desarrollando con C #, agregar encadenamiento a sus métodos puede ser un ahorro de tiempo para los usuarios de esa clase porque Visual Studio lo ayudará a construir la cadena.

Por otro lado, los lenguajes como PHP que son de naturaleza altamente dinámica y que a menudo no tienen soporte de autocompletar en IDEs verán menos clases que admitan el encadenamiento. El encadenamiento solo será apropiado cuando se empleen los phpDoc correctos para exponer los métodos encadenables.

¿Qué es el encadenamiento?

Dada una clase llamada Foolos siguientes dos métodos son ambos encadenables.

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

El hecho de que uno sea una referencia a la instancia actual y cree una nueva instancia no cambia que estos son métodos encadenables.

No existe una regla de oro de que un método encadenable solo debe hacer referencia al objeto actual. De hecho, los métodos encadenables pueden ser a través de dos clases diferentes. Por ejemplo;

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

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

No hay ninguna referencia a thisninguno de los ejemplos anteriores. El código a.What().When()es un ejemplo de encadenamiento. Lo interesante es que el tipo de clase Bnunca se asigna a una variable.

Un método se encadena cuando su valor de retorno se utiliza como el siguiente componente de una expresión.

Aquí hay un ejemplo más

 // 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();

Cuando usar thisynew(this)

Las cadenas en la mayoría de los idiomas son inmutables. Por lo tanto, las llamadas a métodos de encadenamiento siempre generan nuevas cadenas. Donde como un objeto como StringBuilder se puede modificar.

La consistencia es la mejor práctica.

Si tiene métodos que modifican el estado de un objeto y lo devuelven this, no mezcle métodos que devuelvan nuevas instancias. En su lugar, cree un método específico llamado Clone()que haga esto explícitamente.

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

Eso es mucho más claro en cuanto a lo que está sucediendo dentro a.

El paso afuera y el truco de vuelta

A esto lo llamo el truco de salida y retroceso , porque resuelve muchos problemas comunes relacionados con el encadenamiento. Básicamente significa que sales de la clase original a una nueva clase temporal y luego regresas a la clase original.

La clase temporal existe solo para proporcionar características especiales a la clase original, pero solo bajo condiciones especiales.

A menudo hay momentos en que una cadena necesita cambiar de estado , pero la clase Ano puede representar todos esos estados posibles . Entonces, durante una cadena, se introduce una nueva clase que contiene una referencia a A. Esto permite al programador entrar en un estado y volver a A.

Aquí está mi ejemplo, que el estado especial se conozca 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();

Ahora ese es un ejemplo muy simple. Si quisieras tener más control, entonces Bpodrías volver a un nuevo supertipo Aque tenga métodos diferentes.

Reactgular
fuente
44
Realmente no entiendo por qué recomienda el encadenamiento de métodos dependiendo únicamente del soporte de autocompletado similar a Intellisense. Las cadenas de métodos o las interfaces fluidas son un patrón de diseño API para cualquier lenguaje OOP; lo único que hace el autocompletado es evitar errores tipográficos y de escritura.
amon
@amon, leíste mal lo que dije. Dije que es más popular cuando el intellisense es común para un idioma. Nunca dije que dependiera de la característica.
Reactgular
3
Si bien esta respuesta me ayuda mucho a comprender las posibilidades del encadenamiento, todavía me falta la decisión de cuándo usar el encadenamiento. Claro, la consistencia es lo mejor . Sin embargo, me estoy refiriendo al caso cuando modifico mi objeto en la función y return this. ¿Cómo puedo ayudar a los usuarios de mis bibliotecas a manejar y comprender esta situación? (Permitiéndoles encadenar métodos, incluso cuando no es necesario, ¿esto realmente estaría bien? ¿O debería seguir solo de una manera, exigir un cambio?)
Martin Braun
@modiX si mi respuesta es correcta, entonces acéptela como la respuesta. Las otras cosas que está buscando son opiniones sobre cómo usar el encadenamiento, y no hay una respuesta correcta / incorrecta para eso. Eso realmente depende de ti decidir. ¿Quizás te preocupas demasiado por los pequeños detalles?
Reactgular
3

Esos métodos modifican la propia instancia.

Dependiendo del idioma, tener métodos que devuelvan void/ unity modifiquen su instancia o parámetros no es idiomático. Incluso en lenguajes que solían hacer eso más (C #, C ++), está pasando de moda con el cambio hacia una programación de estilo más funcional (objetos inmutables, funciones puras). Pero supongamos que hay una buena razón por ahora.

¿Sería esta la única razón para hacerlo?

Para ciertos comportamientos (pensar x++) se espera que la operación devuelva el resultado, aunque modifique la variable. Pero esa es en gran medida la única razón para hacerlo por su cuenta.

¿Está bien modificar la propia instancia y devolverla también? ¿O solo debería devolver una copia y dejar el objeto original como antes? Porque cuando devuelve la misma instancia modificada, el usuario quizás admitiría que el valor devuelto es una copia, de lo contrario, ¿no se devolvería? Si está bien, ¿cuál es la mejor manera de aclarar esas cosas en el método?

Depende.

En los lenguajes donde es común copiar / nuevo y devolver (C # LINQ y cadenas), devolver la misma referencia sería confuso. En los lenguajes donde modificar y devolver es común (algunas bibliotecas de C ++), copiar sería confuso.

Hacer que la firma sea inequívoca (devolviendo voido usando construcciones de lenguaje como propiedades) sería la mejor manera de aclarar. Después de eso, dejar en claro el nombre SetFoopara mostrar que está modificando la instancia existente es bueno. Pero la clave es mantener las expresiones idiomáticas del idioma / biblioteca con la que está trabajando.

Telastyn
fuente
Gracias por tu respuesta. Principalmente estoy trabajando con .NET Framework, y por su respuesta está lleno de cosas no idiomáticas. Mientras que cosas como los métodos LINQ devuelven una nueva copia del original después de agregar operaciones, etc., cosas como Clear()o Add()de cualquier tipo de colección modificarán la misma instancia y devolverán void. En ambos casos, dices que sería confuso ...
Martin Braun
@modix: LINQ no devuelve una copia al agregar operaciones.
Telastyn
¿Por qué puedo hacer obj = myCollection.Where(x => x.MyProperty > 0).OrderBy(x => x.MyProperty);entonces? Where()devuelve un nuevo objeto de colección. OrderBy()incluso devuelve un objeto de colección diferente, porque el contenido está ordenado.
Martin Braun
1
@modix - Vuelven los nuevos objetos que implementan IEnumerable, pero para ser claro, no son copias, y no son colecciones (a excepción de ToList, ToArray, etc.).
Telastyn
Esto es correcto, no son copias, además son nuevos objetos, de hecho. Además, al decir colección, estaba haciendo referencia a objetos con los que puede contar (objetos que contienen más de un objeto en cualquier tipo de matriz), no a clases que heredan ICollection. Culpa mía.
Martin Braun
0

(Asumí C ++ como su lenguaje de programación)

Para mí, este es principalmente un aspecto de legibilidad. Si A, B, C son modificadores, especialmente si este es un objeto temporal pasado como parámetro a alguna función, por ejemplo

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

comparado con

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

Revisando si está bien devolver una referencia a la instancia modificada, diría que sí, y le diré, por ejemplo, a los operadores de flujo '>>' y '<<' ( http://www.cprogramming.com/tutorial/ operator_overloading.html )

AdrianI
fuente
1
Asumiste mal, es C #. Sin embargo, quiero que mi pregunta sea más común.
Martin Braun
Claro, podría haber sido Java también, pero ese fue un punto menor solo para ubicar mi ejemplo con los operadores en contexto. La esencia es que se reduce a la legibilidad, que está influenciada por las expectativas y los antecedentes del lector, que a su vez están influenciados por las prácticas habituales en el lenguaje / marcos particulares con los que uno está tratando, como también lo han señalado las otras respuestas.
AdrianI
0

También podría hacer el método de encadenamiento con copia de devolución en lugar de modificar la devolución.

Un buen ejemplo de C # es string.Replace (a, b) que no cambia la cadena en la que se invocó, sino que devuelve una nueva cadena que le permite encadenar alegremente.

Peter Wone
fuente
Sí, lo sé. Pero no quería referirme a este caso, ya que tengo que usar este caso cuando no quiero editar la propia instancia, de todos modos. Sin embargo, gracias por señalar eso.
Martin Braun
Según otras respuestas, la consistencia de la semántica es importante. Dado que puede ser coherente con la devolución de copia y no puede ser coherente con la devolución de modificación (porque no puede hacerlo con objetos inmutables), entonces diría que la respuesta a su pregunta es no usar devolución de copia.
Peter Wone