Strukturierung Webanwendung

torax13

Erfahrenes Mitglied
Hallo,

nachdem ich die letzten Jahre zwar Webanwendungen entwickelt hab, dies aber mit einem hausinternen Framework, beschäftige ich mich im Moment mit 'aktuellen' (ich weiß, ein alter Hut für viele) Themen wie JPA und JSF.

Zu den einzelnen Technologien existieren ja viele Anleitungen und Howtos für ausgewählte Beispiele (ich les gerade das 'Java Persistence with Hibernate' eine sehr gute Einführung in JPA). Was mir bisher fehlt ist ein Überblick wie ich ein Projekt mit JPA und JSF am besten strukturiere.

Ich könnt zwar mein kleines Beispielprojekt sicherlich irgenwie 'zurechtfriemeln' aber mir gehts ja gerad darum eine gute Strukur herauszuarbeiten.

Layeransicht:
* Persistensklassen (das reine Mapping)
* DAO (Kappselung der Persistensklassen in Bussiness Objekte, hier kommen auch die Regeln rein?)
* View (JSF)

Hier fehlt sicherlich noch die Controller Schicht, da ich bei JSF aber noch sehr am Anfang stehe, wüßt ich im Moment nicht so richtig, was dessen Aufgabe eigentlich ist.

Wie trennt man das am besten in Packages?
* org.torax.project.persistence (hier z.B. Sonne.java, Mond.java, Sterne.java)
* org.torax.project.DAO (SonneDAO.java, MondDAO.java, SterneDAO.lava)
* ..?

Wo pack ich am besten die JUnit Test Klassen hin?

Wahrscheinlich steh ich einfach nur etwas auf dem Schlauch, habe aber leider kein Beispielprojekt an dem ich mich orientieren kann.

Für Hinweise und oder Links wäre ich sehr dankbar.

Torax
 
Aaaalso. Grunsätzlich ist das enicht verkehrt was du da machst - allerdings sind ein paar kleine Denkfehler drin. Zum anderen ist dasauch immer etwas Geschmacksfrage.

Zum kleinen Fehlerchen. Das was du als Persistenzklassen bezeichnest sind eigenlich Entities die den Kern deines Domänenmodells ausmachen. Daher würde ich das package eher foo.bar.domain nennen. DAO hast du auch etwas falsch verstanden (oder vielleicht nur komisch aufgeschrieben?). Die sind Repositories im Fowlerschen Sinne - sprich Laden und Speichern der Entities. D.h. diese kapseln Daten(bank)zugriffslogik, sollten allerdings keine Logik beinhalten.

Nun kommen wir zum Thema Geschmackssache ;). Klassischerweise hat man eine Präsentationsschicht (JSF, SpringMVC, Struts whatever) die auf einen Servicelayer zugreift. In dem bildet man nette Komponenten: CustomerManager, AccountManager usw. Dieser layer orchestriert quasi Aufrufe an DAOs, Domänenobjekte usw. Hier steckt auch im Allgemeinen viel Geschäftslogik. Wichtig ist dabei auch, dass das Interface des Layers (also auch der einzelnen Teilkomponenten) stark an den Clientbedürfnissen ausgerichtet ist und eher Fachlich orientiert sein sollte. Sowas wie createAccount ist ganz nett z.B. DAOs haben schon recht technische Interfaces: findByUserId. Bei einfachen Webanwendungen macht der Servicelayer eigentlich nix anderes, als Aufrufe an DAOs weiterzureichen. Allerdings abstrahiert er dabei natürlich technische Details vom Client weg. Achso, die Domänenobjekte kannst du dann in diesem Modell durch alle Layer durchreichen, solang die Abhängigkeit unidirektional bleibt (also Presentation -> Domain, Service -> Domain, DAO -> Domain).

Nachteil dieses Modells ist, dass die Domainklassen recht häufig wenig Verhalten aufweisen und dass geschäftslogik, die eigentlich mit Domainklassen verbunden sein sollte in Services liegt. Dem Arbeitet der Ansatz des Domain Driven Design etwas entgegen, dessen wirkliche praktische Umsetzung (Domänenklassen haben Zugriff auf DAOs z.B.) doch etwas advanced ist, bzw. noch in den Kinderschuhen steckt.

Zum packageging: ich bin auf jeden Fall Freund des "Fachlichkeit vor Technik". D.h.

foo.bar.account.dao
foo.bar.account.domain
foo.bar.account.service
foo.bar.account.view

foo.bar.customer.dao
foo.bar.customer.domain
foo.bar.customer.service
foo.bar.customer.view

