首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

8.4  常见的架构设计策略

目前流行的轻量级J2EE应用的架构比较一致,采用的技术也比较一致,通常使用Spring作为核心,向上整合MVC框架,向下整合ORM框架。使用Spring的IOC容器来管理各组件之间的依赖关系时,Spring的声明事务将负责业务逻辑层对象方法的事务管理。

但在固定的技术组合上,依然可能存在小的变化。下面依次讨论可能存在的架构策略。

8.4.1  贫血模式

贫血模式是最常用的设计架构,也是最容易理解的架构。为了让读者通过本书顺利进入轻量级J2EE企业应用开发,本书的第9章及第10章的范例都将采用这种简单的架构模式。

所谓贫血,指Domain Object只是单纯的数据类,不包含业务逻辑方法,即每个Domain Object类只包含基本的setter和getter方法。所有的业务逻辑都由业务逻辑组件实现,这种Domain Object就是所谓的贫血的Domain Object,采用这种Domain Object的架构即所谓的贫血模式。

下面以第9章的消息发布系统的部分代码为例,介绍贫血模式。

在贫血模式里,所有的Domain Object只是单纯的数据类,只包含每个属性的setter和getter方法,如下是两个持久化类。

第一个Domain Object是消息,其代码如下:

public class News extends BaseObject implements Serializable

{

    //主键

    private Long id;

    //消息标题

    private String title;

    //消息内容

    private String content;

    //消息的发布时间

    private Date postDate;

    //消息的最后修改时间

    private Date lastModifyDate;

    //消息所属分类

    private Category category;

    //消息对应的消息回复

    private Set newsReviews;

    //无参数的构造器

    public News() {

    }

    //消息回复对应的getter方法

    public Set getNewsReviews() {

        return newsReviews;

    }

    //消息回复对应的setter方法

    public void setNewsReviews(Set newsReviews) {

        this.newsReviews = newsReviews;

    }

    //消息分类对应的getter方法

    public Category getCategory() {

        return category;

    }

    //消息分类对应的setter方法

    public void setCategory(Category category) {

        this.category = category;

    }

    //消息最后修改时间的getter方法

    public Date getLastModifyDate() {

        return lastModifyDate;

    }

    //消息最后修改时间的setter方法

    public void setLastModifyDate(Date lastModifyDate) {

        this.lastModifyDate = lastModifyDate;

    }

    //消息发布时间的getter方法

    public Date getPostDate() {

        return postDate;

    }

    //消息发布时间的setter方法

    public void setPostDate(Date postDate) {

        this.postDate = postDate;

    }

    //消息内容对应的getter方法

    public String getContent() {

        return content;

    }

    //消息发布者对应的setter方法

    public void setContent(String content) {

        this.content = content;

    }

    //消息主键对应的getter方法

    public Long getId() {

        return id;

    }

    //消息主键对应的setter方法

    public void setId(Long id) {

        this.id = id;

    }

    //消息标题对应的getter方法

    public String getTitle() {

        return title;

    }

    //消息标题对应的setter方法

    public void setTitle(String title) {

        this.title = title;

    }

    //Domain Object重写equals方法

    public boolean equals(Object object) {

        if (!(object instanceof News)) {

            return false;

        }

        News rhs = (News) object;

        return this.poster.equals(rhs.getPoster())

                && this.postDate.equals(rhs.getPostDate());

    }

    //Domain Object重写的hashCode方法

    public int hashCode() {

        return this.poster.hashCode() + this.postDate.hashCode();

    }

    //Domain Object重写toString方法

    public String toString() {

    return new ToStringBuilder(this).append("id", this.id).append("title",

            this.title).append("postDate", this.postDate).append("content",

            this.content).append("lastModifyDate", this.lastModifyDate)

                .append("poster", this.poster)

                .append("category", this.category).append("newsReviews",

                        this.newsReviews).toString();

    }

}

第二个Domain Object是消息对应的回复,其代码如下:

public class NewsReview extends BaseObject

