世界杯之歌

DDD(领域驱动设计)系列主题:失血模型,贫血模型,充血模型和胀血模型详细解读和代码案例说明!

目录

失血模型,贫血模型,充血模型和胀血模型定义及优点和缺点

失血模型

贫血模型

充血模型

胀血模型

失血模型,贫血模型,充血和胀血代码样例

失血模型代码样例

贫血模型代码样例

充血模型代码样例

DDD分层架构模型及基于贫血模型的微服务代码模型

DDD 分层架构模型

基于贫血模型的微服务代码模型

综合评价

失血模型,贫血模型,充血模型和胀血模型定义及优点和缺点

领域模型分为4大类:失血模型、贫血模型、充血模型、胀血模型。这类理论都是由软件设计领域的大牛(如Martin Fowler)提出来的,有其背景和原因。

想要理解这几个分类,先要知道“血”指的是Domain Object的Domain层内容。

失血模型

Domain Object(领域对象)模型仅仅包含对象属性的定义和操作对象属性的getter/setter方法,所有的业务逻辑完全由Business Logic层(业务逻辑层)中的服务类来完成。这种类在java中叫POJO,在.NET中叫POCO。

优点

领域对象结构简单

缺点

肿胀的业务服务代码逻辑,难于理解和维护

无法良好的应对复杂业务逻辑和场景

贫血模型

Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了对象的行为(例如:就像一个完整的人,具有一些属性如姓名、性别、年龄等,还具有一些能力,如走路、吃饭、恋爱等,这样才是一个完整的对象), 但不包含依赖Dao层(持久层)的业务逻辑。这部分依赖于Dao层的业务逻辑将会放到Business Logic层(业务逻辑层)中的服务类来实现,组合逻辑也由服务类负责。可以看出,贫血模型中的领域对象是不依赖于持久层的。代码架构层次结构是: Client-> Business Facade Service -> Business Logic Service(Business Logic Service是依赖Domain Object的行为) -> Data Access Service

优点

层次结构清楚,各层之间单向依赖

对于只有少量业务逻辑的应用来说,使用起来非常自然

开发迅速,易于理解

缺点

无法良好的应对非常复杂逻辑和场景

充血模型

Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了大多数相关的业务逻辑,也包含了依赖于持久层的业务逻辑, Business Logic层是很薄的一层,仅仅简单封装少量业务逻辑以及控制事务、权限逻辑等,不和DAO层打交道。所以,使用充血模型的领域对象是依赖于持久层的。代码架构层次结构是: Client-> Business Facade Service -> Business Logic Service -> Domain Object -> Data Access Service

优点

更加符合OO的原则 Business Logic层很薄,符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重,只充当Facade的角色,不和DAO打交道。

缺点

什么样的逻辑应该放在Domain Object中,什么样的业务逻辑应该放在Business Logic中,这是很含糊的。即使划分好了业务逻辑,由于分散在Business Logic和Domain Object层中,不能更好的分模块开发。熟悉业务逻辑的开发人员需要渗透到Domain Logic中去,而在Domian Logic又包含了持久化,对于开发者来说这十分混乱。 其次,因为Business Logic要控制事务并且为上层提供一个统一的服务调用入口点,它就必须把在Domain Logic里实现的业务逻辑全部重新包装一遍,完全属于重复劳动。

胀血模型

Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了所有相关的的业务逻辑,也包含了不想关的其它应用逻辑(如授权、事务等)。胀血模型取消了Business Logic层(业务逻辑层),只剩下Domain Object和DAO两层,在Domain Object的Domain Logic上面封装事务,授权逻辑等。

优点

简化了代码分层结构也算符合面向对象设计

缺点

取消了Business Logic层(业务逻辑层),在Domain Object的Domain Logic上面封装事务,授权等很多本不应该属于领域对象的逻辑,使业务逻辑再次进行到混论的状态,引起了Domain Object模型的不稳定代码理解和维护性差

失血模型,贫血模型,充血和胀血代码样例

失血模型代码样例

下面用举一个具体的代码来说明:

一个实体类叫做Item,指的是一个拍卖项目 一个DAO接口类叫做ItemDao一个DAO接口实现类叫做ItemDaoHibernateImpl 一个业务逻辑类叫做ItemManager(或者叫做ItemService)

Java代码:

public class Item implements Serializable {

private Long id = null;

private int version;

private String name;

private User seller;

private String description;

private MonetaryAmount initialPrice;

private MonetaryAmount reservePrice;

private Date startDate;

private Date endDate;

private Set categorizedItems = new HashSet();

private Collection bids = new ArrayList();

private Bid successfulBid;

private ItemState state;

private User approvedBy;

private Date approvalDatetime;

private Date created = new Date();

//getter/setter方法省略不写,避免篇幅太长

}

Java代码:

public interface ItemDao {

public Item getItemById(Long id);

public Collection findAll();

public void updateItem(Item item);

}

ItemDao定义持久化操作的接口,用于隔离持久化代码。

Java代码:

public class ItemDaoHibernateImpl implements ItemDao extends HibernateDaoSupport {

public Item getItemById(Long id) {

return (Item) getHibernateTemplate().load(Item.class, id);

}

public Collection findAll() {

return (List) getHibernateTemplate().find("from Item");

}

public void updateItem(Item item) {

getHibernateTemplate().update(item);

}

}

ItemDaoHibernateImpl完成具体的持久化工作,请注意,数据库资源的获取和释放是在ItemDaoHibernateImpl里面处理的,每个DAO方法调用之前打开Session,DAO方法调用之后,关闭Session。(Session放在ThreadLocal中,保证一次调用只打开关闭一次)

Java代码:

public class ItemManager {

private ItemDao itemDao;

public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;}

public Bid loadItemById(Long id) {

itemDao.loadItemById(id);

}

public Collection listAllItems() {

return itemDao.findAll();

}

public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount,

Bid currentMaxBid, Bid currentMinBid) throws BusinessException {

if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {

throw new BusinessException("Bid too low.");

}

// Auction is active

if ( !state.equals(ItemState.ACTIVE) )

throw new BusinessException("Auction is not active yet.");

// Auction still valid

if ( item.getEndDate().before( new Date() ) )

throw new BusinessException("Can't place new bid, auction already ended.");

// Create new Bid

Bid newBid = new Bid(bidAmount, item, bidder);

// Place bid for this Item

item.getBids().add(newBid);

itemDao.update(item); // 调用DAO完成持久化操作

return newBid;

}

}

事务的管理是在ItemManger这一层完成的,ItemManager实现具体的业务逻辑。除了常见的和CRUD有关的简单逻辑之外,这里还有一个placeBid的逻辑,即项目的竞标。

以上是一个完整的第一种模型的示例代码。在这个示例中,placeBid,loadItemById,findAll等等业务逻辑统统放在ItemManager中实现,而Item只有getter/setter方法。

贫血模型代码样例

下面用举一个具体的代码来说明:

一个带有业务逻辑的实体类,即Domain Object是Item。一个DAO接口ItemDao。一个DAO实现ItemDaoHibernateImpl。一个业务逻辑对象ItemManager。

Java代码:

public class Item implements Serializable {

// 所有的属性和getter/setter方法同上,省略

public Bid placeBid(User bidder, MonetaryAmount bidAmount,

Bid currentMaxBid, Bid currentMinBid)

throws BusinessException {

// Check highest bid (can also be a different Strategy (pattern))

if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {

throw new BusinessException("Bid too low.");

}

// Auction is active

if ( !state.equals(ItemState.ACTIVE) )

throw new BusinessException("Auction is not active yet.");

// Auction still valid

if ( this.getEndDate().before( new Date() ) )

throw new BusinessException("Can't place new bid, auction already ended.");

// Create new Bid

Bid newBid = new Bid(bidAmount, this, bidder);

// Place bid for this Item

this.getBids.add(newBid); // 请注意这一句,透明的进行了持久化,但是不能在这里调用ItemDao,Item不能对ItemDao产生依赖!return newBid;

}

}

