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

96

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 de constructores con largas listas de parámetros.

No se siente bien, es difícil de usar y la legibilidad se ve afectada.

Es incluso peor si los campos son algún 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 que el lenguaje esté orientado a objetos.

Soluciones en las que puedo pensar:

  1. "Abominaciones de constructores con largas listas de parámetros"
  2. El patrón del constructor
Malax
fuente
5
Creo que el patrón del constructor es la mejor y más estándar solución para esto.
Zachary Wright
@Zachary: Lo que estás defendiendo solo funciona para una forma especial del patrón de construcción, como lo explica Joshua Bloch aquí: drdobbs.com/java/208403883?pgno=2 Prefiero ir con el término relacionado de "interfaz fluida" para llamar esta "forma especial del patrón de construcción" (ver mi respuesta).
Sintaxis T3rr0r

Respuestas:

76

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í "CORRECTAMENTE HECHO" en negrita porque la mayoría de los programadores de Java se equivocan en las interfaces fluidas y contaminan su objeto con el método necesario para construir el objeto, lo cual, por supuesto, es completamente incorrecto.

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

FooFactory.create () , dondeXXX (..) y withXXX (..) todos crean "algo más".

Que algo más puede ser un FooFactory, aquí hay una forma 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 );
}
Sintaxis T3rr0r
fuente
11
@ all: por favor, no se queje sobre el sufijo "Impl" en "FooImpl": esta clase está oculta dentro de una fábrica y nadie la verá más que la persona que escribe la interfaz fluida. Lo único que le importa al usuario es que reciba un "Foo". También podría haber llamado "FooImpl" "FooPointlessNitpick";)
SyntaxT3rr0r
5
¿Te sientes preventivo? ;) Has sido quisquilloso en el pasado por esto. :)
Greg D
3
Creo que el error común que se refiere es que la gente se suman los métodos (etc) "withXXX" para el Fooobjeto, en lugar de tener una por separado FooFactory.
Dean Harding
3
Todavía tienes el FooImplconstructor con 8 parámetros. ¿Cuál es la mejora?
Mot
4
No llamaría a este código immutable, y tendría miedo de que la gente reutilice objetos de fábrica porque creen que lo es. Quiero decir: FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();donde tallsahora solo contendría mujeres. Esto no debería suceder con una API inmutable.
Thomas Ahle
60

En Scala 2.8, puede usar parámetros con nombre y predeterminados, así como el copymétodo en una clase de caso. Aquí hay un código de ejemplo:

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))
Martin Odersky
fuente
31
+1 por inventar el lenguaje Scala. Sí, es un abuso del sistema de reputación pero ... aww ... Amo tanto a Scala que tuve que hacerlo. :)
Malax
1
Oh, hombre ... ¡Acabo de responder algo casi idéntico! Bueno, estoy en buena compañía. :-) Sin embargo, me pregunto si no vi tu respuesta antes ... <encogiéndose de hombros>
Daniel C. Sobral
20

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, intente hacer espousey Option[Person], y luego casar a dos personas entre sí. No puedo pensar en una forma de resolver eso sin recurrir a un private vary / o un privateconstructor más una fábrica.

Daniel C. Sobral
fuente
11

Aquí hay un par de opciones más:

Opción 1

Haga que la implementación en sí sea mutable, pero separe las interfaces que expone a mutables e inmutables. Esto se toma 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
  }
}

opcion 2

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

Pequeñas Mesas Bobby
fuente
3
La opción 1 es inteligente (pero no demasiado inteligente), así que me gusta.
Timothy
4
Hice esto antes, pero en mi opinión está lejos de ser una buena solución porque el objeto aún es mutable, solo los métodos mutantes están "ocultos". Quizás soy demasiado quisquilloso con este tema ...
Malax
una variante de la opción 1 es tener clases separadas para las variantes inmutable y mutable. Esto proporciona una mayor seguridad que el enfoque de interfaz. Podría decirse que le está mintiendo al consumidor de la API cada vez que le da un objeto mutable llamado Immutable que simplemente necesita ser lanzado a su interfaz mutable. El enfoque de clases separadas requiere métodos para convertir de un lado a otro. La API de JodaTime utiliza este patrón. Consulte DateTime y MutableDateTime.
toolbear
6