{

    //消息回复的主键

    private Long id;

    //消息回复的内容

    private String content;

    //消息回复的回复时间

    private Date postDate;

    //回复的最后修改时间

    private Date lastModifyDate;

    //回复的对应的消息

    private News news;

    //消息回复的构造器

    public NewsReview() {

    }

    //回复内容对应的getter方法

    public String getContent() {

        return content;

    }

    //回复内容对应的setter方法

    public void setContent(String content) {

        this.content = content;

    }

    //回复主键对应的setter方法

    public Long getId() {

        return id;

    }

    //回复主键对应的setter方法

    public void setId(Long id) {

        this.id = id;

    }

    //回复的最后修改时间对应的getter方法

    public Date getLastModifyDate() {

        return lastModifyDate;

    }

    //回复的最后修改时间对应的setter方法

    public void setLastModifyDate(Date lastModifyDate) {

        this.lastModifyDate = lastModifyDate;

    }

    //回复对应的消息的getter方法

    public News getNews() {

        return news;

    }

    //回复对应的消息的setter方法

    public void setNews(News news) {

        this.news = news;

    }

    //回复发布时间的getter方法

    public Date getPostDate() {

        return postDate;

    }

    //回复发布时间的setter方法

    public void setPostDate(Date postDate) {

        this.postDate = postDate;

    }

    //Domain Object重写的equals方法

    public boolean equals(Object object) {

        if (!(object instanceof NewsReview)) {

            return false;

        }

        NewsReview rhs = (NewsReview) object;

        return this.poster.equals(rhs.getPoster()) &&

this.postDate.equals(rhs.getPostDate());

        /*return new EqualsBuilder().append(this.news, rhs.news).append(

            this.content, rhs.content).append(this.postDate, rhs.postDate)

                .append(this.lastModifyDate, rhs.lastModifyDate).append(

                        this.id, rhs.id).append(this.poster, rhs.poster)

                .isEquals();

*/

    }

    //Domain Object对应的hashCode方法

    public int hashCode() {

        return this.poster.hashCode() + this.postDate.hashCode();

        /*return new HashCodeBuilder(-1152635115, 884310249).append(this.news)

                .append(this.content).append(this.postDate).append(

                        this.lastModifyDate).append(this.id)

                .append(this.poster).toHashCode();

*/

    }

    //Domain Object对应的toString方法

    public String toString() {

        return new ToStringBuilder(this).append("id", this.id).append(

            "postDate", this.postDate).append("lastModifyDate",

                this.lastModifyDate).append("content", this.content).append(

                "poster", this.poster).append("news", this.news).toString();

    }

}

从上面贫血模式的Domain Object可看出,其类代码中只有setter和getter方法,这种Domain Object只是单纯的数据体,类似于C的数据结构。虽然它的名字是Domain Object,却没有包含任何业务对象的相关方法。Martin Fowler认为,这是一种不健康的建模方式,Domain Model既然代表了业务对象,就应该包含相关的业务方法。从语言的角度上来说,Domain Model在这里被映射为Java对象(一般都是ORM), Java对象应该是数据与动作的集合,贫血模型相当于抛弃了Java面向对象的性质。

Rod Johnson和Martin Fowler一致认为:贫血的Domain Object实际上以数据结构代替了对象。他们认为Domain Object应该是个完整的Java 对象, 既包含基本的数据,也包含了操作数据相应的业务逻辑方法。

下面是NewsDAOHibernate的源代码,该DAO对象用于操作News对象:

//NewsDAOHibernate继承HibernateDaoSupport,实现NewsDAO接口

public class NewsDAOHibernate extends HibernateDaoSupport implements NewsDAO

{

    //根据主键加载消息

    public News getNews(Long id)

    {

          News news = (News) getHibernateTemplate().get(News.class, id);

          if (news == null) {

               throw new ObjectRetrievalFailureException(News.class, id);  

          }

        return news;

    }

    //保存新的消息