竞标这个业务逻辑被放入到Item中来。请注意this.getBids.add(newBid); 如果没有Hibernate或者JDO这种O/R Mapping的支持,我们是无法实现这种透明的持久化行为的。但是请注意,Item里面不能去调用ItemDAO,对ItemDAO产生依赖!

Java代码:

public interface ItemDao {

public Item getItemById(Long id);

public Collection findAll();

public void updateItem(Item item);

}

ItemDao定义持久化操作的接口,用于隔离持久化代码。

Java代码:

public class ItemDaoHibernateImpl implements ItemDao extends HibernateDaoSupport {

public Item getItemById(Long id) {

return (Item) getHibernateTemplate().load(Item.class, id);

}

public Collection findAll() {

return (List) getHibernateTemplate().find("from Item");

}

public void updateItem(Item item) {

getHibernateTemplate().update(item);

}

}

ItemDaoHibernateImpl完成具体的持久化工作

Java代码:

public class ItemManager {

private ItemDao itemDao;

public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;}

public Bid loadItemById(Long id) {

itemDao.loadItemById(id);

}

public Collection listAllItems() {

return itemDao.findAll();

}

public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount,

Bid currentMaxBid, Bid currentMinBid) throws BusinessException {

item.placeBid(bidder, bidAmount, currentMaxBid, currentMinBid);

itemDao.update(item); // 必须显式的调用DAO,保持持久化

}

}

充血模型代码样例

下面用举一个具体的代码来说明:

Item:包含了实体类信息,也包含了所有的业务逻辑 ItemDao:持久化DAO接口类 ItemDaoHibernateImpl:DAO接口的实现类

Java代码:

public interface ItemDao {

public Item getItemById(Long id);

public Collection findAll();

public void updateItem(Item item);

}

ItemDao定义持久化操作的接口,用于隔离持久化代码。

Java代码:

public class ItemDaoHibernateImpl implements ItemDao extends HibernateDaoSupport {

public Item getItemById(Long id) {

return (Item) getHibernateTemplate().load(Item.class, id);

}

public Collection findAll() {

return (List) getHibernateTemplate().find("from Item");

}

public void updateItem(Item item) {

getHibernateTemplate().update(item);

}

}

ItemDaoHibernateImpl完成具体的持久化工作

Java代码:

public class Item implements Serializable {

//所有的属性和getter/setter方法都省略

private static ItemDao itemDao;

public void setItemDao(ItemDao itemDao) {this.itemDao = itemDao;}

public static Item loadItemById(Long id) {

return (Item) itemDao.loadItemById(id);

}

public static Collection findAll() {

return (List) itemDao.findAll();

}

public Bid placeBid(User bidder, MonetaryAmount bidAmount,

Bid currentMaxBid, Bid currentMinBid)

throws BusinessException {

// Check highest bid (can also be a different Strategy (pattern))

if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {

throw new BusinessException("Bid too low.");

}

// Auction is active

if ( !state.equals(ItemState.ACTIVE) )

throw new BusinessException("Auction is not active yet.");

// Auction still valid

if ( this.getEndDate().before( new Date() ) )

throw new BusinessException("Can't place new bid, auction already ended.");

// Create new Bid

Bid newBid = new Bid(bidAmount, this, bidder);

// Place bid for this Item

this.addBid(newBid);

itemDao.update(this); // 调用DAO进行显式持久化

return newBid;

}

}

DDD分层架构模型及基于贫血模型的微服务代码模型

DDD 分层架构模型

它包括用户接口层、应用层、领域层和基础层,分层架构各层的职责边界非常清晰,又能有条不紊地分层协作。

用户接口层:面向前端提供服务适配,面向资源层提供资源适配。这一层聚集了接口适配相关的功能。应用层职责:实现服务组合和编排,适应业务流程快速变化的需求。这一层聚集了应用服务和事件相关的功能。领域层:实现领域的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,以及它们组合所形成的业务能力。基础层:贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。

分层架构的一个重要原则是每层只能与位于其下方的层发生耦合。

分层架构可以简单分为两种,即严格分层架构和松散分层架构。在严格分层架构中,某层只能与位于其直接下方的层发生耦合,而在松散分层架构中,则允许某层与它的任意下方层发生耦合。

