【如何设计软件 ? 】领域驱动设计 | 4 层架构 + 3 类物件

有想法 x 也有做法

大纲

  • 前人的专案
  • 领域驱动设计
  • 理论与分层结构
  • 领域层的物件
  • 专案架构实作
  • 有想法也有做法

前人的专案

日常开发时,面对前人传承下来的专案,改 A 坏 B ,改 B 坏 C,

各种系统模组疯狂耦合与随意分类,乱糟糟的系统架构,开发得很痛苦 !

已经没救了!

基本上,专案发展到这个程度,

完全修复,不是不可能,只是成本很大 :

修复的收成本 > 修复的收益

你我都知道,在实务上的时程安排 :

「有空再修」-> 美丽,但不切实际的幻想

重新再来

也许,有机会砍掉重练,或者在其他的专案,有机会重头开始 !

但当面对全新的专案时,不希望重蹈覆彻

却发现 !

不是很确定该怎麽做 !?

领域驱动设计

学习之路

我软件设计的「学习之路」,是从「无瑕的程序码 整洁的软件设计与架构篇」,这本书开始。

部分的理解内容,阐述在「软件的本质」。

知行合一

但当时,还有一个疑问,书本内有很多理论与重要的设计原则。

不能很好的结合在日常开发中,中间的转换过程遇到了瓶颈。

直到在「Spring Cloud 微服务架构 开发实战」

提到的架构的规划方法 :

领域驱动设计

补足了我实作层面遇到的问题 !

让我可以将设计的理论原则,结合到 Java Spring 的专案中。

据此延伸,架构的规划是没有语言与平台的局限性。

开发 Android 与 iOS 的 App 时,也可以完全套用这一套软件设计的方法论。


理论与分层结构

映射现实

软件提供的服务,一定是会对映现实世界的某件事物。但开发者与使用者,看待软件的角度,通常是不一样的。如何在两者之间,使用共同的语言,沟通会是一个挑战。

领域驱动设计

Domain Driven Design 简称 DDD

以领域知识为核心建立的模型,领域专家与开发人员,可以透过这个模型进行交流。

确保最终设计出来的东西,是双方共同想要达到的结果

分层架构

领域驱动设计的分层结构,会分成四个部份。

layer-1

  • 用户介面层 (User Interface)
  • 应用层 (Application Layer)
  • 领域层 (Domain Layer)
  • 基础设施层 (Infrastructure Layer)

越往上,离使用者越接近,是客户能够理解的部分。

越往下,则离程序语言与平台越接近,是开发者搭建系统的技术实作。


用户介面层 (User Interface)

用户接口层 / 表示层(Presentaion)

从字面意思上理解,就是 UI 介面。

这一层,负责向用户显示信息或解释用户指令。

除了给人看的介面,也有可能是给机器看的介面,使用者可能会透过另外一个系统来访问你的系统。

可以理解为这一层就是飞机的机场或货轮的港口,负责国内外的进出口平台 :

进出的可以是人也可以是货物

user-interface


应用层 (Application Layer)

定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。

用机场来说明的话,就是要上飞机前的海关检查程序。

application

每一个站点,要调用的领域物件与服务,就是稽核人员、查核护照与检查物品的动作。当出现异常时,依据程序,通知保安与巡警到达现场。

整套协调任务、分配工作的流程,就是「应用」

应用 = 流程

「应用」代表的是各种任务的背後的流程

  • 用户介面层 : 只需要知道,要完成他的工作,需要调用哪一个应用。

  • 应用层 : 只需要知道,要完成他的流程,需要调用领域层的哪个物件与服务。

    「应用层」只管流程步骤,本身并不介入任务的实际执行。


领域层 (Domain Layer)

模型层(Model Layer)

主要负责表达业务概念、业务状态信息及业务规则

以海关的稽核人员来说 :

domain

  • 业务 : 查核旅客的护照与身上的物品

  • 业务状态讯息 : 查核哪些资讯与违禁物的清单

  • 业务规则 : 有违禁品时 要进行通报

    至於要调配哪一位,保安与巡警到达现场,则是管理中心 应用层的职责。


基础设施层 (Infrastructure Layer)

为上面各层提供通用的技术能力

例如:

  • 为应用层传递消息
  • 为领域层提供数据访问及持久化机制
  • 为用户介面层绘制屏幕组件