    public void saveNews(News news) {

          getHibernateTemplate().saveOrUpdate(news);

    }

    //根据主键删除消息

    public void removeNews(Long id)

    {

          getHibernateTemplate().delete(getNews(id));

    }

    //查找全部的消息

    public List findAll()

    {

        getHibernateTemplate().find("from News"));

    }

}

既然DAO对象完成具体的持久化操作,因此基本的CRUD操作都应该在DAO对象中实现。但DAO对象应该包含多少个查询方法,并不是确定的。因此,根据业务逻辑的不同需要, 不同的DAO对象可能有数量不等的查询方法。

对于现实中News,应该包含一个业务方法(addNewsReviews方法)。在贫血模式下,News类的代码并没有包含该业务方法,只是将该业务方法放到业务逻辑对象中实现,下面是业务逻辑对象实现addNewsReviews的代码:

public class FacadeManagerImpl implements FacadeManager

{

    //业务逻辑对象依赖的DAO对象

    private CategoryDAO categoryDAO;

    private NewsDAO newsDAO;

    private NewsReviewDAO newsReviewDAO;

    private UserDAO userDAO;

    //...此处还应该增加依赖注入DAO对象必需的setter方法

    //...此处还应该增加其他业务逻辑方法

    //下面是增加新闻回复的业务方法

    public NewsReview addNewsReview(Long newsId , String content)

    {

        //根据新闻id加载新闻

          News news = newsDao.getNews(newsId);

        //以默认构造器创建新闻回复

          NewsReview review = new NewsReview();

        //设置新闻与新闻回复之间的关联

          review.setNews(news);

        //设置新闻回复的内容

          review.setContent(content);

        //设置回复的回复时间

          review.setPostDate(new Date());

        //设置新闻回复的最后修改时间

          review.setLastModifyDate(new Date());

        //保存回复

          newsReviewDAO.saveNewsReview(review);

          return review;

    }

}

在贫血模式下,业务逻辑对象正面封装了全部的业务逻辑方法,Web层仅与业务逻辑组件交互即可,无须访问底层的DAO对象。Spring的声明式事务管理将负责业务逻辑对象方法的事务性。

在贫血模式下,其分层非常清晰。Domain Object 并不具备领域对象的业务逻辑功能,仅仅是ORM框架持久化所需的POJO,仅是数据载体。贫血模型容易理解,开发便捷,但严重背离了面向对象的设计思想,所有的Domain Object并不是完整的Java对象。

总结起来,贫血模式存在如下缺点:

— 项目需要书写大量的贫血类,当然也可以借助某些工具自动生成。

—  Domain Object的业务逻辑得不到体现。由于业务逻辑对象的复杂度大大增加,许多不应该由业务逻辑对象实现的业务逻辑方法,完全由业务逻辑对象实现,从而使业务逻辑对象的实现类变得相当臃肿。

贫血模式的优点是:开发简单、分层清晰、架构明晰且不易混淆;所有的依赖都是单向依赖,解耦优秀。适合于初学者及对架构把握不十分清晰的开发团队。

8.4.2  Rich Domain Object模式

在这种模式下,Domain Object不再是单纯的数据载体,Domain Object包含了相关的业务逻辑方法。例如News类包含了addNewsView 方法等。

下面是修改后的News类的源代码:

public class News extends BaseObject

{

    //此处省略了其他的属性

    //此处省略了属性对应的setter和getter方法

    //增加新闻回复的业务逻辑方法

    public NewsReview addNewsReview(String content)

    {

        //以默认构造器创建新闻回复实例

          NewsReview review = new NewsReview();

        //设置回复内容

          review.setContent(content);

        //设置回复的发布日期

          review.setPostDate(new Date());

        //设置回复的最后修改日期

          review.setLastModifyDate(new Date());

        //设置回复与消息的关联

          review.setNews(this);

          return review;

    }

    //此处省略了重写的hashCode,equals等方法

}

在上面的Domain Object中,包含了相应的业务逻辑方法,这是一种更完备的建模方法。

