Adaptive Object Model com Jmine

O que hoje é formalmente conhecido como Adaptive Object Model é uma técnica de desenvolvimento que permite descrever entidades usando instâncias e não usando somente classes, por assim dizer.

A vantagem desta abordagem é dar liberdade para que o cliente crie ‘tipos’ e ‘instâncias’ destes tipos de uma forma estruturada, tornando desnecessária a criação de centenas de classes e diminuindo, consequentemente, a complexidade do código de negócio.

O módulo “AOM” do Jmine Tec, é uma implementação desta técnica, que permite a criação de objetos tipados pelos próprios usuários.

Quando usar

É realmente difícil indicar quando ou não usar o AOM para resolver o seu problema, mas esta é uma abordagem indicada, quando você precisa dar liberdade para o usuário definir propriedades de uma entidade e seus valores sem precisar mexer com código.

Como este é um tópico muito abstrato, talvez alguns exemplos de aplicação ajudem a esclarecê-lo... Abaixo uma lista de aplicações que já vimos ou usamos com AOM. Para uma lista mais completa de aplicações, você pode visitar a página do Adaptive Object Model.

  • Validações

    É possível usar o AOM para definir regras de validação, onde o cliente determina propriedades e valores que determinarão a validade ou não de uma determinada operação.

  • ERP

    Imagine um sistema de ERP que guarda e transmite informações de venda de um e-comerce para a Receita Federal. O mesmo pode ser afirmado para os relatórios contábeis enviados para as prefeituras de cada cidade, cada uma impõe um conjunto de restrições e obrigações.

    As informações enviadas à receita ou a cada prefeitura pode ser armazenado usando ORM, assim, alterações em como cada entidade é armazenada e transmitida podem ser aplicadas de uma forma relativamente simples, sem a necessidade de deploy de uma nova versão ou downtime para upgrade.

Uso na Prática

Para fins práticos, vamos recorrer a um exemplo de uso de AOM descrevendo um cenário em que o cliente possa criar novos tipos de carros, com características específicas como cor, quantidade de turbos, elegância etc... Tudo isto será renderizado usando um software específico que entenda as entradas fornecidas.

Abordagem Clássica

Na abordagem clássica, usando herança, os desenvolvedores preparariam cada tipo de carro seria representado por uma classe, por exemplo:

public class Car {

    private String manufacturer;

    private int horsePower;

    private String color;

    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public int getHorsePower() {
        return horsePower;
    }

    public void setHorsePower(int horsePower) {
        this.horsePower = horsePower;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

Esta é a classe (ou interface) pai de todos os demais tipos de carros, ela provavelmente teria um método render(), que cada subtipo de carro sobrescreveria para ser renderizado apropriadamente.

public class FancyCar extends Car {

    private int fanciness;

    public int getFanciness() {
        return fanciness;
    }

    public void setFanciness(int fanciness) {
        this.fanciness = fanciness;
    }
}

O ponto negativo desta abordagem é simplesmente a falta de flexibilidade... Para se adicionar novos tipos de carros ou características é necessário trocar versão do sistema, e da ação imediata do desenvolvedor em cada novo tipo desejado.

AOM

Se flexibilidade é uma característica obrigatória, é possível usar o modelo proposto no AOM para desenvolver o sistema, permitindo que novos tipos de carro sejam descritos e novos carros renderizados de acordo com estas descrições. Resolvendo o exemplo proposto, precisamos apenas de dois tipos:

Código Exemplo: jmine.tec.aom.example.CarType

public class CarType {

    private String name;

    private Set<Property> properties = new HashSet<Property>();

    /**
     * Construtor default
     */
    public CarType(String typeName) {
        this.name = typeName;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * Retorna a lista de propriedades utilizadas na criação de tipos de carros
     *
     * @return um set imutável de propriedades
     */
    public Set<Property> getProperties() {
        return Collections.unmodifiableSet(properties);
    }

    public void addProperty(Property property) {
        this.properties.add(property);
    }
}

Código Exemplo: jmine.tec.aom.example.Car

public class Car {

    private CarType type;

    private Set<AbstractPropertyValue> propertyValues = new HashSet<AbstractPropertyValue>();

    public Car(CarType type) {
        this.type = type;
    }

    public Set<AbstractPropertyValue> getPropertyValues() {
        return Collections.unmodifiableSet(propertyValues);
    }

    public CarType getType() {
        return type;
    }

    public void setType(CarType type) {
        this.type = type;
    }

    public void addPropertyValue(AbstractPropertyValue value) {
        propertyValues.add(value);
    }
}

Com isto, o próprio cliente pode compor novos tipos de carros, e utilizá-los no sistema. Basicamente, cuidamos de dois pontos distintos:

  • Descrição: ocorre através de propriedades, seria algo próximo à definição de uma classe, mas de forma dinâmica, representado no exemplo como CarType
  • Criação: uma vez determinado quais os tipos de propriedades de um determinado tipo, é necessário preencher seus valores. Mantendo a analogia do OOP, isto seria o equivalente a criar uma instância de uma classe.

Para descrever dinamicamente um tipo, usamos o tipo Property, ele é usado em conjunto com AbstractPropertyValue, que define o valor de uma propriedade. Tanto uma propriedade quanto seu valor são entidades armazenadas em banco, por isto é necessário criar algumas dependências antes de começar a descrever tipos.

Você pode criar um conjunto de Property, para que o cliente utilize, ou permitir que ele mesmo cadastre novos tipos de propriedade via tela. Veja um exemplo de criação de Property, PropertyGroup e outras entidades necessárias:

Logo em seguida criamos um PropertyGroup, para só então criarmos os tipos de Propriedades e disponibilizá-las para uso.

Dado que temos um conjunto de propriedades para usarmos, basta definir nossas entidades e definir seus valores, como abaixo:

// Carro Comum
CarType commonCarType = new CarType("Common Car");
commonCarType.addProperty(availableProperties.get("manufacturer"));
commonCarType.addProperty(availableProperties.get("horsePower"));
commonCarType.addProperty(availableProperties.get("color"));

Car commonCar = new Car(commonCarType);
commonCar.addPropertyValue(createStringValue(availableProperties.get("manufacturer"), "General Motors"));
commonCar.addPropertyValue(createIntegerValue(availableProperties.get("horsePower"), 300));
commonCar.addPropertyValue(createStringValue(availableProperties.get("color"), "Ocean Blue"));

// Carro Extravagante
CarType fancyCarType = new CarType("Fancy Car");
fancyCarType.addProperty(availableProperties.get("manufacturer"));
fancyCarType.addProperty(availableProperties.get("horsePower"));
fancyCarType.addProperty(availableProperties.get("color"));
fancyCarType.addProperty(availableProperties.get("fanciness"));

Car fancyCar = new Car(fancyCarType);
fancyCar.addPropertyValue(createStringValue(availableProperties.get("manufacturer"), "General Motors"));
fancyCar.addPropertyValue(createIntegerValue(availableProperties.get("horsePower"), 30));
fancyCar.addPropertyValue(createStringValue(availableProperties.get("color"), "Star White"));
fancyCar.addPropertyValue(createIntegerValue(availableProperties.get("fanciness"), 100));

// Carro de Corrida
CarType raceCarType = new CarType("Fancy Car");
raceCarType.addProperty(availableProperties.get("manufacturer"));
raceCarType.addProperty(availableProperties.get("horsePower"));
raceCarType.addProperty(availableProperties.get("color"));
raceCarType.addProperty(availableProperties.get("noxCharges"));

Car raceCar = new Car(raceCarType);
raceCar.addPropertyValue(createStringValue(availableProperties.get("manufacturer"), "General Motors"));
raceCar.addPropertyValue(createIntegerValue(availableProperties.get("horsePower"), 800));
raceCar.addPropertyValue(createStringValue(availableProperties.get("color"), "Vader Black"));
raceCar.addPropertyValue(createIntegerValue(availableProperties.get("noxCharges"), 4));

// Não estamos limitados ao que está implementado no sistema. É possível combinar as características para
// criar novos tipos dinâmicamente, e basear-se nestas características para dar prosseguimento a outros
// processos e sistemas relacionados.

Descrevendo entidades com propriedades

O domínio do Jmine Tec AOM gira em torno da interface jmine.tec.aom.domain.Propertized, todo tipo de entidade que permite a definição dinâmica de propriedades deve implementá-la. Existe uma classe abstrata que facilita algumas das operações mais básicas de gerenciamento destas propriedades em jmine.tec.aom.domain.impl.AbstractPropertized

Repare que jmine.tec.aom.domain.Propertized distingue dois tipos de propriedades através dos métodos jmine.tec.aom.domain.Propertized.getRealProperties e jmine.tec.aom.domain.Propertized.getSyntheticProperties. Abaixo explicamos a diferença entre os dois tipos.

Caso não haja necessidade de distinção, o método jmine.tec.aom.domain.Propertized.getProperties devolve todas as propriedades cadastradas para a entidade, independente se dinâmicas, reais ou sintéticas.

Tipos de Propriedades

Propriedades Sintéticas

Permite anotar um campo de um objeto como uma propriedade, sem obrigatoriamente estar vindo da estrutura dinâmica do AOM. Isto é feito através da anotação jmine.tec.aom.annotation.SyntheticProperty.

Normalmente Propriedades Sintéticas são usadas para favorecermos desempenho ao troco de sacrificarmos flexibilidade. Também é uma ótima forma de definir campos já existentes de uma entidade como propriedade. Veja o exemplo abaixo:

Propriedades Dinâmicas

Propriedades definidas e mapeadas com Property, guardam o Nome, o Tipo e o Grupo da propriedade. Também é possível definir o valor da propriedade usando o método jmine.tec.aom.domain.Property.withValue, ele criará e retornará um objeto do tipo jmine.tec.aom.domain.AbstractPropertyValue do tipo do valor definido.

Propriedades Reais

O conjunto de propriedades Dinâmicas e Sintéticas, todas tratadas apenas como propriedades.

Property Handlers

Como você já deve ter percebido, a entidade jmine.tec.aom.domain.Propertized é consideravelmente grande. Para diminuir o custo de seu uso, foi criada a classe jmine.tec.aom.domain.impl.AbstractPropertized, ela envelopa os principais usos de um jmine.tec.aom.domain.Propertized, e permite casos específicos com um sistema de hierarquia controlado por Property Handlers, entidades do tipo jmine.tec.aom.domain.Propertized que fazem boa parte do processamento necessário para lidar com propriedades.

Existem algumas implementações de PropertyHandlers, como o BasePropertyHandler e o FullPropertyHandler, mas nada impede a criação de novos Handlers caso necessário.

BasePropertyHandler

Como o próprio nome sugere, é uma implementação básica de jmine.tec.aom.domain.Propertized, que controla Propriedades Dinâmicas e seus valores. Não existe muito segredo, este handler simplesmente recebe a lista de propriedades da entidade e permite que adicionemos seus valores.

FullPropertyHandler

Esta é uma versão um pouco mais avançada, basicamente, permite o uso de Propriedades Sintéticas, Dinâmicas e Reais.

Hierarquia de Handlers

Se você já usou ou está considerando o uso de PropertyHandlers deve ter percebido que eles possuem um parent, até mesmo o jmine.tec.aom.domain.impl.AbstractPropertized possui um. Isto permite que criemos uma hierarquia entre os tipos definidos. Por exemplo:

  • Carro:
    • Propriedades:
      • Cor
      • Cromado
      • Tamanho
      • Estilo
    • Valores:
      • Cor: Branca
  • Carro Conversível (Parent Carro):
    • Propriedades:
      • Velocidade
    • Valores
      • Cor: Preta
      • Cromado: Sim
      • Tamanho: XL
      • Estilo: I Have Money
      • Velocidade: Alta

Carro possui as propriedades, e pode até mesmo possuir algum valor, que serão usados por Carro Conversível. Caso este não declare o valor para a propriedade Cor, por exemplo, o valor do parent será utilizado, e ao mesmo tempo ele declara a propriedade Velocidade, que não existe no parent.

Referências de Código

Por favor, verifiquem o a classe jmine.tec.aom.example.OnlineCarBuilderExampleTest para maiores informações e exemplo de código usando a estrutura do AOM.