Refatorando o JForum - ajuda com a arquitetura

Atencao: A mensagem que se segue ficou um tanto grande, e nao cobre todos os pontos que gostaria. Portanto, ao inves de fazer um livro com as minhas duvidas, vou faze-las em partes, de acordo com o andamento do topico.

Atualmente, a logica de negocios do JForum esta basicamente nas actions - como “logica de negocios” aqui, por favor entendam coisas como verificacao de seguranca, validacao de parametros, definicao do contexto do freemarker e acesso aos DAOs. Ha bastante codigo em classes utilitarias tambem, as quais foram movidas para tais pois eram usadas em mais de um lugar. Nao vou entrar em detalhes de implementacao por nao haver mta necessidade.

Para sair dessa meleca, comecei a refatorar o JForum. A cada hora que passa vejo cada vez mais que estou a ponto de reescrever uma enorme parte do codigo fonte. Embora o codigo atual esteja limpo e, principalmente, facil de entender e manter, falta pouco para um limite da arquitetura, onde entao ficara muito dificil adicionar coisas novas sem prejudicar a estabilidade do sistema (aka, falta de testes da nisso).
Conversando com o Villela e dando uma lida no PoEAA, GOF e Core J2EE Patterns (fora material da net), acabei ficando com muitas duvidas em relacao a como fazer o refactoring. De uma maneira ampla, alem dos ja citados elementos “extensiblidade” e “testabilidade”, faz-se necessario ter uma API de servicos, afim que seja possivel interagir com o JForum usando meios outros que somente um browser.

Tendo isso em mente, o que tenho (praticamente) definido eh assim:

:arrow: As actions somente irao interagir com os servicos, e fazer coisas especificas ao ambiente - no caso de web / browser normal, eh configurar o contexto do sistema de templates / criar objetos a serem passados aos servicos.

:arrow: Sobre “servico” eu entendo como um conjunto de metodos que servem como ponto de entrada / delegadores para a camada de negocios.

Ai comecam a pintar as maiores duvidas, sendo um tanto dificil achar um ponto de partida. Vou explicar o que comecei a fazer, tentar dar alguma justificativa, e esperar que alguem aqui tenha uma boa ideia. A primeira ideia que eu tive foi, obviamente, tirar todo codigo “de negocios” das actions, e mover para classes de servico, como PostService, TopicService e etc… tais classes verificam se o usuario tem direito de execucao na tarefa, se o topico solicitado existe, e entao chamam os metodos dos DAOs para fazer a persistencia. Simplificando o codigo, fica algo como

// PostService.java#delete(int postId)
if (!SecurityRepository.canAccess(SecurityConstants.PERM_MODERATION_POST_REMOVE)) {
	throw new ServiceException(ServiceErrors.POST_CANNOT_DELETE);
}

// ...

PostDAO pdao = DataAccessDriver.getInstance().newPostDAO();
Post p = pdao.selectById(postId);

// ...

pdao.delete(p);

// ...

DataAccessDriver.getInstance().newUserDAO().decrementPosts(p.getUserId());
tdao.decrementTotalReplies(this.underlyingPost.getTopicId());

int minPostId = tdao.getMinPostId(p.getTopicId());
if (minPostId > -1) {
  tdao.setFirstPostId(p.getTopicId(), minPostId);
}

// ...

fdao.setLastPost(p.getForumId(), maxPostId);

Ou seja, o servico se encarrega de cuidar das dependencias e tudo mais. Porem, esse codigo nao esta me agradando muito, pois ainda parece um codigo bastante tanto acoplhado e especialmente fragil. O cv sugeriu usar listeners e mover boa parte da logica para os pojos, formando assim um domain model (ou algo parecido com isso). Indo por esse lado, o codigo anterior resultaria nisso:

Define o listener

interface PostListener {
	void postDeleted(Post p);
}

Classes que precisam ser notificadas sobre alteracoes em posts implementam-a

interface UserDAO extends PostListener { ... }
class ConcreteUserDAO implements UserDAO {
	void postDeleted(Post p) {
		this.decrementPosts(p.getUserId();
	}

	void decrementPosts(int userId) { .... }
	// ...
}

O mesmo para o topico

interface TopicDAO extends PostListener { ... }
class ConcreteTopicDAO implements TopicDAO {
	void postDeleted(Post p) {
		int totalPosts = this.getTotalPosts(p.getTopicId());
		if (totalPosts < 1) {
			this.delete(p.getTopicId());
			return;
		}
		
		int maxPostId = this.getMaxPostId(p.getTopicId());
		this.setMaxPostId(maxPostId);

		int minPostId = this.getMinPostId(p.getTopicId);
		this.setMinPostId(minPostId);
	}
}

A classe de post se encarrega de notificar todo mundo

class Post {
	static List listeners = // ...

	void delete() {
		PostDAO dao = // ...
		dao.delete(this);
		this.notifyPostDeleted();
	}

	void notifyPostDeleted() {
		for (PostListeler pl : listeners) {
			pl.postDeleted(this);
		}
	}

Os observers sao registrados no startup do sistema

Post.addListener(new ConcreteUserDAO());
Post.addListener(new ConcreteTopicDAO());

Removendo uma mensagem.

Post p = // ...
p.delete();

A BEM grosso modo eh isso. Nao tenho uma implementacao concreta, mas tenho varias duvidas. Dicas gerais sobre formas de estruturar o codigo, responsabilidades e tudo mais sao muito bem vindas.

Rafael

Bom, acho que eu nem tenho muito a adicionar aqui… mas pq vc tem uma interface pros DAOs? Se, a principio, vc so tiver uma implementacao, use a impl direto (dado que o seu model nao vai usar o DAO diretamente, ele so vai notificar um Listener - que por acaso eh um DAO). Assim voce evita classes com nomes medonhos do tipo FooImpl ou ConcreteFoo :slight_smile:

E depois se precisar trocar a impl faz um refactoring no código todo…??? Eu tenho opinião contrária… Eu acho q devia continuar com as interfaces…

O que tem de tao dificil em UM extract interface, Eduardo?

Soh existe UM DAO???
E pq não fazer do “jeito correto” logo??? Qual e a vantagem de fazer o extract interface depois?

Nao eh taaao importante assim, mas o ponto eh que, se vc vai ter uma interface com uma unica implementacao, entao pra que a interface? “Ah, eu posso ter uma outra implementacao qualquer dia desses”? Se esse dia nao chegar, vc programou a toa :wink:

E se esse dia chegar?

simples… vai lá e muda!!!

Se esse dia chegar vc perde os 5 minutos que vc precisa pra dar os extract interface nos lugares necessarios, e implementa os novos DAOs, ueh! Voce vai ter que revisar as interfaces de qualquer jeito, pq fatalmente a nova implementacao vai ter alguma diferencazinha que vc vai ter que acomodar pra cobrir todos os casos sem fazer muito esforco. :wink:

XP, XP, XP é isso que o CV ta tentando dizer.
Faca o que precisa hoje, se um dia precisar mudar algo vai la e muda. Ou seja se precisar de um DAO novo extract interface, enquanto esse dia nao chega deixa o sistema com uma interface a menos.

]['s

Pelamordedeus… voces tao brigando por causa dos daos? :mrgreen:

Eu tenho interfaces para eles pq nao uso Hibernate e preciso suportar diversas fontes de dados (suporta ja mysql, hsqldb, postgresql, oracle e sqlserver).

Nao, uma migracao para hibernate nao esta nos planos no momento :wink:

Rafael

btw: eh MySqlForumDAO, OracleForumDAO etc… mas isso quem me traz eh o abstract factory.

Ah nem…
Acho que é muito estilo de programação isso aqui… Cada um acha melhor do seu jeito… Eu prefiro abstrair e esconder atrás de interfaces desde o primeiro momento…
Sejam felizes aqueles que nâo gostam :stuck_out_tongue: :stuck_out_tongue: :stuck_out_tongue:
Mas sei lá, acho que programar classes sem interfaces eh menos OO do que programar com interfaces…
Talvez seja purismo demais, mas eu gosto do código com mais interfaces (sem exageros, mas eu acho que no caso de daos se justifica sim)…

T+

[quote=Rafael Steil]Pelamordedeus… voces tao brigando por causa dos daos? :mrgreen:

Eu tenho interfaces para eles pq nao uso Hibernate e preciso suportar diversas fontes de dados (suporta ja mysql, hsqldb, postgresql, oracle e sqlserver).

Nao, uma migracao para hibernate nao esta nos planos no momento :wink:

Rafael[/quote]
Brigando nada! Eh soh uma discussãozinha bem construtiva…
:slight_smile:

T+

Bom, nesse caso, voce ja TEM mais de uma implementacao. Caso encerrado :mrgreen:

Em Smalltalk, uma linguagem que ninguem aqui discorda que eh 100% OO, nao existem interfaces. E ai, como fica? :wink:

O ponto eh que interfaces sao soh contratinhos que vc coloca na frente das suas classes. Eh o jeito que o Java arrumou pra fazer heranca multipla, mas nao eh algo OBRIGATORIO, ou que deveria te deixar desconfortavel. Se nao precisar ter, nao precisa ter, e pronto. Menos codigo pra escrever, documentar, depurar e manter :smiley:

Em Smalltalk, uma linguagem que ninguem aqui discorda que eh 100% OO, nao existem interfaces. E ai, como fica? :wink:

O ponto eh que interfaces sao soh contratinhos que vc coloca na frente das suas classes. Eh o jeito que o Java arrumou pra fazer heranca multipla, mas nao eh algo OBRIGATORIO, ou que deveria te deixar desconfortavel. Se nao precisar ter, nao precisa ter, e pronto. Menos codigo pra escrever, documentar, depurar e manter :D[/quote]
Como eu disse antes, bem do gosto do fregues…
Como fica akela baboseira toda da OO sobre programacao orientada por contratos nessa linguagem???

Contratos são muito melhor expressos em pós e pré condições do que simplesmente com assinaturas de métodos. Smalltalk eu não sei, mas Eiffel (e até mesmo aquela linguagem de constraints maldita da UML) tem como forma de estabelecer contratos:

  • Inveriante da classe
  • Pós Condição por método
  • Pré-condição por método

Quando você chama um método ele checa a pré-condição, executa o método, checa a pós e após isso checa a invariante. Isso sim é um contrato obedecido :slight_smile:

Bom, o tópico descambou de assunto, as ever, ma Steil… pra mim tá legal. Infelizmente não liberam a droga da porta do CVS daqui pra eu poder fazer um checkout :frowning:

A OCL (Object Constraint Language)? :twisted:

Ah, finalmente voltamos ao assunto :D.

O lance de usar listeners eh uma boa entao?! Legal, show. Agora entra a segunda parte da duvida:

Algum problema em ter alguns metodos na Post, como delete() / update() (que geralmetne precisam de notificacoes / validacoes mais pesadas) e, nos outros casos (comos selects de utilidade geral), acessar o DAO diretamente quando precisar? (“diretamente” aqui seria o servico que chamaria).

Eh ok assim? Por um lado ficaria um pedaco do trabalho em um canto e o resto em outro canto, e nao teria como impedir que alguem chamasse o dao direto (bom, claro que eu nao faria isso, mas vai saber quem vai colocar a mao no codigo).

Ai, complementando essa pergunta, estava pensando em deixar as validacoes no servico… tipo, verificar se o conteudo existe, se o usuario tem direito de acesso etc etc… Como todo mundo sempre ira passar pelo service - inclusive a api externa -, me parece um bom lugar… Eh ok?

Valeu
Rafael

Dae,

acho q esta discussão sobre usar interfaces ou não está poluindo o tópico.

Rafael, gostei da idéia do cv de usar Listeners. Sugiro usar AOP para isto.

Acho q só adicionar uma camada de Business Objects já melhora bastante. Normalmente eu diria para usar Delegates nos casos de lógicas mais complexos, mas a opção dos Listeners me parece melhor neste contexto.

Fora isto tenho apenas duas considerações a fazer:

  1. Tu já está cansado de ler isto mas, eu vou repetir denovo só para não perder a fama de chato: A camada de persistência é a mais complicada na minha opinião. Usar um framework de persistência tornaria muito mais fácil adicionar e MANTER o suporte ao um novo BD. Concordo q o refactoring está em primeiro plano mas, não deixaria de colcoar em segundo plano o uso de um framework de persistência.

  2. Desenvolvimento mais focado em Test Case. O refactoring vai tornar mais simples o uso de Test Cases, eu sei. Apesar disto poderíamos investir mais nisto, principalmente por causa dos DAOS. Sem Test Cases o trabalho manual de testar cada implementação se torna inviável.
    Eu gosto muito de testar deste a camada controller. No Spring é bem fáci lde fazer isto, a getne podia pegar um pouco da mesma idéia e implementar no JForum.
    Olha um test de action usando Spring:

...
 NewsMultiAction action = (NewsMultiAction) ac.getBean("newsViewController");
        ModelAndView mav = action.handleRequest((HttpServletRequest) null,(HttpServletResponse) null);
        Map m = mav.getModel();
        assertNotNull(m.get("newsList"));
        assertEquals(mav.getViewName(), "/News/list");

Assim eu consigo testar até se a página resultante é realmente a página que eu quero. Não chega ao ponto de ver se o HTML está quebrado, nem é este o objetivo e seria fácil de adicionar caso fosse necessário.