注意:不要在Domain Object中对消息回复完成持久化,如需完成持久化,必须调用DAO组件;一旦调用DAO组件,将造成DAO对象和Domain Object的双向依赖;另外,Domain Object中的业务逻辑方法还需要在业务逻辑组件中代理,才能真正实现持久化。

在上面的业务逻辑方法中,并没有进行持久化。如果抛开DAO层,这种Domain Object也可以独立测试,只是没有进行持久化。

DAO对象是变化最小的对象,它们都是进行基本的CRUD操作,在两种模型下的DAO对象没有变化。

另外还需要对业务逻辑对象进行改写,虽然Domain Object包含了基本业务逻辑方法,但业务逻辑对象还需代理这些方法,修改后业务逻辑对象的代码如下:

public class FacadeManagerImpl implements FacadeManager

{

    //业务逻辑对象依赖的DAO对象

    private CategoryDAO categoryDAO;

    private NewsDAO newsDAO;

    private NewsReviewDAO newsReviewDAO;

    private UserDAO userDAO;

    //...此处还应该增加依赖注入DAO对象必需的setter方法

    //...此处还应该增加其他业务逻辑方法

    //下面是增加新闻回复的业务方法

    public NewsReview addNewsReview(Long newsId , String content)

    {

        //根据新闻id加载新闻

          News news = newsDao.getNews(newsId);

        //通过News的业务方法添加回复

        NewsReview review = news.addNewsReview(content);

        //此处必须显示持久化消息回复

          newsReviewDAO.saveNewsReview(review);

          return review;

    }

}

在Rich Domain Object的模型中,addNewsReview方法将放在News类中实现,而业务逻辑对象仅对该方法进行简单的代理,执行必要的持久化操作。

在这里存在一个问题:业务逻辑方法很多,哪些业务逻辑方法应该放在Domain Object对象中实现,而哪些业务逻辑方法完全由业务逻辑对象实现呢?Rod Johnson认为,可重用度高,与Domain Object密切相关的业务方法应放在Domain Object对象中实现。

业务逻辑方法是否需要由Domain Object实现的标准,从一定程序上说明了采用Rich Domain Object模型的原因。由于某些业务方法只是专一地属于某个Domain Object,因此将这些方法由Domain Object实现,能提供更好的软件复用,能更好地体现面向对象的封装性。

Rich Domain Object模型的各组件之间关系大致如图8.2所示(贫血模式的组件关系图与此类似)。

图8.2  Rich Domain Object的组件关系图

这种Rich Domain Object模型主要的问题是业务逻辑对象比较复杂,由于业务逻辑对象需要正面封装所有的DAO对象,因而难免有大量的DAO方法(基本的CRUD)需要业务逻辑对象封装。业务逻辑对象封装DAO方法主要基于如下考虑:

—  DAO对象不应该暴露为Web层。

—  DAO对象的DAO方法必须增加事务控制代码,而事务控制则放在业务逻辑层完成。

为了简化业务逻辑对象的开发,Rich Domain Object模型可以有如下两个方向的改变:

— 合并业务逻辑对象与DAO对象。

— 合并业务逻辑对象和Domain Object。

1.合并业务逻辑对象与DAO对象

在这种模型下DAO对象不仅包含了各种CRUD方法,而且还包含各种业务逻辑方法。此时的DAO对象,已经完成了业务逻辑对象所有任务,变成了DAO对象和业务逻辑对象混合体。此时,业务逻辑对象依赖Domain Object,既提供基本的CRUD方法,也提供相应的业务逻辑方法。

下面是这种模式的代码(Domain Object的实现与前面的Rich Domain Object模式一样,此处不再给出):

// NewsServiceHibernate继承HibernateDaoSupport,实现NewsService接口

public class NewsServiceHibernate extends HibernateDaoSupport

implements NewsService