Es útil recordar que existen diferentes tipos de inmutabilidad . Para su caso, creo que la inmutabilidad de "paleta" funcionará muy bien:

Inmutabilidad de la paleta: es lo que llamo caprichosamente un ligero debilitamiento de la inmutabilidad de una sola escritura. Uno podría imaginar un objeto o un campo que permaneció mutable por un tiempo durante su inicialización, y luego se "congeló" para siempre. Este tipo de inmutabilidad es particularmente útil para objetos inmutables que se referencian circularmente entre sí, o los objetos inmutables que se han serializado en el disco y después de la deserialización deben ser "fluidos" hasta que finalice todo el proceso de deserialización, momento en el que todos los objetos pueden ser congelado.

Así que inicializas tu objeto, luego colocas un indicador 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.

Julieta
fuente
1
Downvotes? ¿Alguien quiere dejar un comentario sobre por qué esta no es una buena solución?
Julieta
+1. Tal vez alguien haya rechazado esto porque está insinuando el uso de clone()para derivar nuevas instancias.
finnw
O tal vez fue porque puede comprometer la seguridad de los subprocesos en Java: java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw
Una desventaja es que requiere que los futuros desarrolladores tengan cuidado al manejar la bandera de "congelación". Si posteriormente se agrega un método mutador que se olvida de afirmar que el método está descongelado, podría haber problemas. De manera similar, si se escribe un nuevo constructor que debería llamar al freeze()método, pero no lo hace, las cosas podrían ponerse feas.
stalepretzel
5

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

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

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

ziggystar
fuente
La supuesta desventaja, crear más instancias de las necesarias, no es realmente un gran problema. La asignación de objetos es muy barata, al igual que la recolección de basura de objetos de corta duración. Cuando el análisis de escape está habilitado de forma predeterminada, es probable que este tipo de "objetos intermedios" se asignen en la pila y no cueste literalmente nada crearlos.
gustafc
2
@gustafc: Sí. Cliff Click contó una vez la historia de cómo compararon la simulación Clojure Ant Colony de Rich Hickey en una de sus cajas grandes (864 núcleos, 768 GB de RAM): 700 subprocesos paralelos que se ejecutan en 700 núcleos, cada uno al 100%, generando más de 20 GB de basura efímera por segundo . El GC ni siquiera se puso a sudar.
Jörg W Mittag
5

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 algunos problemas y necesita una refactorización.

Lo que estoy enumerando como (2) es bueno cuando no hay un estado detrás de la 'fábrica', mientras que (3) es el diseño de elección cuando hay un estado. Me encuentro usando (2) en lugar de (3) cuando no quiero preocuparme por los hilos y la sincronización, y no necesito preocuparme por amortizar una configuración costosa sobre la producción de muchos objetos. (3), por otro lado, se solicita cuando se realiza un trabajo real 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 tiene muchos pequeños objetos inmutables y el patrón preferible es obtener noticias de los antiguos.

Tenga en cuenta que no soy miembro del 'club de fans de patrones'; claro, vale la pena emular algunas cosas, pero me parece que adquieren una vida inútil una vez que la gente les da nombres y sombreros divertidos.

bmargulies
fuente
6
Este es el patrón de constructor (opción 2)
Simon Nickerson
¿No sería eso un objeto de fábrica ('constructor') que emite sus objetos inmutables?
bmargulies
esa distinción parece bastante semántica. ¿Cómo se hace el frijol? ¿En qué se diferencia de un constructor?
Carl
No desea el paquete de convenciones de Java Bean para un constructor (o para mucho más).
Tom Hawtin - tackline
4

Otra opción potencial es refactorizar para tener menos campos configurables. Si los grupos de campos solo funcionan (en su mayoría) 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 de este objeto "grande".

Carl
fuente
1
nota: el kilometraje de esta respuesta puede variar según el problema, el código base y la habilidad del desarrollador.
Carl
2

Yo uso C #, y estos son mis enfoques. Considerar:

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;
}

Usado como por ejemplo new Foo(5, b: new Bar(whatever)). No para las versiones de Java o C # anteriores a la 4.0. pero vale la pena mostrarlo, ya que es un ejemplo de cómo no todas las soluciones son independientes del idioma.

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 # de 3.0 en adelante lo hace 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 que no necesite una cantidad tan grande de parámetros. Podría dividir sus responsabilidades en varias clases. O pase los 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.

Joren
fuente