可以理解为,海关安检时用的:

  • X 光机
  • 金属探测器
  • 电脑设备

infra


层级关系

层与层之间的调用,是由上而下,并且可以跨层呼叫
但不会出现,由下而上或者平行呼叫的情况出现

layer-2

就像是要请其他的部门协助支援:

一定是循组织规章程序作业,递交申请或者是向上通报,而不是直接跑到对方部门 ,在对方主管都不知情的情况下,要求同事协助帮忙。

这个服务调用之间的顺序与流程,也有一些原则可以遵循:

元件耦合性原则:
ADP : 无循环依赖原则
SDP : 稳定依赖原则
SAP : 稳定抽象原则

这部分,是属於「元件耦合性」的问题,後续会再独立介绍。


领域层的物件

领域驱动设计,可以理解为,以领域层为核心,驱动整个系统的设计方法。

在跟领域专家讨论与建构模型的顺序:

  1. 领域层
  2. 应用层
  3. 用户界面层

(领域专家不需要去管技术实作)

描述模型

领域层的模型建立,还缺了三个东西,用来描述这个模型 :

  • 实体
  • 值对象
  • 服务

实体(Entity)

物件导向概念中的**「物件」,并且带有「标示符」**的对象。

POJO 物件

public class AppInfo {
    private Long id; // <- 「标示符」
    private String name;
    private String version;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
}

「标示符」

指的是当中的属性有唯一识别码,在经过软件的各个分层结构时,仍然保持一致。

例如:

  • 人的身份字号
  • 资料库产生的流水号主键

值对象(Value Object)

相似於实体(Entity),两者的差异在於值对象,没有「标示符」。

POJO 物件

public class AppInfo {
    private String name; // <- 没有「标示符」
    private String version;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
}

为什麽没有标示符 ?

因为有时候,人们对於某个对象是什麽并不感兴趣,
只关心他所拥有的属性,能够描述领域的特殊方面,即可完成任务。

服务(Service)

跟实体与值对象不太一样,在与领域专家规划模型时,会发现领域中有些方面是很难映射成对象。

尤其是领域中的动作,不属於任何对象,却代表了一个重要行为,此种行为通常会跨越若干的对象。

将此行为加入到任一对象中会破坏对象。

对象 = 物件 = 实体 / 值对象

最佳实做的方式是将它当成一个服务(Service)


可以简单地理解:

实体与值对象 -> 名词
服务 -> 动词

名词与动词两者互相搭配,描述了领域中的一项任务。


专案架构实作

改造目标

Java 最常见的 Spring 框架示范如何调整

一般 Spring 框架,书籍中的范例,通常会切分成三个阶层 :

  • Controller : @Controller 的分一个资料夹放
  • Service : @Service 也分一个资料夹来放
  • DAO : 所有有关资料库存取的服务与对象也分一个资料夹放

spring-sample-layer

优势

  • 简单
  • 快速实现

缺点

  • 服务多的时候会变得复杂
  • Service 与 DAO 深度耦合
  • Service 不能进行单元测试

领域分层

套用领域驱动设计的四个分层

用户界面层 (User Interface)

对应的是 Controller 保持不变


应用层 与 领域层 对应的是 Service

拆分成两个 :

  • 应用服务层 (Application Service)
  • 领域服务层 (Domain Service)

基础设施层,对应的是 DAO

DAO 该算是基础设施层的一个子项模组

所以在基础设施层,划分一个数据库的区块存放

Infrastructure/repository

基础设施层,细节部分

spring-boot-infra

  • util/ : 共用元件的资料夹
  • filter/ : Spring 设定

领域层,细节的部分:

spring-boot-domain

会在切分 实体 Entity 资料夹 与 值对象 Value Object 资料夹

此部分可以分类的更细

元件内聚性原则:
REP : 再使用性 - 发布等价原则
CCP : 共同封闭原则
CRP : 共同重复使用原则

这部分,是属於「元件内聚性」的问题,後续会再独立介绍。


应用层,细节的部分:

spring-boot-application

由於应用层,代表的是一连串动作的最终结果,返回的结果,可能包含了很多,实体与值对象的内容。

所以,会在应用层内,划分一个 DTO 资料夹

DTO 代表的是 Data Transfer Object,资料传输物件

