Moviendo rápido con valores inmutables en Dart

built_value de Dart proporciona valores inmutables potentes y convenientes para Dart. Los valores pueden ser inmutables, pero el paquete no ha sido…

El paquete built_value de Dart proporciona valores inmutables potentes y convenientes para Dart. Los valores pueden ser inmutables, ¡pero el paquete no se ha quedado quieto! En los seis meses desde que escribí por última vez sobre built_value ha habido diez mejoras importantes en el paquete.

También detecté algunos usos interesantes de built_value en la comunidad de Dart:

En fin, sin más dilación, aquí están todas las grandes cosas nuevas en built_value.

1. Clases genéricas

#

Los modelos de objetos sofisticados usan genéricos, así que por supuesto built_value necesita genéricos. Ahora puedes declarar tu tipo usando genéricos y la clase builder generada también los soportará:

abstract class GenericValue<T>
    implements Built<GenericValue<T>, GenericValueBuilder<T>> {
  T get value;
  ...
}

var value = new GenericValue<String>((b) => b..value = 'string');

Los tipos de valor que usan genéricos son serializables, como de costumbre.

2. Plugins de serialización

#

Una nueva API de SerializerPlugin te permite hacer modificaciones transversales a la serialización, ejecutando código arbitrario antes y después de que cada objeto sea serializado o deserializado.

Podrías, por ejemplo, usar esto para mapear entre dos versiones incompatibles de un protocolo. Pero un mejor ejemplo es la siguiente mejora…

3. Plugin de serialización “Standard JSON”

#

Por defecto, built_value serializa hacia y desde su propio formato JSON basado en listas, por rendimiento y flexibilidad.

Pero mucha gente necesita interoperar con datos y APIs JSON existentes. ¿Qué deberían hacer?

Ahora puedes instalar StandardJsonPlugin y cambiar a un formato JSON estándar basado en mapas.

final standardSerializers =
    (serializers.toBuilder()
        ..addPlugin(new StandardJsonPlugin())).build();

Mira el ejemplo (hacia el final).

4. Memoización de getters

#

Las clases inmutables son geniales — pero ¿qué deberías hacer con los campos derivados? Si un campo derivado es costoso de calcular, entonces las clases inmutables dan dos malas opciones: calcularlos por adelantado y siempre pagar una penalización posiblemente innecesaria, o recalcularlos bajo demanda y potencialmente pagar ese mismo gran costo muchas veces.

Entremos los getters @memoized. Añade esta anotación a un getter de built_value y se calculará de forma perezosa, pero como mucho una vez; luego se almacena en un campo oculto en la instancia.

Desde la versión 1.1.2 el propio built_value es un gran ejemplo de getters @memoized. Se usa un built_value para envolver un Element del analyzer, y el cómputo se hace de forma perezosa en los getters:

abstract class ValueSourceClass
    implements Built<ValueSourceClass, ValueSourceClassBuilder> {
  ClassElement get element;

  @memoized
  BuiltList<String> get genericParameters =>
      new BuiltList<String>(element.typeParameters
          .map((element) => element.computeNode().toString()));

  @memoized
  BuiltList<ValueSourceField> get fields =>
      ValueSourceField.fromClassElements(element, builderElement);

  // And many more @memoized getters.
}

5. Generación de código más rápida

#

…y como resultado de mover a getters memoizados, built_value es ahora significativamente más rápido generando código.

6. Factories más simples

#

Antes de la versión 0.5.5 cada clase built_value tenía que tener un método factory siguiendo una plantilla estándar. Este requisito se ha relajado. Aquí hay algunas buenas formas de usar esta mayor libertad.

Para clases con uno o dos campos, simplemente toma exactamente esos campos en el constructor:

abstract class Value implements Built<Value, ValueBuilder> {
  ClassElement get element;

  factory Value(ClassElement element) =>
      new _$Value._(element: element);
}

void main() {
   var element = getElement();
   var value = new Value(element);
}

O, donde quieres establecer valores predeterminados para los campos, ahora puedes hacerlo en la factory, en lugar de tener que escribir tu propia clase builder:

abstract class Value implements Built<Value, ValueBuilder> {
  int get x;
  int get y;
  bool get awesome;

  factory Value([updates(ValueBuilder b)]) =>
      new _$Value((b) => b
          ..awesome = True
          ..update(updates));
}

void main() {
  var value = new Value((b) => b
      ..x = 10
      ..y = 20);
}

Finalmente, si te gustan los argumentos nombrados, puedes exponer el constructor basado en argumentos nombrados en lugar del constructor basado en builder:

abstract class Value implements Built<Value, ValueBuilder> {
  int get x;
  int get y;

factory Value({int x, int y}) = _$Value._;

void main() {
  var value = new Value(x: 10, y: 20);
}

7. Salida toString más bonita y personalizable

#

Antes de la versión 1.0.0 la salida de toString de built_value era bastante plana. Ahora, por defecto está indentada, así que es fácil de leer:

CompoundValue {
  simpleValue=SimpleValue {
    anInt=1,
  },
}

Pero también es personalizable, mediante una variable global de nivel superior newBuiltValueToStringHelper. Si preferías el formato antiguo:

newBuiltValueToStringHelper =
    (className) => new FlatBuiltValueToStringHelper(className);

Entonces:

CompoundValue {simpleValue=SimpleValue {anInt=1}}

Y si quieres escribir la tuya propia — por supuesto que puedes. La clase BuiltValueToStringHelper es muy fácil de implementar:

abstract class BuiltValueToStringHelper {
  void add(String field, Object value);
  String toString();
}

8. DateTime y JsonObject serializables

#

La clase DateTime del SDK ahora es serializable — siempre que uses UTC en lugar de zona horaria local. (¡Serializar con zona horaria local es una receta para el desastre!)

Y, para ayudar más con la interoperabilidad con datos y APIs JSON existentes, ahora hay una clase JsonObject que envuelve valores json, listas y mapas. Los campos de tipo JsonObject se serializan directamente hacia y desde JSON crudo.

9. Matcher personalizado para tests

#

Los métodos operator== generados en built_value ya son muy útiles para tests, pero hay algo que no te dan: mensajes de error fácilmente legibles. En su lugar, cuando una comprobación de ‘equals’ en un test falla, obtendrás el toString completo de los valores esperado y actual.

¡Ya no más! Ahora puedes usar built_value_test, un nuevo paquete de pub. Si lo usas en un test ahora verás errores como este:

was <3> instead of <5> at location ['simpleValue']['anInt']

Si esto se parece mucho al mensaje que obtienes cuando una comparación de mapas falla en un test, es porque es lo mismo: está implementado convirtiendo los valores a mapas y luego comparándolos.

10. Código compatible con implicit-dynamic e implicit-cast; mejores mensajes de error

#

El código generado por built_value se ha limpiado significativamente para hacerlo pasar dos lints muy estrictos: ya no usa dynamic implícitamente en ningún sitio, y ya no depende de casts implícitos.

Estas dos mejoras pueden (o no) llevar a mejor código por parte del compilador.

Y se añadieron mejores mensajes de error para cuando declaras un campo de serializador con el tipo equivocado.

¡Eso fue todo! Gracias por leer. Si tienes alguna idea o petición de funcionalidad para built_value, dirígete al issue tracker.

Más de Dart