大牛聊软件设计之领域设计

大多数的程序员写的代码都不属于DDD,有设计的不多,更多的是像写“面条代码”。从端上一条线杀到数据库完成一个操作。设计集中在数据库(有时数据库设计都没有,一堆字段也没个注释),代码更多的是靠自我修养。这个时候就需要依赖强大的测试来保证我们的代码质量。这样久而久之就会越陷越深,陷入到技术债当中。

领域模型探讨

1. 领域模型设计:基于对象 VS 基于数据库

谈到设计,我们通常会从两种维度入手:

a. Data Modeing: 通过数据抽象系统关系,即数据库设计

b. ObjectModeing: 通过面向对象方式抽象系统关系,即面向对象。我们一开始就把整个系统的对象列出来。

大部分架构师都是从数据库设计开始设计软件系统的,少部分人是从objectModeing方式进行开始设计软件系统的。这两种建模方式并不冲突,但从哪个方向开始设计,对系统最终形态有很大的区别。

DataModel

领域模型(也叫数据模型)

好的领域模型可以让产品结构清楚,修改更方便,演进成本更低。在一个开发团队里面,架构师很重要,他决定了软件结构,这个结构决定了软件未来的可读性、可扩展性、可演进性。传统的开发模式是:架构师来设计领域模型,然后开发人员基于这个领域模型进行开发。

10多年前这个领域模型其实就是数据字典。架构师在需求讨论过程中不停演进更新这个数据字典。数据库设计是根本,一切开发都是围绕这本数据字典展开


在servicec层就是去manage大部分的逻辑,POJO作为数据在manage层不停的变换和组合。所以在manage层操作的都是POJO对象。

service层是一个巨大的加工工厂,围绕着数据库这份DNA,完成业务逻辑。

ObjectModel

2004rh ,EricEvans发表了Domain-Driver Design-Tackkling Complexity in the Heart of software(领域驱动设计),开创性的理论阐述

在聊到DDD的时候,可以做个假设,如果我们的机器内存足够大永远不当机,那我们就不需要再设计数据库了。那这个时候你会怎么设计你的软件 ?

没了数据库,那领域模型就要基于程序本身来设计了,这个时候就把设计模式的作用发挥出来了。可以充分发挥设计模式的价值了。

有人说:“类与表有点像,甚至认为表和类就是对应原,行row和object就是对应的”。其实这种认知是不对的(我之前的理解也是这样的,doamin->POJO->table)

类和表有以下几个显著区别,这些区别对领域建模的表达丰富度有显著差别,因为类里面有封装、继承、多态,我们对领域模型的表达要生动得多,对SOLID原则也会严谨

  • 【引用】关系数据库表示多对多的关系是第三张表来实现,这个领域模型表示不具像化,业务同学看不懂
  • 【封装】类可以设计方法,数据并不能完整的表达领域模型,数据表可以知道一个人三维,并不知道“一个人是可以跑的”
  • 【继承,多态】类可以多态,数据上无法识别人与猪除了三维数据还有行为的区别,数据表不知道:“一个人跑起来和一头猪跑起来是不一样的”

在面向对象里面我们可以设计领域模型,service层就是基于这些模型做的业务操作(它不再是万能的上帝之手了,很多动作就交给domainobjects去处理):领域模型并不完成业务,每个domain object都是完成属于自己应有的行为;就如同人跑这个动作,person.run 是一个与业务无关的行为。但在manager或service在调用some person.run的时候可能完成的100米比赛这个业务,也可以是跑去送外卖的这个业务。



它的领域模型重点是放在Domain层上。

数据库不再去承载领域模型这个包袱了,数据库回归的是persistence这个本质。它需要完成两件事情:

【存】将对象数据持久化到存储介质中

【取】高效的把数据查询并返回到内存中

由于没有承载领域建模这个特性了,数据库的设计就可以做的非常天马行空了。我们可以做列式数据库、K-V数据库,文档数据库,可以设计中间表去完成大数据查询。

数据库的设计更多的是为了存、取需求。不需要表达领域模型。

新的架构图:


  • 领域模型是用于领域操作的,当然也可以用于查询(read),不过这个查询是代价的。在这个前提下,一个aggregate可能内含了若干数据,这些数据除了类似于getById这种方式,不适用于多样化查询。
  • 查询是基于数据库的,所有的复杂变态查询应该绕过Domain层,直接与数据库打交道。(直接拼SQL?)
2. 领域模型:失血、贫血、充血模型
这个模型是MartinFowler提出来的概念。有点像:瘦、中等,健壮、胖。
失血模型:基于数据库的领域设计方式.以Java为例,POJO只有简单的基于field的setter,getter,POJO之间的关系隐藏在对象的某些IDj里面,由外面的manage解释。比如son.fatherId
贫血模型:
  1. public class Son{  
  2.     private Father father;  
  3.     public Father getFather(){return this.father;}  
  4. }  
public class Son{
    private Father father;
    public Father getFather(){return this.father;}
}
儿子这个领域对象需要知道它的父亲是谁。所以我们可以在儿子这个领域对象里面加上父亲这个
那父亲是不是也得知道儿子是谁。所以父亲这个领域对象需要:
  1. public class Father{  
  2.     private Son son;  
  3.     private Son getSon(){return this.son;}  
  4. }  
public class Father{
    private Son son;
    private Son getSon(){return this.son;}
}

这两个类就是我们所说的贫血模型。