Das hat den Vorteil, dass man fachliche einheitn unabhängig voneinander deployen kann. In diesen packages liegen dann eigentlich immer die Interfaces + evtl. exposedte Exceptions. Unter .dao und .service mach ich meist noch ein impl package und plazier da die Implementierungen, einfach um die Packagedependencies zu kappen.

Hilfreich für eine Kontrolle der Abhängigkeiten sind Tools wie SonarJ oder JDepend. Damit kannst du überwachen, dass du keine Zyklen in die Architektu bekommst usw.

JUnittestklassen hab ich meist in einem separaten src-Folder, darunter allerdings die packages genauso wie bei den Produktivsourcen. Auch aus Deploymentgründen.

Reicht das als Anfangstipp? Wenn nicht... weiter fragen ;)

REINHAUN!
 
Hallo,


Da ich meine Anwendungen immer so entwerfe, dass die einzelnen Schichten immer möglichst lose gekoppelt sind und man so einfach weitere Schichten austauschen / parallel hinzufügen kann (Statt web Präsentationsschicht, ein Eclipse Rich Client) achte ich immer darauf das die einzelnen Schichten möglichst keine zyklischen Abhängigkeiten zueinander haben.

Ich würde das dann so machen:
Präsentationsschicht (JSF, JSP; Servlets) de.tutorials.core.presentation... (hier Model View Controller, oder ein anderes Pattern für den Presentation Layer (MVP, PM))
Geschäftslogikschicht (BusinessServices, Domain Classes) -> de.tutorials.core.services, de.tutorials.core.domain
Persistenzschicht (Legacy System ResourceConnectors,DAO's / Repositories, DataAssembler (JDBC/Spring JDBC basiert zur Massendatenverarbeitung) ) -> de.tutorials.core.persistence, de.tutorials.core.persistence.dao

(de.tutorials.core ist in meinem Beispiel das Basis package des OSGi Bundles)

Für alle BusinessServices,DAO / Repositories 's, DataAssembler, Connectors gibts öffentliche interfaces. Diese liegen im direkten package:
Bsp:
de.tutorials.core.persistence.dao.IGenericDAO

Implementierungen verstecke ich immer in einem darunterliegenden Internal package:
Bsp:
de.tutorials.core.persistence.dao.internal.GenericHibernateDAO

So habe ich die möglichkeit den Klassenzugriff innerhalb der einzelnen Schichten besser zu steuern (per AspectJ, OSGi Exported / Imported Packages, Dynamic Import, etc.).

Geschäftslogik hätte ich in meinen Services und / oder in meinen Domain Classes. Die mapping Informationen (hbm.xml) würde
ich auch in dem Domain package ablegen. Konfigurieren würde ich meine Services / Domain Objects über Spring, mit AspectJ und

@Configurable (
http://www.tutorials.de/forum/java/...-eclipse-rcp-view-mit-aspectj-und-spring.html)

Ansonsten verwende ich für große Webanwendungen eine "Serverside Eclipse" Komponentenmodell. Komponenten sind in diesem Sinne
dann meine OSGi Bundles, die dann Services, Hibernate Mappings, Spring application contexts über extension points und extensions in das Gesamtsystem einbringen. Damit hat man dann einen hochflexiblen Erweiterungsmechanismus :) Wenn man dann noch die "Erweiterbarkeit" als AspectJ aspect definiert bleibt die Anwendungslogik auf komplett frei von irgendwelchen Plugin-Mechanismus-Interna... ;-)
Siehe auch:
http://www.tutorials.de/forum/java/...ate-equinox-osgi-eclipse-extensionpoints.html
http://www.tutorials.de/forum/java/...-von-osgi-services-auf-basis-von-equinox.html
http://www.tutorials.de/forum/java/265296-extension-points-eclipse-fleh.html

Weiterhin würde ich nicht notwendigerweise für jedes Domain Object ein eigenes DAO schreiben sondern ein generisches, von dem
ich dann in Spezialfällen ableite.

Beispiel:
Java:
/**
 * 
 */
package de.tutorials.framework.persistence.dao;

import java.io.Serializable;
import java.util.List;

/**
 * @author Thomas.Darimont
 * 
 */
public interface IGenericDAO {
    /**
     * 
     * @param entity
     * @return
     */
    <TKey extends Serializable> TKey makePersistent(Object entity);

    /**
     * 
     * @param entity
     */
    void makeTransient(Object entity);

    /**
     * 
     * @param key
     * @return
     */
    <TEntity, TKey extends Serializable> TEntity getBy(Class<TEntity> entityClass, TKey key);

    /**
     * 
     * @return
     */
    <TEntity> List<TEntity> findAll(Class<TEntity> entityClass);

    /**
     * 
     * @param attributeName
     * @param attributeValue
     * @return
     */
    <TEntity> List<TEntity> findByAttribute(Class<TEntity> entityClass,
            String attributeName, Object attributeValue);

    /**
     * 
     * @param attributeNames
     * @param attributeValues
     * @return
     */
    <TEntity> List<TEntity> findByAttributes(Class<TEntity> entityClass,
            String[] attributeNames, Object[] attributeValues);
}

Implementierung:
Java:
/**
 * 
 */
package de.tutorials.framework.persistence.dao.internal;

import java.io.Serializable;
import java.sql.SQLException;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.Property;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import de.tutorials.framework.persistence.dao.IGenericDAO;

/**
 * @author Thomas.Darimont
 * 
 */
public class GenericHibernateDAO extends HibernateDaoSupport implements
        IGenericDAO {

    @SuppressWarnings("unchecked")
    public <TEntity> List<TEntity> findAll(final Class<TEntity> entityClass) {
        return getHibernateTemplate().loadAll(entityClass);
    }

    @SuppressWarnings("unchecked")
    public <TEntity> List<TEntity> findByAttribute(
            final Class<TEntity> entityClass, final String attributeName,
            final Object attributeValue) {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session)
                    throws HibernateException, SQLException {
                return session.createCriteria(entityClass).add(
                        Property.forName(attributeName).eq(attributeValue))
                        .list();
            }
        });
    }

    @SuppressWarnings("unchecked")
    public <TEntity> List<TEntity> findByAttributes(
            final Class<TEntity> entityClass, final String[] attributeNames,
            final Object[] attributeValues) {
        return getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session session)
                    throws HibernateException, SQLException {
                Criteria criteria = session.createCriteria(entityClass);
                for (int i = 0; i < attributeNames.length; i++) {
                    criteria.add(Property.forName(attributeNames[i]).eq(
                            attributeValues[i]));
                }
                return criteria.list();
            }
        });
    }

    @SuppressWarnings("unchecked")
    public <TEntity, TKey extends Serializable> TEntity getBy(
            Class<TEntity> entityClass, TKey key) {
        return (TEntity) getHibernateTemplate().get(entityClass, key);
    }

    @SuppressWarnings("unchecked")
    public <TKey extends Serializable> TKey makePersistent(Object entity) {
        return (TKey) getHibernateTemplate().merge(entity);
    }

    public void makeTransient(Object entity) {
        getHibernateTemplate().delete(entity);
    }
}

verwendet wird das dann beispielsweise so:
Java:
        IGenericDAO genericDao = new GenericHibernateDAO(); // normally injected via Spring...

        List<Bubu> bubus = genericDao.findAll(Bubu.class);
        List<Gaga> gagas = genericDao.findAll(Gaga.class);

        List<Bubu> bubus0 = genericDao.findByAttribute(Bubu.class, "aaa", 4711);

        List<Bubu> bubus1 = genericDao.findByAttributes(Bubu.class,
                new String[] { "aaa", "bbb" }, new Object[] { 4711, 3421 });
        
        Long id = genericDao.makePersistent(new Bubu());
        
        Bubu bubu = genericDao.getBy(Bubu.class, 1L);

Ansonsten nutze ich bei sowas exzessiv die Möglichkeiten des Zusammenspiels von Technologien wie Spring, AspectJ, Hibernate, iBatis,
Terracotta, OSGi, Equinox etc.

Gruß Tom
 
Danke Euch beiden für Eure ausführlichen Erleuterungen. Insbesondere die Vorschläge von MSProductions zielen so in die Richtung, die ich micr auch vorgestellt hab und räumen auch einige Unklarheiten meinerseits aus.

Das soll nicht heißen, das ich deine Vorschläge schlecht finde Thomas, ich denke aber ich mach erstmal Schritt 1 bevor ich Schritt 2 mache. Wenn ich zuviele neue Sachen gleichzeitig versuche, lande ich definitiv auf der Nase. Ich werde aber später, mit mehr Erfahrung (hoffentlich) nochmal auf den Thread zurückkommen und dann sicherlich Hinweise daraus entnehmen können.

Fachliche / Technische Trennung: guter Hinweis, ich automatisch angefangen das technologisch zu trennen. Bin mir nocht ganz sicher, was mir da besser gefällt.

Werde nun erstmal anfagen ein bischen Refactoring zu betreiben. Ich komm bestimmt mit weiteren Fragen zurück ;)

Gruß Torax
 
Zurück