Construir objetos grandes e inmutables sin usar constructores que tengan listas de parámetros largas


Tengo algunos objetos grandes (más de 3 campos) que pueden y deben ser inmutables. Cada vez que me encuentro con ese caso tiendo a crear abominaciones constructoras con largas listas de parámetros. No se siente bien, es difícil de usar y la legibilidad sufre.

Es aún peor si los campos son algún tipo de tipo de colección como listas. Un simple addSibling(S s) facilitaría mucho la creación del objeto, pero hace que el objeto sea mutable.

¿Qué usan ustedes en tales casos? Estoy en Scala y Java, pero creo que el problema es agnóstico del lenguaje siempre y cuando el lenguaje esté orientado a objetos.

Soluciones que se me ocurren:

  1. "Abominaciones constructoras con largas listas de parámetros"
  2. El Patrón del Constructor

Gracias por tu aporte!

Author: Robert Harvey, 2010-05-17

9 answers

Bueno, ¿quieres un objeto más fácil de leer e inmutable una vez creado?

Creo que una interfaz fluida CORRECTAMENTE HECHA te ayudaría.

Se vería así (ejemplo puramente inventado):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

Escribí "CORRECTLY DONE" en negrita porque la mayoría de los programadores Java obtienen interfaces fluent incorrectas y contaminan su objeto con el método necesario para construir el objeto, lo que por supuesto es completamente incorrecto.

El truco es que solo el el método build() realmente crea un Foo (por lo tanto, Foo puede ser inmutable).

FooFactory.crear(), whereXXX(..) and withXXX(..) todos crean "algo más".

Que algo más puede ser una FooFactory, aquí hay una manera de hacerlo....

Tu FooFactory se vería así:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}
 75
Author: SyntaxT3rr0r,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-25 13:54:22

En Scala 2.8, puede usar parámetros nominados y predeterminados, así como el método copy en una clase case. He aquí un ejemplo de código:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))
 60
Author: Martin Odersky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 14:33:18

Bueno, considere esto en Scala 2.8:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

Esto tiene su parte de problemas, por supuesto. Por ejemplo, trate de hacer espouse y Option[Person], y luego conseguir dos personas casadas entre sí. No puedo pensar en una manera de resolver eso sin recurrir a un constructor private var y/o private más una fábrica.

 20
Author: Daniel C. Sobral,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 15:42:58

Aquí hay un par de opciones más:

Opción 1

Hacer que la implementación en sí sea mutable, pero separar las interfaces que expone a mutable e inmutable. Esto está tomado del diseño de la biblioteca Swing.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

Opción 2

Si su aplicación contiene un conjunto grande pero predefinido de objetos inmutables (por ejemplo, objetos de configuración), puede considerar usar el framework Spring.

 11
Author: Little Bobby Tables,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 13:47:45

Ayuda recordar que hay diferentes tipos de inmutabilidad. Para su caso, creo que la inmutabilidad de "paleta" funcionará realmente bien:

Inmutabilidad de paleta: es lo que caprichosamente llamar a un ligero debilitamiento de escritura-una vez inmutabilidad. Uno podría imagine un objeto o un campo que permaneció mutable por un tiempo durante su inicialización, y luego se "congeló" para siempre. Este tipo de la inmutabilidad es particularmente útil para inmutable objetos que circulan referencia entre sí, o inmutable objetos que han sido serializados a disco y sobre la necesidad de deserialización ser "fluido" hasta que todo el deserialization process is done, at ¿qué punto todos los objetos pueden ser congelar.

Así que inicializa su objeto, luego establece una bandera de "congelación" de algún tipo que indica que ya no se puede escribir. Preferiblemente, escondería la mutación detrás de una función para que la función siga siendo pura para los clientes que consumen su API.

 6
Author: Juliet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 22:52:04

También puede hacer que los objetos inmutables expongan métodos que parezcan mutadores (como addSibling) pero que devuelvan una nueva instancia. Eso es lo que hacen las inmutables colecciones Scala.

El inconveniente es que puede crear más instancias de las necesarias. También es aplicable solo cuando existen configuraciones válidas intermedias (como algunos nodos sin hermanos, lo que está bien en la mayoría de los casos) a menos que no desee tratar con objetos parcialmente construidos.

Por ejemplo un gráfico el borde que aún no tiene destino no es un borde de gráfico válido.

 5
Author: ziggystar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 12:28:14

Considere cuatro posibilidades:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

Para mí, cada uno de 2, 3 y 4 se adapta a una situación diferente. El primero es difícil de amar, por las razones citadas por el OP, y generalmente es un síntoma de un diseño que ha sufrido algo de fluencia y necesita un poco de refactorización.

Lo que estoy listando como (2) es bueno cuando no hay estado detrás de la 'fábrica', mientras que (3) es el diseño de elección cuando hay estado. Me encuentro usando (2) en lugar de (3) cuando no quiero preocuparme por hilos y sincronización, y no tengo que preocuparme por amortizar alguna configuración costosa sobre la producción de muchos objetos. (3), por otro lado, se llama cuando se trabaja realmente en la construcción de la fábrica (configuración desde un SPI, lectura de archivos de configuración, etc.).

Finalmente, la respuesta de otra persona mencionó la opción (4), donde tienes muchos objetos inmutables y el patrón preferible es obtener los nuevos de los antiguos.

Tenga en cuenta que no soy miembro del' club de fans del patrón ' sure claro, algunas cosas valen la pena emular, pero me parece que toman una vida inútil de su propia una vez que la gente les da nombres y sombreros divertidos.

 5
Author: bmargulies,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 13:34:27

Otra opción potencial es refactorizar para tener menos campos configurables. Si los grupos de campos solo trabajan (principalmente) entre sí, reúnalos en su propio pequeño objeto inmutable. Los constructores/constructores de ese objeto" pequeño "deberían ser más manejables, al igual que el constructor/constructor para este objeto" grande".

 4
Author: Carl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 12:41:09

Uso C#, y estos son mis enfoques. Considere:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

Opción 1. Constructor con parámetros opcionales

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

Utilizado como, por ejemplo, new Foo(5, b: new Bar(whatever)). No para las versiones de Java o C# anteriores a la 4.0. pero aún vale la pena mostrar, ya que es un ejemplo de cómo no todas las soluciones son agnósticas al lenguaje.

Opción 2. Constructor tomando un único objeto de parámetro

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

Ejemplo de uso:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

C# a partir de la versión 3.0 hace que esto sea más elegante con la sintaxis del inicializador de objetos (semánticamente equivalente al ejemplo anterior):

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

Opción 3:
Rediseñe su clase para no necesitar un gran número de parámetros. Podrías dividir su repsonsibilities en múltiples clases. O pase parámetros no al constructor sino solo a métodos específicos, bajo demanda. No siempre es viable, pero cuando lo es, vale la pena hacerlo.

 2
Author: Joren,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-17 12:49:30