这两个领域对象之间存在循环引用 的风险,为了解决有向无环,防止这个循环调用。可以这样操作:

  1. public class Father{  
  2.     //private Son son; 删除这个引用  
  3.     private SonRepository sonRepo;//添加一个Son的repo  
  4.     private getSon(){return sonRepo.getByFatherId(this.id);}  
  5. }  
public class Father{
    //private Son son; 删除这个引用
    private SonRepository sonRepo;//添加一个Son的repo
    private getSon(){return sonRepo.getByFatherId(this.id);}
}

通过这种方式,我们在构造Father这个对象的时候就不会再构造一个Son对象了。作为代价就是我们在Father这个类里面引入了SonRepository这个对象。我们在一个domain里面引入了一个持久化操作(对数据库表的get\delete\select操作),这个就是我们所说的 充血模型。
充血模型:充血模型让domainobject失去了血统的纯正性,他不再是一个纯的内存对象,这个对象里面隐藏了一个对数据库的操作,这对测试是不友好的。我们在做UT的时候不应该去做DB连接。为了保证模型的完整性,充血模型在有些情况下是必然存在的。
举个比较形象的示例吧
比如某个店铺里面卖好几千个商品,而某个商品有好几千个属性。那如果我们在构建一个店的时候把所有商品都拿出来,那这个效率就太差了。
  1. public class Shop{  
  2.     //private List<Product> products; 这个商品列表在构建时太大了  
  3.     private ProductRepository productRepo;   // 这是一个持久化操作,需要操作DB的  
  4.     public List<Product> getProducts(){  
  5.         //return this.products;  
  6.         return productRepo.getShopProducts(this.id);  
  7.     }  
  8. }  
public class Shop{
    //private List<Product> products; 这个商品列表在构建时太大了
    private ProductRepository productRepo;   // 这是一个持久化操作,需要操作DB的
    public List<Product> getProducts(){
        //return this.products;
        return productRepo.getShopProducts(this.id);
    }
}

3. 领域模型下的依赖注入
上面我们发现在Shop这个领域对象里面有依赖数据库操作。这存在依赖注入。比如要跑UT 就会非常麻烦。
依赖注入的简单理解:
  • 依赖注入在runtime是一个singleton对象,只有在spring扫描范围内的对象(@ Compent)才能通过annotation(@Autowired)用上依赖注入,通过new出来的对象是无法通过annotation得到注入的
  • 个人推荐使用构造器依赖注入(之前团队内也争过是否用构造器做依赖注入),这种情况下测试友好,对象构造完整好,显式的告诉你必须mock/stub哪个对象。

回到上面的充血模型

  1. public class Father{  
  2.     private SonRepository sonRepo;  
  3.     private Son getSon(){return sonRepo.getByFatherId(this.id);}  
  4.     <span style=”color:#ff0000;”><strong>public Father(SonRepository sonRepo){this.sonRepo = sonRepo;}</strong></span>  
  5. }  
  6. ​  
public class Father{
    private SonRepository sonRepo;
    private Son getSon(){return sonRepo.getByFatherId(this.id);}
    public Father(SonRepository sonRepo){this.sonRepo = sonRepo;}
}
​
那我们看看能不能依据依赖注入的方式把sonRepo对象给注入进来。Father不可能是一个singleton对象,它可能在两个场景会被new出来:新建、查询。从Father的构造过程,SonRepository是无法注入的,这时就使用工厂模式就显示其意义出来了。

  1. @Component  
  2. public class FatherFactory{  
  3.     private SonRepository sonRepo;  
  4.     <span style=”color:#ff6666;”>@Autowired  
  5.     public FatherFactory(SonRepository sonRepo){}</span>  
  6.     public Father createFather(){  
  7.         return new Father(sonRepo);  
  8.     }  
  9. }  
@Component
public class FatherFactory{
    private SonRepository sonRepo;
    @Autowired
    public FatherFactory(SonRepository sonRepo){}
    public Father createFather(){
        return new Father(sonRepo);
    }
}

使用构造器的方式来进行依赖注入。
4. 领域模型:测试友好

上面说了在domain里面带上了persistence之后把测试变得对DB有依赖了。那像mock/stub这些依赖是高效单元测试的基本要求。

  1. public class Father{  
  2. <span style=”color:#ff6666;”>   private SonRepository sonRepo;//=new SonRepository()这里不能构造  
  3. </span> private getSon(){return sonRepo.getByFatherId(this.id);}  
  4.     //放到构造函数里  
  5.     public Father(SonRepository sonRepo){this.sonRepo = sonRepo;}  
  6. }  
public class Father{
   private SonRepository sonRepo;//=new SonRepository()这里不能构造
 private getSon(){return sonRepo.getByFatherId(this.id);}
    //放到构造函数里
    public Father(SonRepository sonRepo){this.sonRepo = sonRepo;}
}

5. 领域模型:repository的实现方式

由于 领域模型存在于内存对象里,这些对象最终是要落到数据库。由于不需要考虑领域模型的束缚,所以数据库设计就可以灵活多变了。


加了一层tunnel接口,通过这个接口可以实现对domain对象在不同类型的DB存取。而且respository并没有直接进行持久化工作,而是将domain对象转换成POJO再交给tunnel去做持久化工作。


今天看到一句比较鸡汤的话:你可能身处人生低谷,但不要放弃学习,不能沮丧。更不能停止前进的脚步!


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值