分层架构的优点,Martin Fowler在《Patterns of Enterprise Application Architecture》一书中给出了答案:

开发人员可以只关注整个结构中的某一层 可以很容易的用新的实现来替换原有层次的实现 可以降低层与层之间的依赖 有利于标准化 有利于各层逻辑的复用

适当的分层体系结构将开发层面进行隔离,这些层不受其他层的更改的影响,从而使重构更加容易。划分任务并定义单独的层是架构师面临的挑战。当需求很好地适应了模式时,这些层将易于解耦或分层开发。

使用场景:

需要快速构建的新应用程序 需要严格的可维护性和可测试性标准的应用

基于贫血模型的微服务代码模型

现在,我们来看一下,按照 DDD 分层架构模型设计出来的微服务代码模型到底长什么样子呢?

其实,DDD 并没有给出标准的代码模型,不同的人可能会有不同理解。下面要说的这个微服务代码模型是我经过思考和实践后建立起来的,主要考虑的是微服务的边界、分层以及架构演进。

微服务一级目录结构

微服务一级目录是按照 DDD 分层架构的分层职责来定义的。从下面这张图中,我们可以看到,在代码模型里分别为用户接口层、应用层、领域层和基础层,建立了 interfaces、application、domain 和 infrastructure 四个一级代码目录。

这些目录的职能是这样的。

Interfaces(用户接口层):它主要存放用户接口层与前端交互、展现数据相关的代码。前端应用通过这一层的接口,向应用服务获取展现所需的数据。这一层主要用来处理用户发送的 Restful 请求,解析用户输入的配置文件,并将数据传递给 Application 层。数据的组装、数据传输格式以及 Facade 接口等代码都会放在这一层目录里。Application(应用层):它主要存放应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。Domain(领域层):它主要存放领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里Infrastructure(基础层):它主要存放基础资源服务相关的代码,为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。

下面具体说明一下领域层的代码目录结构

Domain 是由一个或多个聚合包构成,共同实现领域模型的核心业务逻辑。聚合内的代码模型是标准和统一的,包括:entity、event、repository 和 service 四个子目录。

Aggregate(聚合):它是聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务。以聚合为单位的代码放在一个包里的主要目的是为了业务内聚,而更大的目的是为了以后微服务之间聚合的重组。聚合之间清晰的代码边界,可以让你轻松地实现以聚合为单位的微服务重组,在微服务架构演进中有着很重要的作用。Entity(实体):它存放聚合根、实体、值对象以及工厂模式(Factory)相关代码。实体类采用贫血模型,同一实体相关的业务逻辑都在实体类代码中实现。跨实体的业务逻辑代码在领域服务中实现。Event(事件):它存放事件实体以及与事件活动相关的业务逻辑代码。Service(领域服务):它存放领域服务代码。一个领域服务是多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中,你也可以把每一个领域服务设计为一个类。如果领域服务内的业务逻辑相对复杂,我建议你将一个领域服务设计为一个领域服务类,避免由于所有领域服务代码都放在一个领域服务类中,而出现代码臃肿的问题。领域服务封装多个实体或方法后向上层提供应用服务调用。Repository(仓储):它存放所在聚合的查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合,我们设定了一个原则:一个聚合对应一个仓储。

特别说明:按照 DDD 分层架构,仓储实现本应该属于基础层代码,但为了在微服务架构演进时,保证代码拆分和重组的便利性,把聚合仓储实现的代码放到了聚合包内。这样,如果需求或者设计发生变化导致聚合需要拆分或重组时,我们就可以将包括核心业务逻辑和仓储代码的聚合包整体迁移,轻松实现微服务架构演进。

综合评价

在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都是可行的。但是我个人仍然主张使用贫血模型。其理由:

虽然贫血模型的Domain Object确实不够rich,但Domain Object只包含属于它本身的领域逻辑,不包含持久化逻辑,将有效的隔离和屏蔽了持久化技术,进而可以对持久化技术进行灵活的替换。贫血模型中提出的按照是否依赖持久化进行划分,这种标准是非常确定的,不会引起开发团队设计上的争议。