¿Hay algún punto en el uso de constructores e interfaces fluidas con inicializadores de objetos?

10

En Java y C #, puede crear un objeto con propiedades que se pueden establecer en la inicialización definiendo un constructor con parámetros, definiendo cada propiedad después de construir el objeto o utilizando el patrón de interfaz de generador / fluido. Sin embargo, C # 3 introdujo inicializadores de objetos y colecciones, lo que significaba que el patrón de construcción era en gran medida inútil. En un lenguaje sin inicializadores, uno podría implementar un constructor y luego usarlo así:

Vehicle v = new Vehicle.Builder()
                    .manufacturer("Toyota")
                    .model("Camry")
                    .year(1997)
                    .colour(CarColours.Red)
                    .addSpecialFeature(new Feature.CDPlayer())
                    .addSpecialFeature(new Feature.SeatWarmer(4))
                    .build();

Por el contrario, en C # uno podría escribir:

var vehicle = new Vehicle {
                Manufacturer = "Toyota",
                Model = "Camry",
                Year = 1997,
                Colour = CarColours.Red,
                SpecialFeatures = new List<SpecialFeature> {
                    new Feature.CDPlayer(),
                    new Feature.SeatWarmer { Seats = 4 }
                }
              }

... eliminando la necesidad de un generador como se ve en el ejemplo anterior.

Según estos ejemplos, ¿los constructores siguen siendo útiles en C #, o han sido reemplazados completamente por los inicializadores?

svbnet
fuente
are builders useful¿Por qué sigo viendo este tipo de preguntas? Un Builder es un patrón de diseño; claro, puede estar expuesto como una característica del lenguaje, pero al final del día es solo un patrón de diseño. Un patrón de diseño no puede ser "incorrecto". Puede o no cumplir con su caso de uso, pero eso no hace que todo el patrón sea incorrecto, solo la aplicación del mismo. Recuerde que al final del día los patrones de diseño resuelven problemas particulares: si no enfrenta el problema, ¿por qué trataría de resolverlo?
VLAZ
@vlaz No estoy diciendo que los constructores estén equivocados; de hecho, mi pregunta era si había algún caso de uso para los constructores, dado que los inicializadores son una implementación del caso más común en el que se usarían los constructores. Obviamente, la gente ha respondido que un constructor sigue siendo útil para configurar campos privados, lo que a su vez ha respondido a mi pregunta.
svbnet
3
@vlaz Para aclarar: estoy diciendo que los inicializadores han reemplazado en gran medida el patrón de interfaz fluido / constructor "tradicional" de escribir una clase de generador interno, y luego escribir métodos de establecimiento de encadenamiento dentro de esa clase que crean una nueva instancia de esa clase padre. Estoy no diciendo que inicializadores son un reemplazo para ese específico constructor de patrón; Estoy diciendo que los inicializadores evitan que los desarrolladores tengan que implementar ese patrón de generador específico, salvo por los casos de uso mencionados en las respuestas, lo que no hace que el patrón de generador sea inútil.
svbnet
55
@vlaz No entiendo tu preocupación. "Decías que un patrón de diseño no es útil" - no, Joe preguntaba si un patrón de diseño es útil. ¿Cómo es esa una pregunta mala, incorrecta o equivocada? ¿Crees que la respuesta es obvia? No creo que la respuesta sea obvia; Esto me parece una buena pregunta.
Tanner Swett
2
@vlaz, los patrones singleton y localizador de servicios están equivocados. Los patrones de diseño Ergo pueden estar equivocados.
David Arno

Respuestas:

12

Como tocó @ user248215, el verdadero problema es la inmutabilidad. La razón por la que usaría un constructor en C # sería para mantener la especificidad de un inicializador sin tener que exponer propiedades configurables. No es una cuestión de encapsulación, por eso he escrito mi propia respuesta. La encapsulación es bastante ortogonal, ya que invocar a un setter no implica lo que el setter realmente hace o lo ata a su implementación.

Es probable que la próxima versión de C #, 8.0, introduzca una withpalabra clave que permita que los objetos inmutables se inicialicen de manera clara y concisa sin necesidad de escribir constructores.

Otra cosa interesante que puede hacer con los constructores, a diferencia de los inicializadores, es que pueden dar lugar a diferentes tipos de objetos dependiendo de la secuencia de los métodos llamados.

Por ejemplo

value.Match()
    .Case((DateTime d) => Console.WriteLine($"{d: yyyy-mm-dd}"))
    .Case((double d) => Console.WriteLine(Math.Round(d, 4));
    // void

var str = value.Match()
    .Case((DateTime d) => $"{d: yyyy-mm-dd}")
    .Case((double d) => Math.Round(d, 4).ToString())
    .ResultOrDefault(string.Empty);
    // string

Solo para aclarar el ejemplo anterior, es una biblioteca de coincidencia de patrones que utiliza el patrón de construcción para crear una "coincidencia" especificando casos. Los casos se agregan llamando al Casemétodo que le pasa una función. Si valuees asignable al tipo de parámetro de la función, se invoca. Puede encontrar el código fuente completo en GitHub y, dado que los comentarios XML son difíciles de leer en texto sin formato, aquí hay un enlace a la documentación integrada de SandCastle (consulte la sección Comentarios )

Aluan Haddad
fuente
No veo cómo esto no se pudo hacer un inicializador de objetos, usando un IEnumerable<Func<T, TResult>>miembro.
Caleth
@Caleth Eso fue realmente algo con lo que experimenté, pero hay algunos problemas con ese enfoque. No permite cláusulas condicionales (no mostradas pero utilizadas y demostradas en la documentación vinculada) y no permite la inferencia de tipos de TResult. Finalmente, la inferencia de tipos tuvo mucho que ver con eso. También quería que "pareciera" una estructura de control. Además, no quería usar un inicializador ya que eso permitiría la mutación.
Aluan Haddad
12

Los inicializadores de objetos requieren que el código de llamada tenga acceso a las propiedades. Los constructores anidados pueden acceder a miembros privados de la clase.

Si desea hacer Vehicleinmutable (haciendo que todos los setters sean privados), entonces el generador anidado podría usarse para establecer las variables privadas.

usuario248215
fuente
0

¡Todos tienen diferentes propósitos!

Los constructores pueden inicializar campos que están marcados readonly, así como miembros privados y protegidos. Sin embargo, está algo limitado en lo que puede hacer dentro de un constructor; debe evitar pasar thisa métodos externos, por ejemplo, y debe evitar llamar a miembros virtuales, ya que pueden ejecutarse en el contexto de una clase derivada que aún no se ha construido. Además, los constructores están garantizados para ejecutarse (a menos que la persona que llama esté haciendo algo muy inusual), por lo que si establece campos en el constructor, el código en el resto de la clase puede suponer que esos campos no serán nulos.

Los inicializadores se ejecutan después de la construcción. Solo te permiten llamar propiedades públicas; no se pueden establecer campos privados y / o de solo lectura. Por convención, el acto de establecer una propiedad debe ser bastante limitado, por ejemplo, debe ser idempotente y tener efectos secundarios limitados.

Los métodos de creación son métodos reales y, por lo tanto, permiten más de un argumento y, por convención, pueden tener efectos secundarios, incluida la creación de objetos. Si bien no pueden establecer campos de solo lectura, prácticamente pueden hacer cualquier otra cosa. Además, los métodos se pueden implementar como métodos de extensión (como casi todas las funciones de LINQ).

John Wu
fuente