资料传输物件,用於应用层的服务,资料物件的输入与输出。

通常,会以领域层的实体或值对象继承实作:

public class AppInfoDTO extends AppInfo {
    private String groupId;
    private String groupName;
    private String groupDescription;

    public String getGroupId() {
        return groupId;
    }

    public void setGroupId(String groupId) {
        this.groupId = groupId;
    }

    public String getGroupName() {
        return groupName;
    }

    public void setGroupName(String groupName) {
        this.groupName = groupName;
    }

    public String getGroupDescription() {
        return groupDescription;
    }

    public void setGroupDescription(String groupDescription) {
        this.groupDescription = groupDescription;
    }
}

除了,直接扩展,省去了重新建构的时间以外,两者之间也建立了强关联,

一眼就可以看出这个传输物件,是属於哪一类的领域项目。


资料访问服务,细节部分:

spring-boot-dao

若持久化框架,映射的「持久化物件」允许继承(ORM 框架 : MyBatis )

 持久化物件: 
 PO , Persistent Object

同样会让该物件继承领域层的实体或值对象

public class AppInfoPO extends AppInfo {
    public String createUser;
    public String createTime;
    public String modifyUser;
    public String modifyTime;

    public String getCreateUser() {
        return createUser;
    }

    public void setCreateUser(String createUser) {
        this.createUser = createUser;
    }

    public String getCreateTime() {
        return createTime;
    }

    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }

    public String getModifyUser() {
        return modifyUser;
    }

    public void setModifyUser(String modifyUser) {
        this.modifyUser = modifyUser;
    }

    public String getModifyTime() {
        return modifyTime;
    }

    public void setModifyTime(String modifyTime) {
        this.modifyTime = modifyTime;
    }
}

扩增的部分,通常是资料库资料表才会特别纪录的数据:

  • 新增使用者
  • 修改使用者
  • 新增时间
  • 修改时间

单元测试

以上的安排规划,使得领域层的服务,只会依赖於同层的领域物件;

不需要基础设施层的支持,就可以单独的进行单元测试。

service-test

架构无国界

这种安排规划,也可以套用在其他种程序语言与平台上

Android

AndroidProjSE

  • Android 的 Activity 就是 使用者介面层
  • Android SDK 提供的各种服务 就是基础设施层
  • 应用层与领域层甚至连改都不用改

iOS

iOSProjSE

  • iOS 的 Controller 与 SDK 也是同样道理

有想法也有做法

伊隆 • 马斯克 :

如果你就读工程科系,并且对设计东西很有想法的话
自己创业,是个相对简单的事情。

你需要的就只是找几个和你理念相同的夥伴。

-- Youtube : 伊隆‧玛斯克对学生和大学毕业生的终极建议 - 如何拥有成功人生

你如果对设计有想法的话,现在更有了作法,起码明天上班时,就可以试着去优化你的系统。

我这个结合了书籍知识以及个人经验的,专案结构的规划方案,也是经过了一段时间的讨论,

才被团队逐渐认可,并且套用到下一个新的专案项目中。

事情并非一簇可几,但起码有了方向。

并且我认为软件设计的思维模式,也是一种大局观的思考方式 :

如果你可以组织映射现实的软件架构,那麽你当然也可以组织现实的实际事物。

未来的各种挑战,说不定哪时候就可以派上用场。


参考资料


<<:  Day46. 范例:摩斯电码 (解译器模式)

>>:  2021最新Canonical终极指南,短短的语法让你的SEO功力倍增提升网站能见度

【Day8】EditProfileFragment X Storage上传照片

在昨天的Profile页面中,我们可以看到有照片的讯息,那我们今天主要要来做的就是~ 把手机相簿里...

【day25】上传多张照片(下)

连假第二天,在这边先祝大家连假快乐啦,那延续昨天,我们现在已经把String的List拿到了,现在...

[Day24] Flutter with GetX Shimmer

Shimmer iOS Swift的话是类似SkeletonView 一般用在等待的时候 像是API...

行动应用APP资安篇

针对我们所谓的Mobile Security(移动装置安全、行动装置安全)。 经常会联想到智慧型手机...

C# 入门之函数(补充)

前面我们有讲过 C# 中的函数,今天我们补充一点。 在 C# 中,支持一种函数叫做 “匿名函数”,即...