领域驱动设计整理——实体和值对象设计
实体引言在领域驱动设计里,实体的设计可以说是通用语言的核心,也是最开始在模型划分中需要考虑的。怎么样设计实体和怎么样划分限界上下文同样重要。实体的概念就是要保证通用语言的完整性。领域驱动让设计实体的关注点从数据的属性和表的关联转化到了富有行为的领域概念上。 实体是具有可变性的,这是一个和值对象比较明显的区分,也即实体是可以持续得变化,持续得修改,并且具有唯一的标识。在设计实体的时候需要跳出CRUD的设计思维。把关注重点从数据模型设计转移到实体模型上。实体是能够表达什么概念,具有哪些行为,领域范围是哪些。实体的唯一标识是用来区分实体的,在实体的整个生命周期中这个唯一标识都是不变的。 设计实体实体设计中,需要先确定实体的唯一标识。在Java的实体设计中,可以借助框架来实现唯一标识。这里先不讨论具体实现细节。设计唯一标识其实可以有多种方式。
package com.lijingyao.bookrent.entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
/** * Created by lijingyao on 15/12/20 13:14. */
@MappedSuperclass
public abstract class LayerSuperType {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
protected Long getId() {
return id;
}
protected void setId(Long id) {
this.id = id;
}
}
实体就可以不用关注id生成,如下,是一个代表用户的实体。 package com.lijingyao.bookrent.entity;
import com.sun.istack.internal.NotNull;
import java.util.Calendar;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;
/** * Created by lijingyao on 15/12/20 12:48. */
@Entity
@Table(
name = "br_user",indexes = {@Index(name = "IDX_USER_NAME",columnList = "name",unique = false)})
public class User extends LayerSuperType {
@NotNull
@Column(name = "name")
private String name;
@Column(name = "utc_create")
private Calendar utcCreate;
@Column(name = "utc_modified")
private Calendar utcModified;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Calendar getUtcCreate() {
return utcCreate;
}
public void setUtcCreate(Calendar utcCreate) {
this.utcCreate = utcCreate;
}
public Calendar getUtcModified() {
return utcModified;
}
public void setUtcModified(Calendar utcModified) {
this.utcModified = utcModified;
}
}
定义实体定义实体需要先理解了领域的通用语言。因为实体需要在表达完整的通用语言基础上再对实体的属性进行定义,然后还需定义实体的唯一标识。 值对象概念和特性值对象值不变的对象,也就是说有特定含义的表达。所以值对象没有唯一标识,也作为反映通用语言的一种方式,就像领域驱动中的一个部件。值对象相对实体概念上更加简单,但关键点是一个领域概念是设计成实体还是值对象。 在上下文中集成集成值对象要保持最小化集成和最少职责。如果值对象需要依赖上游上下文的聚合。假设还是刚才的租书的场景,书籍管理系统是在一个上下文中,出租书籍上下文中需要表示一个如下概念:书籍类别(bookType)+ 出租记录(record)可以定义一个“XX类Top10 租售书籍”。这个概念就可以设计成一个值对象-BestRent。这里的BestRent并不包含Book中的type属性。而是通过bookType 这样一个自己拥有的属性来表明一个书的类型。如下是一个简单的值对象: package com.lijingyao.bookrent.vo;
/** * Created by lijingyao on 15/12/26 15:00. */
public class BestRent {
/** * The type of a book.category see {@link BookType} */
private String bookType;
/** * the rent number of one book. */
private Integer topNum;
public BestRent() {
}
public BestRent(String bookType,Integer topNum) {
this.bookType = bookType;
this.topNum = topNum;
}
public BestRent topNScience(Integer topNum) {
if (null == topNum) {
throw new IllegalArgumentException("topNum may not be null.");
}
return new BestRent(BookType.SCIENCE.name(),topNum);
}
public BestRent top10Science() {
return new BestRent(BookType.SCIENCE.name(),10);
}
public void setBookType(String bookType) {
this.bookType = bookType;
}
public void setTopNum(Integer topNum) {
this.topNum = topNum;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BestRent bestRent = (BestRent) o;
if (bookType != null ? !bookType.equals(bestRent.bookType) : bestRent.bookType != null) return false;
return !(topNum != null ? !topNum.equals(bestRent.topNum) : bestRent.topNum != null);
}
@Override
public int hashCode() {
int result = bookType != null ? bookType.hashCode() : 0;
result = 31 * result + (topNum != null ? topNum.hashCode() : 0);
return result;
}
}
通过值对象就不需要关注Book领域的业务和BookType了。这个值对象保持了无副作用性,因为它不会改变任何领域对象的状态,也不需要唯一标识。一个固定的name和一个固定的bookType就可以定义一个不变的概念,比如”自然科学类的Top10”就可以通过top10Science方法来获得。 标准类型(Standard Type)标准类型是标识某些事物的描述对象的表示方式。比如表示货币类型,标准类型可以用RMB,JPY,AID,USD等货币类型。系统中建立标准类型可以统一通信标准,防止有人拼写错误,或者用非标准的描述对象(比如临时的String对象)来表示标准概念。 策略模式实现值对象设计在构建一个值对象的时候,需要考虑到不变性的概念,可以隐藏对象本身的Setters方法。只有通过构造函数才能使用委派给自己的属性进行设置。 package com.lijingyao.bookrent.controllers;
import com.lijingyao.bookrent.service.RentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
public class BookResource {
@Autowired
private RentService rentService;
@RequestMapping(value = "/{type}/adv",method = {RequestMethod.GET})
public ResponseEntity getBestSellBookAdv(@PathVariable("type") String type,@RequestParam("num") Integer num) {
return new ResponseEntity(rentService.getBookAdvByType(type,num),HttpStatus.OK);
}
}
controller直接通过service获取资源数据。如果存在不同类别的书籍,那么service内部就需要有一个switch 来实现,最简单粗暴的方式就是: public String getBookAdvByType(String type,Integer num) {
if (type.equals("SCIENCE")) {
return "自然科学 图书的畅销Top :" + num.toString();
}else if(type.equals("NOVEL")){
return "小说 图书的畅销Top :" + num.toString();
}else{
...
}
return "没有找到此类别图书广告语.";
}
这种方式可以发现,对于以后扩展书籍类型的话,需要修改Service的实现,也就不符合开放闭合原则。如果借助枚举类来管理书籍类型,再用函数式编程处理广告语产生的行为。那么Service的代码就可以写成: package com.lijingyao.bookrent.service;
import com.lijingyao.bookrent.service.vo.BookType;
import org.springframework.stereotype.Service;
/** * Created by lijingyao on 15/12/26 14:37. */
@Service
public class RentService {
public String getBookAdvByTypeStrategy(String type,Integer num) {
BookType bookType = BookType.valueOf(type);
if (null == bookType) {
return "There is no advertise of this kind of book.";
}
return bookType.bestRentOf(num);
}
}
行为bookType.bestRentOf 的实现交给了具体的策略。策略的Handler,也就是枚举类如下: package com.lijingyao.bookrent.service.vo;
import java.util.function.Function;
/** * Created by lijingyao on 15/12/20 13:43. */
public enum BookType {
NOVEL((topNum) -> BestRentUtils.advOfBestRent("小说",topNum)),SCIENCE(((topNum) -> BestRentUtils.advOfBestRent("自然科学",topNum))),TECHNOLOGY(((topNum) -> BestRentUtils.advOfBestRent("科学技术",topNum)));
private Function<Integer,String> strategy;
public String bestRentOf(Integer topNum) {
return strategy.apply(topNum);
}
BookType(Function<Integer,String> strategys) {
this.strategy = strategys;
}
}
启动Springboot 输入:http://localhost:8080/books/NOVEL/adv?num=20 结语无论是值对象还是实体,在设计持久化的时候,需要先根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。这是一种DDD的思维方式,所以要尽量避免数据模型从领域模型中泄露给客户端。下一篇文章会总结下领域服务和应用服务。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |