Patrón: Abstract document

El uso de atributos o propiedades de forma dinámica en lenguajes tipados, es un tarea compleja de afrontar. A la vez que no es correcto cuando implementamos una clase colocarle N atributos, los cuales pueden o no ser tenidos en cuenta por los objetos cuando se instancien. En otras palabras, es incorrecto y una violación estructural de diseño definir una clase con N atributos, si los objetos de la misma no van a dar valor a todas esas propiedades. Entonces, estamos ante un problema que puede tener lugar en muchos escenarios. Cuando un problema se vuelve repetitivo, siempre existe una solución a ese problema común, y nace un patrón o buena práctica para darle solución.

Patrón estructural Abstract Document

Para resolver el problema planteado en la introducción del artículo, podemos aplicar el patrón estructural «Abstract Document». El patrón de documento abstracto permite construir objetos con atributos dinámicos, manteniendo la seguridad de los tipos y aportando la flexibilidad de los lenguajes no tipados. Este patrón utiliza el concepto de «rasgos» para habilitar la seguridad de tipos, y separar las propiedades en un conjunto de interfaces.

En otras palabras, podemos agregar propiedades a los objetos de una clase de forma dinámica (extensibilidad), pudiendo tener objetos estructuralmente diferentes de una misma clase.

Caso de uso

Supongamos que tenemos una tienda de venta de televisores, pero no de todos los televisores tenemos todas las propiedades, entonces, queremos construir televisores solo usando las propiedades disponibles que tenemos de cada uno, para ofrecer solo esa información.

El patrón Abstract Document para este caso sería (en Java):

public interface Document {

    Void put(String key, Object value);

    Object get(String key);

    <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor);
}
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;

public abstract class AbstractDocument implements Document {

    private final Map<String, Object> properties;

    protected AbstractDocument(Map<String, Object> properties) {
        Objects.requireNonNull(properties, "properties map is required");
        this.properties = properties;
    }

    @Override
    public Void put(String key, Object value) {
        properties.put(key, value);
        return null;
    }

    @Override
    public Object get(String key) {
        return properties.get(key);
    }

    @Override
    public <T> Stream<T> children(String key, Function<Map<String, Object>, T> constructor) {
        return Stream.ofNullable(get(key))
                .filter(Objects::nonNull)
                .map(el -> (List<Map<String, Object>>) el)
                .findAny()
                .stream()
                .flatMap(Collection::stream)
                .map(constructor);
    }

}

Definimos todas las posibles propiedades.

public enum Property {
    PRICE, MODEL, SCREEN_SIZE
}

Ahora definimos las propiedades posibles en interfaces.

import java.util.Optional;

public interface HasModel extends Document {
    default Optional<String> getModel() {
        return Optional.ofNullable((String) get(Property.MODEL.toString()));
    }
}
import java.util.Optional;

public interface HasPrice extends Document {
    default Optional<Number> getPrice() {
        return Optional.ofNullable((Number) get(Property.PRICE.toString()));
    }
}
import java.util.Optional;

public interface HasScreenSize extends Document {
    default Optional<Number> getScreenSize() {
        return Optional.ofNullable((Number) get(Property.SCREEN_SIZE.toString()));
    }
}

Definimos la clase TV que servirá de base para crear los objetos con propiedades dinámicas.

public class TV extends AbstractDocument implements HasModel, HasPrice, HasScreenSize {
    protected TV(Map<String, Object> properties) {
        super(properties);
    }
}

Veamos algunos ejemplos de objetos

import java.util.Map;

public class Main {

    public static void print(TV tv) {
        System.out.println("TV Properties: ");
        tv.getModel().ifPresent(System.out::println);
        tv.getScreenSize().ifPresent(System.out::println);
        tv.getPrice().ifPresent(System.out::println);
        System.out.println("============");
    }

    public static void main(String[] args) {

        // TV 1
        Map<String, Object> props1 = Map.of(Property.MODEL.toString(), "Samsung",
                                            Property.PRICE.toString(), 2000);
        var tv1 = new TV(props1);
        print(tv1);

        // TV 2
        Map<String, Object> props2 = Map.of(Property.MODEL.toString(), "Samsung",
                                            Property.PRICE.toString(), 2000,
                                            Property.SCREEN_SIZE.toString(), 32);
        var tv2 = new TV(props2);
        print(tv2);
    }
}

Resultado de la corrida:

TV Properties: 
Samsung
2000
============
TV Properties: 
Samsung
32
2000
============

Espero te sea útil este artículo. Puedes acceder al ejemplo en GitHub.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *