Acoplamento arquitetural usando DAOFactory/Service

Estou com dificuldades em tornar a arquitetura de um sistema mais flexível e desacoplada, vou dar uma breve descrição do projeto de software usado e do problema que estou tendo.

Como exemplo vou usar figurativamente o domínio “Produto” quando quero cadastrar o mesmo:

  1. O controller recebe o form na requisição, valida e converte o mesmo para um objeto de domínio;
  2. De posse do objeto de domínio, instancio a classe ProdutoService dentro do controller passando no construtor da ProdutoController a dependência DAOFactory;
  3. Chamo o método salvar da classe ProdutoService passando o objeto de domínio;
  4. No método salvar da classe ProdutoService instancio o ProdutoDAO por meio da DAOFactory;
  5. Chamo o método salvar da classe ProdutoDAO.

Minhas dúvidas:

  1. Ficar passando esse DAOFactory de dentro do Controller está me parecendo bem errado, tendo em vista que está criando um acoplamento no controller, e o correto seria a camada de serviço se comunicar com a camada da dados(DAO). Como resolvo isso?
  2. Está ficando muito complicado gerenciar todas as dependências de DAO usadas dentro da classe ProdutoService haja vista que não existe ferramenta de injeção de dependência no framework usado. Com o pattern Factory conseguiria resolver isso?
  3. Em um cenário real tenho uma classe Service que trata de vários objetos de domínio diferentes, o que gera uma quantidade ainda maior de dependência dos DAOs, neste caso não sei se fiz errado a classe Service ou se a solução proposta por mim no item 2 resolveria o problema.

Concordo. No seu caso, acho melhor deixar apenas a service conhecer alguma coisa de DAO.

Quais complicações vc está tendo?

Entendo que o DAOFactory é uma dependência do Service, por isso passo ele no construtor do Service(desacoplando para facilitar os mocks de teste), porém não vejo como passar ele como dependência pro Service sem ser por meio do controller. Tem alguma ideia de como fazer isso? Lembrado o que o DAOFactory é estático…

Digamos que minha ProdutoService trate de vários objetos de domínios que interagem entre si, Produto e Item por exemplo, e que exista um método vincularItemProduto…vou precisar do ProdutoDAO e ItemDAO para realizar este método…estava colocando todas estas dependências como atributos de ProdutoService, talvez fosse o correto deixar isto somente dentro do método que usará as chamadas aos DAOs.

ProdutoService {
  private DAOFactory daoFactory;
  private ProdutoDAO produtoDAO;
  private ItemDAO itemDAO;

  public void vincularItemProduto(Produto p, Item i) {
    this.produtoDAO = daoFactory.getProdutoDAO();
    this.itemDAO = daoFactory.getItemDAO();

    // lógica de negócio para vincular..depois persiste os registros
    produtoDAO.salvar(p);
    itemDAO.salvar(i);
  }
}

Como vc está criando uma instancia de um service no controller? Você está trabalhando com JBDC puro (objeto Connection) ?

Uso Hibernate, porém como disse não existe recurso de injeção de dependência…a única forma que vi de fazer essa ligação foi instanciando o service no controller.

ProdutoController {
  public void salvarProduto() {
    //instancia produto vindo do form
    new ProdutoService.salvar(produto);
  }
}

Uma forma bacana de fazer é usar uma factory para instanciar suas services. Com isso a factory ficaria responsavel por criar as services já passando no construtor as instancias dos DAOs.

Isso faria com que a responsabilidade de criar as instancias ficasse com a factory. Seria aplicado o conceito de IoC (Inversão de Controle).

Se precisar, faço um exemplo. Flws!

Exatamente isso que estou buscando, implementar IOC sem utilizar injeção de dependência, e uma das formas que tinha visto era manualmente por meio do padrão Factory.

Neste caso a chamada dos métodos da Factory ficaria dentro do controller, certo?

Isso. A factory ficaria responsável por criar as instancias das services, com isso, “injetando” as dependencias via construtor.

Mais uma dúvida, tenho nesta classe Service mais de 10 métodos e cada método usa um tipo de DAO diferente(dependência), ou seja, no padrão Factory terei essa mesma quantidade de fábricas, uma para cada cenário de método, seria isso?

Não tive oportunidade de implementar este padrão ainda, então imagino que seja isso…apesar que será bastante trabalhoso.

Pensei melhor, talvez possa criar somente uma fábrica com um método, passando para ele uma lista de GenericDAO e lá dentro instanciar os DAOS implementados e “injetar” via construtor na classe Service.

Na factory, vc soh precisa criar o método para criar o service 1 vez (um para cada service). Se um dia a service precisar de mais uma dependencia, vc altera a factory adicionando a dependencia no construtor e pronto.

Ex.:

ServiceA

public class ServiceA {
	private final DaoA daoA;
	private final DaoB daoB;
	
	public ServiceA(DaoA daoA, DaoB daoB) {
		this.daoA = daoA;
		this.daoB = daoB;
	}
}

Factory

public class Factory {
	
	public static final ServiceA serviceA() {
		return new ServiceA(GenericDAO.getDaoA(), GenericDAO.getDaoB());
	}
}

Entendi, mas como disse minha Service tem mais de 10 métodos e cada método usa um DAO diferente, ou seja, para cada método terá um cenário de dependência de DAO diferente…neste caso se eu colocar todas essas dependências no construtor, cada vez que a fábrica instanciar o Service vai “injetar” diversos DAOs que nem serão usados, não sei se consegui explicar…

Não sei como funciona em ferramentas de injeção de dependência por trás dos panos, é feito desta forma injetando todos DAOs de uma vez?

Sim. entendi. Na real, essa solução funcionaria melhor se houvesse um controle sobre as instâncias a serem injetadas (como um contexto mesmo). E uma forma de fazer isso é já deixar as instancias armezenadas em algum lugar, como um Map por exemplo, e só recuperar desse map quando a instância dos services forem solicitadas (dá até para fazer o mesmo com as instâncias dos services também se for possível).

Ai já complica mais, hehe. Acho que vou fazer dessa forma mais simples num primeiro momento, mesmo que não seja tão otimizado porém acho que isso não vai atrapalhar em nada.

Outra coisa, caso daqui alguns dias crie outro método na minha classe Service, que use um novo DAO que não está sendo injetado no construtor, terei que alterar a Factory/Construtor Service sempre nestes caso né?

public static final ServiceA serviceA() {
  return new ServiceA(GenericDAO.getDaoA(), GenericDAO.getDaoB(), GenericDAO.getDaoC());
}

Montei um exemplo mais elaborado, veja o que tu acha:

Factory

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Factory {

    private static final Map<Class<?>, Object> INSTANCES = new HashMap<>();
    
    static {
//        manualBeanDefinition();
        automaticBeanDefinition("com.lucas.tests.main.mycontext");
    }
    
    /**
     * Adiciona manualmente as classes nas INSTANCES.
     */
    private static void manualBeanDefinition() {
        addBean(new DaoA());
        addBean(new DaoB());
    }
    
    /**
     * Escaneia as classes do pacote informado e verifica se as classes encontradas possui a anotação @DAO. 
     * Se tiver, adiciona nas INSTANCES.
     * 
     * obs.: O código pode ser melhorado para procurar as classes recursivamente (dentro de subpacotes)
     */
    private static void automaticBeanDefinition(String basePackage) {
        List<Class<?>> classes = ReflectionUtil.getClassesFromPackage(basePackage);
        
        for(Class<?> classe : classes) {
            try {
                if(classe.isAnnotationPresent(DAO.class)) {
                    addBean(classe.newInstance());
                }
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static <T> T getBean(Class<?> klass) {
        T bean = (T) INSTANCES.get(klass);
        
        if(bean == null) {
            throw new IllegalArgumentException("No bean definition found for " + klass);
        }
        
        return bean;
    }
    
    private static <T> void addBean(T bean) {
        System.out.println("Creating bean for class " + bean.getClass());
        INSTANCES.put(bean.getClass(), bean);
    }
    
    public static <T> T create(Class<T> classe) {
        return (T) ReflectionUtil.createFromConstructor(classe, klass -> getBean(klass));
    }
    
    @FunctionalInterface
    interface SupplierBean {
        Object getBean(Class<?> klass);
    }
    
    /* *****************************************************
     * Declaração dos métodos para criação da services
     * *****************************************************/
    
    public static final ServiceA serviceA() {
        return create(ServiceA.class);
    }
}

ReflectionUtil

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;

import com.lucas.tests.main.mycontext.Factory.SupplierBean;

public class ReflectionUtil {

    public static List<Class<?>> getClassesFromPackage(String basePackage) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        String path = basePackage.replace('.', '/');
        File dir = new File(classLoader.getResource(path).getFile());
        File[] files = dir.listFiles();
        List<Class<?>> classes = new ArrayList<>();
        
        for(File file : files) {
            try {
                classes.add(Class.forName(basePackage + "." + file.getName().replace(".class", "")));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        
        return classes;
    }
    
    public static <T> T createFromConstructor(Class<T> klass, SupplierBean beanSupplier) {
        Constructor<?>[] constructors = klass.getDeclaredConstructors();
        
        if(constructors == null || constructors.length == 0) {
            throw new IllegalStateException("Could not create instance for " + ServiceA.class + ". No constructor found!");
        }
        
        Constructor<?> constructor = constructors[0];
        Parameter[] parameters = constructor.getParameters();
        Object[] objParams = new Object[constructor.getParameterCount()];
        
        for(int i = 0; i < constructor.getParameterCount(); i++) {
            objParams[i] = beanSupplier.getBean(parameters[i].getType());
        }
        
        try {
            return (T) constructor.newInstance(objParams);
        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            return null;
        }
    }
}

ServiceA

public class ServiceA {

    private final DaoA daoA;
    private final DaoB daoB;

    public ServiceA(DaoA daoA, DaoB daoB) {
        this.daoA = daoA;
        this.daoB = daoB;
    }
    
    public String callDaoA() {
        return daoA.test();
    }
    
    public String callDaoB() {
        return daoB.test();
    }
    
    public String callDaoAAndDaoB() {
        return callDaoA() + ":" + callDaoB();
    }
}

Anotação DAO

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DAO {

}

DaoA

@DAO
public class DaoA {

    public String test() {
        return "daoA";
    }
}

DaoB

@DAO
public class DaoB {

    public String test() {
        return "daoB";
    }
}

Main (representa o controller)

public class MainMyContext {
    
    public static void main(String[] args) {
        ServiceA serviceA = Factory.serviceA();
        System.out.println("DaoA: " + serviceA.callDaoA());
        System.out.println("DaoB: " + serviceA.callDaoB());
        System.out.println("DaoA e DaoB: " + serviceA.callDaoAAndDaoB());
    }
}

Ficou bem completo, vou precisar estudar com calma e tentar implementar pra entender bem, minha ideia era fazer algo assim mesmo, deixar dinâmico a criação dos Services e a injeção dos DAOs.

Pensando um pouco, acho que estou com uma quantidade grande de dependências de DAO na minha Service, talvez esta última esteja com muitas responsabilidades e tenha que quebrar em mais Services…quando fizer essa Factory vai facilitar bastante.

Obrigado!

1 curtida

Precisando de qualquer ajuda no entendimento, soh perguntar man.

Esse código que montei precisa tratar ainda algumas coisas ok (tratamento de error adequados, e validações, por exemplo)