{

    //此处添加NewsService对象依赖的DAO对象,以及对应的setter方法

    //根据主键加载消息

    public News getNews(Long id)

    {

          News news = (News) getHibernateTemplate().get(News.class, id);

          if (news == null) {

               throw new ObjectRetrievalFailureException(News.class, id);  

          }

        return news;

    }

    //保存新的消息

    public void saveNews(News news) {

          getHibernateTemplate().saveOrUpdate(news);

    }

    //根据主键删除消息

    public void removeNews(Long id)

    {

          getHibernateTemplate().delete(getNews(id));

    }

    //查找全部的消息

    public List findAll()

    {

        getHibernateTemplate().find("from News"));

    }

    //下面是增加新闻回复的业务方法

    public NewsReview addNewsReview(Long newsId , String content)

    {

        //根据新闻id加载新闻

          News news = newsDao.getNews(newsId);

        //通过News的业务方法添加回复

        NewsReview review = news.addNewsReview(content);

        //此处必须显示持久化消息回复

          newsReviewService.saveNewsReview(review);

          return review;

    }

}

正如上面见到的,DAO对象和业务逻辑对象之间容易形成交叉依赖(可能某个业务逻辑方法的实现,必须依赖于原来的DAO对象)。当DAO对象被取消后,业务逻辑对象取代了DAO对象,因此变成了一个业务逻辑对象依赖多个业务逻辑对象。而每个业务逻辑对象都可能包含需要多个DAO对象协作的业务方法,从而导致业务逻辑对象之间的交叉依赖。

业务逻辑对象和DAO对象合并后的组件关系如图8.3所示。

图8.3  合并DAO对象和业务逻辑对象

这种模型也导致了DAO方法和业务逻辑方法混合在一起,显得职责不够单一,软件分层结构不够清晰。此外,使业务逻辑对象之间交叉依赖,容易产生混乱,未能做到彻底的简化。

2.合并业务逻辑对象和Domain Object

在这种模型下,所有的业务逻辑都应该被放在Domain Object里面,而此时的业务逻辑层不再是传统的业务逻辑层,它仅仅封装了事务和少量逻辑,完全无需DAO对象的支持。而Domain Object依赖于DAO对象执行持久化操作,此处Domain Object和DAO对象形成双向依赖,这种设计在某些地方也被称为充血模式,但有时会带来相当大的危险。

在这种设计模式下,几乎不再需要业务逻辑层,而Domain Object则依赖DAO对象完成持久化操作,下面是在这种模式下的News类代码:

public class News extends BaseObject

{

    //此处省略了其他的属性。

    //此处省略了属性对应的setter和getter方法

    //增加新闻回复的业务逻辑方法

    public NewsReview addNewsReview(String content)

    {

        //以默认构造器创建新闻回复实例

          NewsReview review = new NewsReview();

        //设置回复内容

          review.setContent(content);

        //设置回复的发布日期

          review.setPostDate(new Date());

        //设置回复的最后修改日期

          review.setLastModifyDate(new Date());

        //设置回复与消息的关联

          review.setNews(this);

        //直接调用newsReviewsDao完成消息回复的持久化。

        newsReviewsDao.save(review);

          return review;

    }

    //此处省略了重写的hashCode,equals等方法

}

从上面代码中可以看到,由于Domain Object必须使用DAO对象完成持久化,因此Domain Object必须接收IOC容器的注入,而Domain Object获取容器注入的DAO对象,通过DAO对象完成持久化操作。

合并业务逻辑对象和Domain Object后各组件的关系如图8.4所示。

这种模型的优点是:业务逻辑对象非常简单,只提供简单的事务操作,业务逻辑对象无须依赖于DAO对象。

但这种模型的缺点也是非常明显的:

—  DAO对象和Domain Object形成了双向依赖,其复杂的双向依赖会导致很多潜在的问题。

— 业务逻辑层和Domain层的逻辑混淆不清,在实际项目中,极容易导致架构混乱。

— 由于使用业务逻辑对象提供事务封装特性,业务逻辑层必须对所有的Domain Object的逻辑提供相应的事务封装,因此业务逻辑对象必须重新定义Domain Object实现的业务逻辑,其工作相当烦琐。

图8.4  合并业务逻辑组件和Doamin Object

8.4.3  抛弃业务逻辑层

在Rich Domain Object模型的各种变化中,虽然努力简化业务逻辑对象,但业务逻辑对象依然存在,依然使用业务逻辑对象正面封装所有的业务请求。下面介绍更彻底的简化即,彻底放弃业务逻辑层。

抛弃业务逻辑层也有两种形式:

—  Domain Object彻底取代业务逻辑对象。

— 由控制器直接调用DAO对象。

1.Domain Object完全取代业务逻辑对象

这种设计模式是充血模式更加激进的演化。由于在充血模式中业务逻辑对象的作用仅仅只提供事务封装,业务逻辑对象存在的必要性不是很大,因此考虑对Domain Object的业务逻辑方法增加事务管理,而Web层的控制器则直接依赖于Domain Object。

这种模型更加简化,使Domain Object与DAO对象形成双向依赖,而Web层的控制器直接调用Domain Object的业务逻辑方法。这种模型在有些地方也被称为胀血模式。

这种模型的优点是:分层少,代码实现简单。

但这种模型的缺点也很明显:

— 业务逻辑对象的所有业务逻辑都将在Domain Object中实现,势必引起Domain Object的混乱。

—  Domain Object必须向Web层直接暴露,可能导致意想不到的问题。

这种模型与充血模式的缺点相同:Domain Object必须配置在Spring容器中,接受Spring容器的依赖注入。

在这种架构模型下,Domain Object相当不稳定。如果业务逻辑需要改变,Domain Object也需要发生改变,而DAO对象与Domain Object形成双向依赖,这将导致从底层的Domain Object和DAO对象的修改,使这种架构模式的分层完全失去意义。各层之间以强耦合方式组合在一起,各层对象互相依赖,牵一发而动全身,几乎是最差的一种策略。

2.控制器完成业务逻辑

在这种模型里,控制器直接调用DAO对象的CRUD方法,通过调用基本的CRUD方法,完成对应的业务逻辑方法。这种模型下,业务逻辑对象的功能由控制器完成。事务则推迟到控制器中完成,因此对控制器的execute方法增加事务控制即可。

对于基本的CRUD操作,控制器可直接调用DAO对象的方法,省略了业务逻辑对象的封装,这就是这种模型的最大优势。对于业务逻辑简单(当业务逻辑只是大量的CRUD操作时)的项目,使用这种模型也未尝不是一种好的选择。

但这种模型将导致控制变得臃肿,因为每个控制器除了包含原有的execute方法之外,还必须包含所需要的业务逻辑方法的实现。极大地省略了业务逻辑层的开发,避免了业务逻辑对象不得不大量封装基本的CRUD方法的弊端。

这种模型也有其缺点:

— 因为没有业务逻辑层,对于那些需要多个DAO参与的复杂业务逻辑,在控制器中必须重复实现,其效率低,也不利于软件重用。

—  Web层的功能不再清晰,人为复杂化。Web层不仅负责实现控制器逻辑,还需要完成业务逻辑的实现,因此必须精确控制何时调用DAO方法控制持久化。

— 扩大了事务的影响范围。大部分情况下,只有业务逻辑方法需要增加事务控制,而execute方法无须增加事务控制。但如果execute方法直接调用了DAO对象的CRUD方法,则会导致这些方法不在事务环境下执行。为了让数据库访问都在事务环境下进行,因此不得不将事务范围扩大到整个execute方法。

本章小结

本章首先介绍了笔者在架构设计方面一些经验,从企业应用开发面临的困难讲起,并讲解了面对这些困难时应该采用何种应对策略。

其次介绍了常用的代理模式的使用,并深入介绍了由此衍生出来的Spring AOP框架。

最后重点介绍了贫血模型、Rich Domain Object模型、以及几种简化的模型,并分别分析了几种模型各自的优缺点。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论