前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >最好的工程师枕边读物DDD的启蒙书《代码精进之路:从码农到工匠》

最好的工程师枕边读物DDD的启蒙书《代码精进之路:从码农到工匠》

作者头像
燃192
发布2023-02-28 13:57:36
8690
发布2023-02-28 13:57:36
举报
文章被收录于专栏:Java技术圈子

代码精进之路:从码农到工匠

读后感:是一本很不错的技术书籍,并没有拘泥于技术细节,而是从微知著的讲实现复杂业务系统的落地细节与模型设计,作者具有很高的技术素养,从简入深的讲解从一个变量的命名,到复杂系统的领域建模的思考过程,对代码精益求精的态度值得每一个技术人员学习。适用于至少1年开发经验的开发人员。

如果是1-3年的开发人员,可以从中汲取功能实现如何写的内聚优雅。书中提供了很多有价值有非常有代表性的case。

如果是3-5年的工程师,可以从中汲取领域建模与架构设计的思路,从抽象出一个聚合根到,企业级应用系统领域驱动设计模型实现COAL都讲到。比概念性的书多了工程实现,又摒弃了技术细节,关注与业务设计。

关于COLA架构,提供给了没有机会接触DDD架构技术的开发者企业级项目的完整设计实现。

而且对于DDD作者了解的很透彻

很多人是通过阅读《领域驱动设计:软件核心复杂性应对之道》开始入门领域驱动设计的,这本书中提到了很多概念,比如Repository、Domain和ValueObject等,但是初学者可能会误认为在项目架构中加入Repository、Domain和ValueObject就变成了DDD架构。如果没有悟出其精髓就在项目中加入这些概念,那充其量也不过是“老三层架构”的变种;反之,对于一个面向对象分析的高手而言,不使用这些概念也可以实现领域驱动设计。这本书就能扫盲何为DDD最精髓的设计。

代码精进之路:从码农到工匠序一前言第一部分 技艺第一章 命名1.3 有意义的命名1.4 保持一致性1.5 自明的代码第二章 规范2.1 认知成本2.3 代码规范2.错误码2.4 埋点规范2.6 防止破窗2.7 本章小结第三章 函数3.2 软件中的函数3.3 封装判断3.4 函数参数3.5 短小的函数3.6 职责单一3.7 精简辅助代码3.8 组合函数模式3.9 SLAP3.10 函数式编程3.11 本章小结第4章 设计原则4.1 SOLID概览4.4 LSP4.5 ISP4.6 DIP4.7 DRY4.8 YAGNI4.10 KISS原则4.11 POLA原则第5章 设计模式5.2 GoF5.3 拦截器模式5.4 插件模式5.5 管道模式5.6 本章小结第6章 模型6.1 什么是模型6.3 类图6.4 领域模型6.5 敏捷建模6.6 广义模型6.7 本章小结第7章DDD的精髓7.1 什么是DDD7.3 数据驱动和领域驱动7.4 DDD的优势7.6 领域建模方法7.7 模型演化7.8 为什么DDD饱受争议第二部分 思想第8章 抽象8.4 抽象的层次性8.7 本章小结第9章 分治9.2 函数分解9.3 写代码的两次创造第10章 技术人员的素养10.1 不教条

序一

程序员的快乐和骄傲很大程度上来自于那些具有美感的代码。

前言

软件的本质复杂性。《人月神话》的作者Frederick P.Brooks.Jr曾说:“软件的复杂性是一个基本特征,而不是偶然如此。”问题域有其复杂性,而软件在实现过程中又有很大的灵活性和抽象性,导致软件具有天然的复杂性。

写好代码的技艺不是一蹴而就的,它是一个系统化的工程,不是看几本书、写几年代码就能轻松习得的,而需要我们对自己的思维习惯、学习方法和工程实践进行彻底的反省和重构。

比如effective java,任何不区分上下文和情景的教条都有可能在实施过程中遭遇惨败。

本章介绍了很多前人总结的优秀设计原则,包括最著名的SOLID,它为我们提供了非常好的OO设计指导原则,比如扩展性的终极目标是满足OCP。我个人特别推崇DIP,因为它是架构设计的重要指导原则。

技术团队管理者:管理者的一个很重要的使命就是帮助团队成长,包括制定规范和技术传承。

第一部分 技艺

第一章 命名

1.3 有意义的命名

实体类承载了核心业务数据和核心业务逻辑,其命名要充分体现业务语义,并在团队内达成共识,如Customer、Bank和Employee等。辅助类是辅佐实体类一起完成业务逻辑的,其命名要能够通过后缀来体现功能。例如,用来为Customer做控制路由的控制类CustomerController、提供Customer服务的服务类CustomerService、获取数据存储的仓储类CustomerRepository。对于辅助类,尽量不要用Helper、Util之类的后缀,因为其含义太过笼统,容易破坏SRP(单一职责原则)。比如对于处理CSV,可以这样写:CSVHelper.parse(String) CSVHelper.create(int[])但是我更建议将CSVHelper拆开:CSVParser.parse(String) CSVBuilder.create(int[])

这里说的模块(Module)主要是指Maven中的Module,相对于包来说,模块的粒度更大,通常一个模块中包含了多个包。在Maven中,模块名就是一个坐标

。一方面,其名称保证了模块在Maven仓库中的唯一性;另一方面,名称要反映模块在系统中的职责。例如,在COLA架构中,模块代表着架构层次,因此,对任何应该遵循COLA规范的应用都有着xxx-controller、xxx-app、xxx-domain和xxx-Infrastructure这4个标准模块。更多内容请参考12.3节。

1.4 保持一致性

遵守对仗词的命名规则有助于保持一致性,从而提高代码的可读性。像first/last这样的对仗词就很容易理解;而像fileOpen()和fClose()这样的组合则不对称,容易使人迷惑。下面列出一些常见的对仗词组:

add/remove

increment/decrement

open/close

begin/end

insert/delete

show/hide

create/destroy

lock/unlock

source/target

first/last

min/max

start/stop

get/set

next/previous

up/down

old/new

后置限定词很多程序中会有表示计算结果的变量,例如总额、平均值、最大值等。如果你要用类似Total、Sum、Average、Max、Min这样的限定词来修改某个命名,那么记住把限定词加到名字的最后,并在项目中贯彻执行,保持命名风格的一致性。

统一语言就是要确保团队在内部的所有交流、模型、代码和文档中都要使用同一种编程语言。实际上,统一语言(Ubiquitous Language)也是领域驱动设计(Domain Driven Design,DDD)中的重要概念,在7.4.1节中会有更加详细的介绍。

统一技术语言有些技术语言是通用的,业内人士都能理解,我们应该尽量使用这些术语来进行命名。这些通用技术语言包括DO、DAO、DTO、ServiceI、ServiceImpl、Component和Repository等。例如,在代码中看到OrderDO和OrderDAO,马上就能知道OrderDO中的字段就是数据库中Order表字段,对Order表的操作都在OrderDAO里面。

1.5 自明的代码

中间变量我们可以通过添加中间变量让代码变得更加自明,即将计算过程打散成多个步骤,并用有意义的变量名来命名中间变量,从而把隐藏的计算过程以显性化的方式表达出来。

例如,我们要通过Regex来获得字符串中的值,并放到map中。

代码语言:javascript
复制
Matcher matcher = headerPattern.matcher(line); 
if(matcher.find()){ 
    headers.put(matcher.group(1), matcher.group(2)); 
}

用中间变量,可以写成如下形式:

代码语言:javascript
复制
Matcher matcher = headerPattern.matcher(line); 
if(matcher.find()){ 
    String key = matcher.group(1); 
    String value = matcher.group(2); 
    headers.put(key, value); 
}

中间变量的这种简单用法,显性地表达了第一个匹配组是key,第二个匹配组是value。只要把计算过程打散成一系列良好命名的中间值,不透明的语义自然会变得透明。

名字和方法名能理解就不要再复述不要复述功能,要解释背后意图

在这里等待2秒——>休息2秒,为了等待关联系统处理结果

或者直接用一个private方法将其封装起来,用显性化的方法名来表达意图,这样就不需要注释了。

private void waitProcessResultFromA( )

第二章 规范

2.1 认知成本

在学习过程中,我们要交的学费叫作认知成本。

知识是人类对经验范围内的感觉进行总结归纳之后发现的规律

2.3 代码规范

写文章和代码,小段空格隔离方法和段落可以大大增加可读性空行在代码中的概念区隔作用同样适用。

一个简单的原则就是将概念相关的代码放在一起:相关性越强,彼此之间的距离应该越短。

有些语言在定义时提倡以前缀来区分局部变量、全局变量和变量类型。例如,JavaScript是弱类型语言,所以其中会有匈牙利命名法的习惯,用li_count表示local int局部整形变量,使用$给jQuery的变量命名。语言的命名风格多样,无可厚非,但是在同一种语言中,如果使用多种语言的命名风格,就会令其他开发工程师反感。

类名采用“大驼峰”形式,即首字母大写的驼峰,例如Object、StringBuffer、FileInputStream。

方法名采用“小驼峰”形式,即首字母小写的驼峰,方法名一般为动词,与参数组成动宾结构,例如Thread的sleep(long millis)、StringBuffer的append(String str)。

常量命名的字母全部大写,单词之间用下划线连接,例如TOTAL_COUNT、PAGE_SIZE等。

枚举类以Enum或Type结尾,枚举类成员名称需要全大写,单词间用下划线连接,例如SexEnum.MALE、SexEnum.FEMALE。

抽象类名使用Abstract开头;

异常类使用Exception结尾;

实现类以impl结尾;

测试类以它要测试的类名开始,以Test结尾。

包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词,包名统一使用单数形式。

通常以com或org开头,加上公司名,再加上组件或 者功能模块名,例如org.springframework.beans。

日志要写的足够精炼还能体现问题。有时候使用不当轻则磁盘爆满,重责抢占日志对象资源卡死机器

导致日志文件管理混乱、日志输出格式不统一,结果在出现故障时影响工作效率。

1.异常处理很多的应用系统因为没有统一的异常处理规范,增加了人为的复杂性,具体体现在以下两个方面。

(1)代码中到处充斥着异常捕获的try/catch的代码,搞乱了代码结构,把错误处理与正常流程混为一谈,严重影响了代码的可读性。

(2)异常处理不统一,有的场景对外直接抛出异常,有的场景对外返回错误码,这种不一致性让服务的调用方摸不着头脑,增加了服务的使用成本和沟通成本。

我建议在业务系统中设定两个异常,分别是BizException(业务异常)和SysException(系统异常),而且这两个异常都应该是Unchecked Exception。为什么不建议用Checked Exception呢?因为它破坏了开闭原则。

用模板模式>针对业务异常和系统异常要做统一的异常处理,类似于AOP,在应用处理请求的切面上进行异常处理收敛,其处理流程如下:

代码语言:javascript
复制
try {
    //业务处理
    Response res = process(request);
}catch (BizException e) {
    //业务异常使用WARN级别
    logger.warn("BizException with error code:{},error message:{}", e.getErrorCode(), e.getErrorMsg());
}catch (SysException ex) {
    //系统异常使用ERROR级别
    log.error("System error" + ex.getMessage(), ex);
}catch (Exception ex) {
    //兜底
    log.error("System error" + ex.getMessage(), ex);
}

千万不要在业务处理内部到处使用try/catch打印错误日志,这样会使功能代码和业务代码缠绕在一起,让代码显得很凌乱,并且影响代码的可读性。

2.错误码

编号错误码对于平台、底层系统或软件产品,可以采用编号式的编码规范,好处是编码风格固定,给人一种正式感;缺点是必须要配合文档才能理解错误码代表的意思。例如,数据库软件Oracle中总共有2000多个异常,其编码规则是ORA-00001~ORA-02149,每一个错误码都有对应的错误解释。·ORA-00001:违反唯一约束条件。

大型分布式架构下的业务系统中,每个业务都由很多分布式服务组成,而且这些服务都提供给内部系统使用。在这种情况下,除了编号错误码之外,更推荐使用显性化的错误码。显性化的错误码具有更强的灵活性,适合敏捷开发。

我们可以将错误码定义成3个部分:类型+场景+自定义标识。每个部分之间用下划线连接,内容以大驼峰的方式书写。这里可以打破Java的常量命名规范,驼峰方式会更方便阅读。对于错误类型,我们可以做一个约定:P代表参数异常(ParamException)、B代表业务异常(BizException)、S代表系统异常(SystemException)。一个完整的示例如表2-1所示。

2.4 埋点规范

做互联网产品,了解用户的行为和心智很重要。有一句话叫“业务数据化、数据业务化”,即业务要沉淀数据、数据要反哺业务。对于产品经理来说,要清楚用户的第一件事情是做什么、接着还会做什么、用户的轨迹和动线是怎样的。对于运营人员来说,要清楚一次活动带来了多少访问流量、转化率如何、通过不同渠道来的用户表现怎么样、最终这些用户有多少转化成了活跃用户。以上这些需求都可以使用“埋点技术”实现,“埋点”对于互联网运营至关重要。开源的统计分析工具很多,较常用的有谷歌分析、百度统计和腾讯分析等。

图2-3 数据处理的5个阶段

2.6 防止破窗

在软件工程中,“破窗效应”可谓是屡见不鲜。面对一个混乱的系统和一段杂乱无章的代码,后来人往往会加入更多的垃圾代码。这也凸显了规范和重构的价值。首先,我们要有一套规范,并尽量遵守规范,不要做“打破第一扇窗”的人;其次,发现有“破窗”,要及时地修复,不要让事情进一步恶化。整洁的代码需要每个人的精心呵护,需要整个团队都具备一些工匠精神。

2.7 本章小结

在软件开发过程中,大到体系结构和应用架构规范,小到代码格式和空行的约定,都在一定程度上影响着系统的复杂程度。和命名一样,规范的有无,并不影响代码在机器中的解释执行,但是对系统的可理解性和代码的可读性却有着巨大的影响。

第三章 函数

3.2 软件中的函数

在英语中,Function一般代表函数式语言中的函数,而Method代表面向对象语言中的函数。

3.3 封装判断

好的函数应该是清晰易懂的,我们先从一个简单又实用的函数重构技法说起。

如果没有上下文,if和while语句中的布尔逻辑就难以理解。

如果把解释条件意图作为函数抽离出来,用函数名把判断条件的语义显性化地表达出来,就能立即提升代码的可读性和可理解性。

下面来看一个例子,在我们的CRM系统中,需要判断一个客户是否可以被业务员捡入自己的私海库[插图]。原来的代码是这样写的:

代码语言:javascript
复制
if(customer.getCrmUserId().equals(NIL_VALUE) && customer.getCustomerGroup() != CustomerGroup. CANCEL_ GROUP) {                          privateSea.pickUp(customer); 
}
// 在上述代码中,if后面的判断条件令人十分费解,原因是缺少封装和合理的命名,我们可以用封装判断将其改写成:
if(canPickUpToPrivateSea()) { 
    privateSea.pickUp(customer); 
} 
private boolean canPickUpToPrivateSea(){ 
    if(StringUtil.isBlank(this.getCrmUserId())){ 
        return false; 
    } 
    if(this.getCustomerGroup() == CustomerGroup.CANCEL_GROUP){ 
        return false; 
    } return true; 
}

不难发现,重构后的代码要更容易理解,因为通过封装判断,判断条件的业务语义被显性化地表达出来了,代码的可读性自然也好了很多。

3.4 函数参数

在程序设计中,一大忌讳就是教条。

在某些场景下,两个参数可能比一个参数好。

例如,Point p = new Piont(0 , 0);

两个参数就比一个参数要合理,坐标系中的点就应该有两个参数。

总体上来说,参数越少,越容易理解,函数也越容易使用和测试,因为各种参数的不同组合的测试用例是一个笛卡儿积。

如果函数需要3个以上参数,就说明其中一些参数应该封装为类了。

例如,要绘制一条直线,可以用如下函数声明:

Line makeLine(double startX, double startY, double endX, double endY);

上述代码中的X和Y是作为一组概念被共同传递的,我们应该为这一组概念提供一个新的抽象,叫作Point。

这样将参数对象化之后,参数的个数减少了,表达上也更加清晰。

Line makeLine(Point start, Point end);class Point{ double x; double y;}

3.5 短小的函数

对超长方法的结构化分解是提升代码可读性最有效的方式之一。

华为的标准是不超过200行,有些建议是不超过一个屏幕。 如果是Java语言,我建议一个方法不要超过20行代码,当我把这个规定作为团队代码审查的硬性指标后,发现代码质量得到了显著的改善。

3.6 职责单一

按照行数规定函数的长度是定量的做法,实际上,我更喜欢另一种定性的衡量方法,即一个方法只做一件事情,也就是函数级别的单一职责原则(Single Responsibility Principle,SRP)。

遵循SRP不仅可以提升代码的可读性,还能提升代码的可复用性。

因为职责越单一,功能越内聚,就越有可能被复用,这和代码的行数没有直接的关联性,但是有间接的关联性。

然而短小的函数并不一定就意味着就不需要拆分,只要不满足SRP,就值得进一步分解。

哪怕分解后的子函数只有一行代码,只要有助于业务语义显性化的表达,就是值得的。

举例说明,下面是一个给员工发工资的简单方法:

代码语言:javascript
复制
 public void pay(List employees){ 
     for (Employee e: employees){ 
         if(e.isPayDay()){ 
             Money pay = e.calculatePay(); e.deliverPay(pay); 
         } 
     } 
 }

这段代码非常短小,但实际上做了3件事情:遍历所有雇员,检查是否该发工资,然后支付薪水。

按照SRP的原则,以下面的方式改写更好:

代码语言:javascript
复制
public void pay(List employees){ 
    for (Employee e: employees){ 
        payIfNecessary(e); 
    } 
} 
private void payIfNecessary(Employee e) { 
    if(e.isPayDay()){ 
        calculateAndDeliverPay(e); 
} 
} 
private void calculateAndDeliverPay(Employee e) { 
    Money pay = e.calculatePay(); e.deliverPay(pay); 
}

虽然原来的方法并不复杂,但按照SRP分解后的代码显然更加容易让人读懂,这种拆分是有积极意义的。

基本上,遵循SRP的函数都不会太长,再配上合理的命名,就不难得到我们想要的短小的函数。

3.7 精简辅助代码

所谓的辅助代码(Assistant Code),是程序运行中必不可少的代码,但又不是处理业务逻辑的核心代码,比如判空、打印日志、鉴权、降级和缓存检查等。这些代码往往会在多个函数中重复冗余,减少辅助代码可以让代码显得更加干净整洁,易于维护。

模板设计模式,回调函数写logger能避免这个问题 让函数中的代码能直观地体现业务逻辑,而不是让业务代码淹没在辅助代码中。

XXXX.check(Target target);

让函数中的代码能直观地体现业务逻辑,而不是让业务代码淹没在辅助代码中。

下面来看一个简单的示例,假如我们要获取一个如下的稍有一定嵌套深度的属性值。

代码语言:javascript
复制
String isocode = user.getAddress(). getCountry(). getIsocode(). toUpperCase();

因为任何访问对象方法或属性的调用都可能导致NPE,因此如果我们要确保不触发异常,就得在访问每一个值之前对其进行明确的检查:

代码语言:javascript
复制
if (user != null) { 
    Address address = user.getAddress(); 
    if (address != null) { 
        Country country = address.getCountry(); 
        if (country != null) { 
            String isocode = country.getIsocode(); 
            if (isocode != null) { 
                isocode = isocode.toUpperCase(); 
            } 
        } 
    }
}

我们可以用Optional来代替冗长的null检查:

代码语言:javascript
复制
String isocode = Optional.ofNullable(user).flatMap(User::getAddress).flatMap(Address::getCountry).map(Country::getIsocode).orElse("default");

可以看到,新的写法比旧的判空方式在复杂度和简洁性上都提升了很多,简洁也是一种美。

该方法的功能其实很简单,就是根据传入的productId集合批量查询Product,由于实现逻辑中夹杂着缓存逻辑,所以整体代码显得臃肿,让人看着很不舒服。实际上,我们完全可以自研一个缓存框架,使用注解(Annotation)来代替这些铅板代码(Boilerplate Code)。如果使用这种方式重构上面的代码,可以得到如下代码:

代码语言:javascript
复制
@MultiCacheable(cacheNames ="product") 
public List getProducts(@CacheKeyList List productIds, @CacheNotHit List notExistIds) { 
    return notExistIds.stream().map(
        productId -> getProductsById (productId)).collect(Collectors.toList()); 
}

可以看到,重构后的代码清晰了很多。而我们现在只需要关注业务逻辑本身,缓存这个技术细节的辅助代码被从业务逻辑中剥离出去,并进行统一维护,既减少了重复,又避免了和具体缓存实现的耦合,可谓是一举多得。

Spring Cloud Hystrix为我们提供了一个非常优雅的解决方案。利用Hystrix提供的API,我们可以使用注解的方式定义降级服务,从而不用在业务逻辑里面使用try/catch来做异常情况下的服务降级。

代码语言:javascript
复制
public class UserService {
    @Autowired
    private RestTemplate restTemplate;
    @HystrixCommand(fallbackMethod = "defaultUser")
    public User getUserById(Long id){
        return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class, id);
    }
    //在远程服务不可用时,使用降级方法:defaultUser
    public User defaultUser(){
        return new User();
    }
}

3.8 组合函数模式

组合函数模式(Composed Method Pattern)出自Kent Beck的Smalltalk Best Practice Patterns一书,是一个非常容易理解上手、实用,对代码可读性和可维护性起到立竿见影效果的编程原则。组合函数要求所有的公有函数(入口函数)读起来像一系列执行步骤的概要,而这些步骤的真正实现细节是在私有函数里面。组合函数有助于代码保持精炼并易于复用。阅读这样的代码就像在看一本书,入口函数是目录,目录的内容指向各自的私有函数,而具体的内容是在私有函数里实现的。

3.9 SLAP

抽象层次一致性(Single Level of Abstration Principle,SLAP),是和组合函数密切相关的一个原则。组合函数要求将一个大函数拆成多个子函数的组合,而SLAP要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,就会显得凌乱,难以理解。

举个例子,假如有一个冲泡咖啡的原始需求,其制作咖啡的过程分为3步。

(1)倒入咖啡粉。

(2)加入沸水。

(3)搅拌。

其伪代码(pseudo code)如下:

代码语言:javascript
复制
public void makeCoffee() { 
    pourCoffeePowder(); 
    pourWater(); 
    stir();
}

如果要加入新的需求,比如需要允许选择不同的咖啡粉,以及选择不同的风味,那么代码就会变成这样:

代码语言:javascript
复制
public void makeCoffee(boolean isMilkCoffee, boolean isSweetTooth, CoffeeType type) { 
    //选择咖啡粉 
    if (type == CAPPUCCINO) { 
        pourCappuccinoPowder(); 
    } else if (type == BLACK) { 
        pourBlackPowder(); 
    } else if (type == MOCHA) { 
        pourMochaPowder(); 
    } else if (type == LATTE) { 
        pourLattePowder(); 
    } else if (type == ESPRESSO) { 
        pourEspressoPowder(); 
    } 
    //加入沸水 
    pourWater(); 
    //选择口味 
    if (isMilkCoffee) { 
        pourMilk(); 
    } 
    if (isSweetTooth) { 
        addSugar(); 
    } 
    //搅拌 
    stir(); 
}

如果继续有更多的需求加入,那么代码会进一步恶化,最后变成一个谁也看不懂且难以维护的逻辑迷宫

再回看上面的代码,新需求的引入当然是根本原因。

但除此之外,另一个原因是新代码已经不再满足SLAP了。

具体选择用什么样的咖啡粉是倒入咖啡粉这个步骤应该去考虑的实现细节,和主流程步骤不在一个抽象层次上。

同理,加奶和加糖也是实现细节。因此,在引入新需求以后,制作咖啡的主要步骤从原来的3步变成了4步。

(1)倒入咖啡粉,会有不同的选择。

(2)加入沸水。

(3)调味,根据需求加糖或加奶。

(4)搅拌。按照组合函数和SLAP原则,我们要在入口函数中只显示业务处理的主要步骤。

具体的实现细节通过私有方法进行封装,并通过抽象层次一致性来保证,一个函数中的抽象在同一个水平上,而不是高层抽象和实现细节混杂在一起。根据SLAP原则,我们可以将代码重构为:

代码语言:javascript
复制
public void makeCoffee(boolean isMilkCoffee, boolean isSweetTooth, CoffeeType type) { 
    //选择咖啡粉 
    pourCoffeePowder(type); 
    //加入沸水 
    pourWater(); 
    //选择口味 
    flavor(isMilkCoffee, isSweetTooth); 
    //搅拌 
    stir(); } 
private void flavor(boolean isMilkCoffee, boolean isSweetTooth) { 
    if (isMilkCoffee) { 
        pourMilk(); 
    } 
    if (isSweetTooth) { 
        addSugar(); 
    } 
} 
private void pourCoffeePowder(CoffeeType type) { 
    if (type == CAPPUCCINO) { 
        pourCappuccinoPowder(); 
    } else if (type == BLACK) { 
        pourBlackPowder(); 
    } else if (type == MOCHA) { 
        pourMochaPowder(); 
    } else if (type == LATTE) { 
        pourLattePowder(); 
    } else if (type == ESPRESSO) { 
        pourEspressoPowder(); 
    } 
}

重构后的makeCoffee()又重新变得整洁如初了,满足SLAP实际上是构筑了代码结构的金字塔。金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。关于金字塔原理的更多内容,请参考8.5.3节

接下来我们来看Spring的“好味道”。在Spring中,做上下文初始化的核心类AbstractApplicationContext的refresh()函数为我们在遵循SLAP方面做了一个很好的示范。

代码语言:javascript
复制
public void refresh()throws BeansException,IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();
            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);
            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);
                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);
                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);
                // Initialize message source for this context.
                initMessageSource();
                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();
                // Initialize other special beans in specific context subclasses.
                onRefresh();
                // Check for listener beans and register them.
                registerListeners();
                // Instantiate all remaining(non-lazy-init)singletons.
                finishBeanFactoryInitialization(beanFactory);
                // Last step: publish corresponding event.
                finishRefresh();
            }
            catch (BeansException ex) {
                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();
                // Reset 'active' flag.
                cancelRefresh(ex);
                // Propagate exception to caller.
                throw ex;
            }
            finally {
                // Reset common introspection caches in Spring's core,
                // since we might not ever need metadata for singleton
                // beans anymore...
                resetCommonCaches();
            }
        }
    }

试想,如果上面的代码逻辑不是这样写,而是平铺在refresh()函数中,结果会是怎样?

3.10 函数式编程

函数式编程中最重要的特征之一,就是你可以把函数(你的代码)作为参数传递给另一个函数。为什么这个功能很重要呢?主要有以下两个原因。减少冗余代码,让代码更简洁、可读性更好。·函数是“无副作用”的,即没有对共享的可变数据操作,可以利用多核并行处理,而不用担心线程安全问题。

4种不同方式的代码实现。

(1)经典类实现。

(2)匿名类实现。

(3)Lamda实现。

(4)方法引用实现。

3.11 本章小结

一个系统容易腐化的部分正是函数,不解决函数的复杂性,就很难解决系统的复杂性。

第4章 设计原则

原则并非是形而上学的静态客观真理,不是说每一个设计都要教条地遵守每一个原则,而是要根据具体情况进行权衡和取舍。

4.1 SOLID概览

SOLID是5个设计原则开头字母的缩写,其本身就有“稳定的”的意思,寓意是“遵从SOLID原则可以建立稳定、灵活、健壮的系统”。5个原则分别如下。

Single Responsibility Principle(SRP):单一职责原则。

Open Close Principle(OCP):开闭原则。

Liskov Substitution Principle(LSP):里氏替换原则。

Interface Segregation Principle(ISP):接口隔离原则。

Dependency Inversion Principle(DIP):依赖倒置原则。

开闭原则和里氏代换原则是设计目标;单一职责原则、接口分隔原则和依赖倒置原则是设计方法。

设计模式亦遵循这几个原则

### 4.3 OCP

很多的设计模式都以达到OCP目标为目的。例如,装饰者模式,可以在不改变被装饰对象的情况下,通过包装(Wrap)一个新类来扩展功能;策略模式,通过制定一个策略接口,让不同的策略实现成为可能;适配器模式,在不改变原有类的基础上,让其适配(Adapt)新的功能;观察者模式,可以灵活地添加或删除观察者(Listener)来扩展系统的功能。

4.4 LSP

警惕instanceof这样的设计实际上破坏了LSP原则,因为在Retangle出现的地方使用Square进行替换,就会抛出异常。实际上,这也是一个著名的设计问题——正方形-矩形问题(Square-rectangle Problem)

4.5 ISP

包括maven依赖也是这样最大的好处是可以将外部依赖减到最少。你只需要依赖你需要的东西,这样可以降低模块之间的耦合(Couple)。

4.6 DIP

然而“面向接口编程”只是实现DIP的一个技法,DIP本身的意义要宽泛得多,它是一种思想,是一种软件设计的哲学。

图4-8 依赖倒置后的Logger依赖所以我强烈建议所有的业务系统都应该有这样一个Logger抽象,来屏蔽对具体Logger框架的依赖。这也是为什么我们要在COLA中引入新的Logger抽象,目的就是要和具体的Logger框架进行解耦。

DIP同样有着重要的指导意义

领域层不应该直接依赖基础设施层,它们之间的解耦就是通过DIP完成的

4.7 DRY

DRY是Don’t Repeat Yourself的缩写,DRY原则特指在程序设计和计算中避免重复代码,因为这样会降低代码的灵活性和简洁性,并且可能导致代码之间的矛盾,“散弹式修改”是Robert Martin在《重构》一书中列出的一个典型代码“坏味道”

4.8 YAGNI

YAGNI是针对“大设计”(Big Design)提出来的,是“极限编程”提倡的原则,是指你自以为有用的功能,实际上都是用不到的。因此,除了核心的功能之外,其他的功能一概不要提前设计,这样可以大大加快开发进程。它背后的指导思想就是尽可能快、尽可能简单地让软件运行起来。但是,这里出现了一个问题。仔细推敲,你会发现DRY原则和YAGNI原则是不兼容的。前者追求“抽象化”,要求找到通用的解决方法;后者追求“快和省”,意味着不要把精力放在抽象化上面,因为很可能“你不会需要它”。因此,就有了Rule of Three原则。

Rule of Three也被称为“三次原则”,是指当某个功能第三次出现时,就有必要进行“抽象化”了。这也是软件大师Martin Fowler在《重构》一书中提出的思想。三次原则指导我们可以通过以下步骤来写代码。

(1)第一次用到某个功能时,写一个特定的解决方法。

(2)第二次又用到的时候,复制上一次的代码。

(3)第三次出现的时候,才着手“抽象化”,写出通用的解决方法。

这3个步骤是对DRY原则和YAGNI原则的折中,是代码冗余和开发成本的平衡点。

4.10 KISS原则

好的目标不是越复杂越好,反而是越简洁越好。

我们一定要理解“简单”和“简陋”的区别。

4.11 POLA原则

POLA(Principle of least astonishment)是最小惊奇原则,写代码不是写侦探小说,要的是简单易懂,而不是时不时冒出个“Surprise”。

第5章 设计模式

设计模式(Design Pattern)是一套代码设计经验的总结,并且该经验必须能被反复使用,被多数人认可和知晓。设计模式描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案,具有一定的普遍性,可以反复使用。其目的是提高代码的可重用性、可读性和可靠性。设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。正确使用设计模式,可以提高程序员的思维能力、编程能力和设计能力,使程序设计更加标准化、代码编制更加工程化,从而大大提高软件开发效率。

5.2 GoF

根据模式所完成的工作类型来划分,模式可分为创建型模式、结构型模式和行为型模式,如图5-2所示。

(1)创建型模式:用于描述“怎样创建对象”,主要特点是“将对象的创建与使用分离”。GoF中提供了单例、原型、工厂方法、抽象工厂、建造者5种创建型模式。(2)结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF中提供了代理、适配器、桥接、装饰、外观、享元、组合7种结构型模式。(3)行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器11种行为型模式。以上提到了GoF23种设计模式的分类,简要介绍如下。

(1)单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点,以便外部获取该实例,其拓展是有限多例模式。

(2)原型(Prototype)模式:将一个对象作为原型,通过对其进行复制操作而复制出多个和原型类似的新实例。

(3)工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。

(4)抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。

(5)建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同的需要分别创建它们,最后构建成该复杂对象。

(6)代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问,即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。(7)适配器(Adapter)模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

(8)桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。

(9)装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。

(10)外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。

(11)享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。

(12)组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。

(13)模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使子类可以在不改变该算法结构的情况下,重定义该算法的某些特定步骤。

(14)策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。

(15)命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。

(16)职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式可以去除对象之间的耦合。(17)状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。

(18)观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。

(19)中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。

(20)迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

(21)访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。

(22)备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。

(23)解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。

5.3 拦截器模式

我们可以按照下面的步骤实现一个拦截器模式。

拦截器模式完全是利用面向对象技术的,巧妙地使用组合模式外加递归调用实现了灵活、可扩展的前置处理和后置处理。在拦截器模式中,主要包含以下角色。·TargetInvocation:包含了一组Interceptor和一个Target对象,确保在Target处理请求前后,按照定义顺序调用Interceptor做前置和后置处理。·Target:处理请求的目标接口。·TargetImpl:实现了Target接口的对象。·Interceptor:拦截器接口。·InterceptorImpl:拦截器实现,用来在Target处理请求前后做切面处理。各角色之间的关系如图5-4所示。

我们可以按照下面的步骤实现一个拦截器模式。

(1)创建Target接口。

代码语言:javascript
复制
public interface Target{
    public Response execute(Request request);
}

(2)创建Interceptor接口。

代码语言:javascript
复制
 public interface Interceptor {
    public Response intercept(TargetInvocation targetInvocation);
}

(3)创建TargetInvocation。

代码语言:javascript
复制
public class TargetInvocation {
    private List<Interceptor> interceptorList = new ArrayList<>();
    private Iterator<Interceptor> interceptors;
    private Target target;
    private Request request;
    public Response invoke(){
        if( interceptors.hasNext() ){
            Interceptor interceptor = interceptors.next();
            //此处是整个算法的关键,这里会递归调用invoke()
            interceptor.intercept(this);//2
        }
        return target.execute(request);
    }
    public void addInterceptor(Interceptor interceptor){
        //添加新的Interceptor到TargetInvocation中
        interceptorList.add(interceptor);
        interceptors = interceptorList.iterator();
    }
}

(4)创建具体的Interceptor。AuditInterceptor实现如下:

代码语言:javascript
复制
public class AuditInterceptor implements Interceptor{
    @Override
    public Response intercept(TargetInvocation targetInvocation) {
        if(targetInvocation.getTarget() == null) {
            throw new IllegalArgumentException("Target is null");
        }
        System.out.println("Audit Succeeded ");
        return targetInvocation.invoke();
    }
}

LogInterceptor实现如下:

代码语言:javascript
复制
public class LogInterceptor implements Interceptor {
    @Override
    public Response intercept(TargetInvocation targetInvocation) {
        System.out.println("Logging Begin");
        Response response = targetInvocation.invoke();
        System.out.println("Logging End");
        return response;
    }
}

(5)使用InterceptorDemo来演示拦截器设计模式。

代码语言:javascript
复制
public class InterceptorDemo {
    public static void main(String[] args) {
        TargetInvocation targetInvocation = new TargetInvocation();
        targetInvocation.addInterceptor(new LogInterceptor());
        targetInvocation.addInterceptor(new AuditInterceptor());
        targetInvocation.setRequest(new Request());
        targetInvocation.setTarget(request->{return new Response();});
        targetInvocation.invoke();
    }
}

(6)执行程序,输出结果。

代码语言:javascript
复制
Logging Begin
Audit Succeeded 
Logging End

5.4 插件模式

试一试chrom的插件和idea的插件 插件模式的实现原理和策略模式类似,要求主程序中做好扩展点接口的定义,然后在插件中进行扩展实现。因此,插件模式的难点不在于如何开发插件,而在于如何实现一套完整的插件框架。在一个插件框架中,通常会涉及以下概念。ExtensionPoint:扩展点,用来标识可以扩展的功能点。Extension:扩展,是对ExtensionPoint的扩展实现。PluginDescriptor:插件描述,即描述插件的元数据,定义了包括对外暴露的扩展点,运行插件所需要的依赖等信息。一个PluginDescriptor对应一个Plugin.xml配置。PluginRegistry:插件注册,用来进行插件注册和存储。PluginManager:插件管理,用来装载和激活插件实例。Plugin:插件实例,当PluginManager调用activate方法激活Plugin时,就会产生一个Plugin实例。上述概念之间的关系如图5-5所示。

有关这些实现细节,我推荐一个开源项目JPF(Java Plug-in Framework),它受到了Eclipse的插件式启发,致力于打造一个通用的Java插件框架。有兴趣的读者可以访问SOURCEFORGE的JPF相关页面获取相关资料和源代码。

5.5 管道模式

5.5.1 链式管道看过Tomcat源码或阿里巴巴开源的MVC框架WebX源码的读者,应该对其中的管道(Pipeline)和阀门(Valve)不会陌生。一个典型的管道模式,会涉及以下3个主要的角色。

(1)阀门:处理数据的节点。

(2)管道:组织各个阀门。

(3)客户端:构造管道并调用。对应现实生活中的管道,我们一般使用一个单向链表数据结构作为来实现,如图5-6所示。

这也是链式管道区别于拦截器模式之处。其实在功能上,拦截器、管道、过滤器、责任链有类似之处,在实际工作中,我们可以根据具体情况灵活选用。

支撑Stream API背后的原理正是管道模式。在构建Stream时,会调用核心类ReferencePipeline来创建管道,其内部采用双向列表的数据结构对操作(Operation)进行存放,然后包(wrap)成Sink链表等待执行,整个处理是延迟执行的,只有在最后收集(Collect)被调用时才会被执行。

5.6 本章小结

设计模式只是一种工具或手段,而不是目的。千万不要为了让程序看起来更有设计感,而在场景中套用设计模式。

第6章 模型

6.1 什么是模型

模型是对现实世界的简化抽象。

只要有助于我们对问题域的理解,均可认为是好的模型。

根据使用场景的不同,模型大致可以分为物理模型、概念模型、数学模型和思维模型等。

◆ 6.2 UML

在软件领域,影响力最强的建模工具当属统一建模语言(Unified Modeling Language,UML)了。

UML分为结构型和行为型建模图形,具体分类如图6-4所示。

参考资料《面向对象分析与设计》和Larman的《UML和模式应用》这两本书。

6.3 类图

组合(Composition)关系也表示类之间整体和部分的关联关系

在用代码实现组合关系时,通常在整体类的构造方法中直接实例化成员类。成员对象域整体对象有同样的生命周期,也就是要“共生死”,这也是组合和聚合的主要区别

代码上的体现是组合没有Setter方法,图6-11对应的Java代码片段如下:

代码语言:javascript
复制
public class Head {
    private Mouth mouth;
    public Head() {
        mouth = new Mouth(); //实例化成员类
    }
}
public class Mouth {
}

依赖(Dependency)关系是一种使用关系,特定事物的改变可能会影响到使用该事物的其他事物,

在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方

在系统实施阶段,依赖关系通常通过3种方式来实现。

(1)第一种方式(也是常用的一种方式)是将一个类的对象作为另一个类中方法的参数,如图6-12所示。[插图]图6-12 依赖关系实例

(2)第二种方式是在一个类的方法中将另一个类的对象作为其局部变量。

(3)第三种方式是在一个类的方法中调用另一个类的静态方法。

第一种方式对应的Java代码片段如下:

代码语言:javascript
复制
public class Teacher {
    public void use(Projector projector) {
        projector.demonstrate();
    }
}
public class Projector {
    public void demonstrate() {
    }
}

6.4 领域模型

算法加数据结构?不是,是数据加展示 在理解领域模型之前,我们先思考一下软件开发的本质是什么。从本质上来说,软件开发过程就是问题空间到解决方案空间的一个映射转化,如图6-16所示

6.5 敏捷建模

重点是交付软件,而不是交付模型。模型能带来价值时,我们就使用;如果模型没有价值,不能加速软件的交付,就不创建它们。

6.6 广义模型

C4模型由Simon Brown提出。C4模型提出使用上下文(Context)、容器(Container)、组件(Component)和代码(Code)等一系列分层的图表,来描述不同缩放级别的软件架构,其主要构件如图6-17所示。[插图]图6-17 C4模型中的主要构件

UI流程图使用页面之间的流转来描述系统交互流程。用户可以通过UI流程图进行业务分析和检查,UI流程图也可以作为系统文档向新人介绍业务。如图6-18所示,UI流程图和C4模型一样,虽然不是标准的UML,但也非常实用。

除描述技术以外,用户也可以用图形化的方式来描述业务

图6-20是关于O2O就医的流程,这张图非常巧妙地使用了线条(line),线条本身除了表达时间顺序外,还用来作为线上(online)和线下(offline)的区隔,线条上面的是online,下面的是offline,直观明了,让人印象深刻。

图6-20 基本就医的O2O流程

6.7 本章小结

建模并不意味着要用特定的符号、工具和流程。不管你用什么建模工具、什么表示法,只要有助于对问题域的理解,就是好的模型。

第7章DDD的精髓

7.1 什么是DDD

这些数据对象除了简单的setter/getter方法外,不包含任何业务逻辑,业务逻辑都是以过程式的代码写在Service中。这种方式极易上手,但随着业务的发展,系统也很容易变得混乱复杂。

7.3 数据驱动和领域驱动

7.3 数据驱动和领域驱动7.3.1 数据驱动目前主流的开发模式是由数据驱动的。

数据驱动的开发很容易上手,有了业务需求,创建数据库表,然后编写业务逻辑,

开发过程如图7-1所示。数据驱动以数据库为中心,其中最重要的设计是数据模型,但随着业务的增长和项目的推进,软件开发和维护的难度会急剧增加。[插图]图7-1 数据驱动研发过程以客户关系管理(Customer Relationship Management,CRM)为例,其中很重要的概念有销售、机会、客户、私海、公海,实体的定义分别如下。

销售(Sales):公司的销售人员,一个销售可以拥有多个销售机会。

机会(Opportunity):销售机会,每个机会包含至少一个客户信息,且归属于一个销售人员。

客户(Customer):客户,也就是销售的对象。

私海(Private sea):专属于某个销售人员的领地(Territory),私海里面的客户,其他销售人员不能触碰。

公海(Public sea):公共的领地,所有销售人员都可以从公海里捡入客户到其私海。

通过DDD的战略设计和战术设计,我们可以为问题域划分出合适的子域,并对域中的业务进行建模。

复杂的数据库关系和对象关系之间的差异,其本质是数据模型和领域模型之间的差异,而这种差异的多样性和灵活性是很难通过规则预先定义的,这也是为什么工具的作用会很有限。现在的互联网大厂大多使用MyBatis,原因也在于此。因此,如果你打算实践DDD,请一定不要让工具帮你去建模,工具不会抽象,也不会思考,还是要老老实实自己动手去建。

7.4 DDD的优势

业务侧拥有需要实现的概念。业务语言中的术语由公司的的业务侧和技术侧通过协商来定义(意味着业务侧也不能总是选到最好的命名),目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言。代码、类、方法、属性和模块的命名必须和统一语言相匹配,必要的时候需要对代码进行重构!

DDD的核心是领域模型,这一方法论可以通俗地理解为先找到业务中的领域模型,以领域模型为中心,驱动项目开发。

重点! DDD鼓励我们接触到需求后第一步就是考虑领域模型,而不是将其切割成数据和行为,然后用数据库实现数据,用服务实现行为,最后造成需求的首尾分离。DDD会让你首先考虑业务语言,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。重点不同,导致编程世界观不同。

代码复杂度是由业务复杂度和技术复杂度共同组成的。实践DDD还有一个好处,是让我们有机会分离核心业务逻辑和技术细节,让两个维度的复杂度有机会被解开和分治

图7-8 业务逻辑和技术细节分离的架构

### 7.5 DDD的核心概念

以上就是我在实际工作中寻找领域实体的大致过程。从方法论的角度来说,也叫作“用例分析法”,详细的步骤会在7.6.1节中介绍。

聚合根(Aggregate Root)是DDD中的一个概念,是一种更大范围的封装,会把一组有相同生命周期、在业务上不可分割的实体和值对象放在一起,只有根实体可以对外暴露引用,这也是一种内聚性的表现。确定聚合边界要满足固定规则(Invariant),是指在数据变化时必须保持的一致性规则,具体规则如下。·根实体具有全局标识,最终负责检查规定规则。·聚合内的实体具有本地标识,这些标识在Aggregate内部才是唯一的。·外部对象不能引用除根Entity之外的任何内部对象。·只有Aggregate的根Entity才能直接通过数据库查询获取,其他对象必须通过遍历关联来发现。·Aggegate内部的对象可以保持对其他Aggregate根的引用。·Aggregate边界内的任何对象在修改时,整个Aggregate的所有固定规则都必须满足。

可以看寒食的视频

https://www.bilibili.com/video/BV1wS4y127QG/?spm_id_from=333.999.0.0

仍以银行转账的例子来说明,如图7-10所示,

比如收单,拦截 有些领域中的动作是一些动词,看上去并不属于任何对象。它们代表了领域中的一个重要的行为,所以不能忽略它们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时,推荐的实践方式是将它声明成一个服务。这样的对象不再拥有内置的状态,其作用仅仅是为领域提供相应的功能。Service往往是以一个活动来命名,而不是Entity来命名。

识别领域服务,主要看它是否满足以下3个特征。(1)服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。(2)被执行的操作涉及领域中的其他对象。(3)操作是无状态的。

消息作为分布式系统间耦合度最低、最健壮、最容易扩展的一种通信机制,是我们实现分布式系统互通的重要手段。

事件命名,重中之重!!1.事件命名 事件是表示发生在过去的事情,所以在命名上推荐使用Domain Name +动词的过去式+ Event,这样可以更准确地表达业务语义。

2.事件内容事件内容在计算机术语中叫作payload,有以下两种形式。

(1)自恰(Enrichment):就是在事件的payload中尽量多放数据,这样consumer不需要回查就能处理消息,也就是自恰地处理消息。

(2)回查(Query-Back):这种方式是只在payload放置id属性,然后consumer通过回调的形式获取更多数据。这种形式会加重系统的负载,可能会引起性能问题。

在复杂接口通过条件查询选择是否把调用记录,过滤信息,子订单,过期单据追加进去 事件内容在计算机术语中叫作payload,有以下两种形式。

领域实体的意义是有上下文的,比如同样是Apple,在水果店和苹果手机专卖店中表达出的含义就完全不一样。边界上下文(Bounded Context)的作用是限定模型的应用范围,在同一个上下文中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。

dto和do 不同上下文之间的实体也需要映射。在DDD中,这种机制叫作上下文映射(Context Mapping),我们可以使用防腐层(Anti-Corruption)来完成映射的工作。

7.6 领域建模方法

1.方法介绍用例分析法是进行领域建模中最简单可行的方式,其步骤如下。

(1)获取用例描述

关联意味着两个模型之间存在语义联系,在用例中的表现通常为两个名词被动词连接起来,如图7-12所示。

如何判断一个名词是否是属性?可以用下面两种方式。·能完全通过基本数据类型(数字、文本、日期)表达的大多是属性。·如果一个名词只关联一个概念类,并且其自身没有属性,那么它就是另一个概念类的属性。

通常来说,我们会将更内聚的一组模型划分为一个子领域,形成更高一层的抽象,有利于系统的表达和分工。

接下来,我们按照用例分析法的步骤来建模。(1)寻找概念类首把所有名词标记出来,作为概念类的候选类:vendors, sales employees, companies, basic wage, commission, order, order number。(2)添加关联如图7-13所示,接下来为名词添加关联,连接这些名词的动词会出现在关联的线上。注意,根据上面的用例,我们还不清楚给供应商(Vendor)支付佣金(Commission)的主体是谁,但这并不妨碍在本阶段的建模。

(3)添加属性最后,为这些候选的概念类选择属性。在本例中,如果一个概念类只处于一个被动的关联关系中(如Basic Wage, Commission, OrderNumber),那么它需要作为关联类的属性,如图7-14所示。[插图]图7-14 添加属性示例

在四色模型中,我们将抽象出来的对象分成4种原型(archetype)。

(1)业务关键时刻(Moment-Interval)这种对象表示那些在某个时间点存在或者会存在一段时间。这样的对象往往表示了一次外界的请求,比如一次询价(Quotation)、一次下单(Order)或者一次租赁

Moment-Interval是最重要的一类对象,是系统的价值所在,一般用粉红色来表示

(2)角色(Role)这种对象表示一种角色,往往由人或者物来承担,会有相应的责任和权利。

例如,一次下单涉及两个Role,分别是客户(Customer)和商品(Product)。一般用黄色来表示。

(3)人-事-物(Party,Place or Thing)这种对象往往表示一种客观存在的事物,例如人、组织、产品或者配件等,这些事物会在一种moment-interval中扮演某个Role。例如,某个人既会在一次购买中扮演Customer的角色,也可以在询价中扮演询价人的角色。这类对象的重要程度排在第三,一般用绿色来表示。

(4)描述(Description)这种对象一般是用于分类或者描述性的对象,它的属性一般是这一类事物都有的属性,一般用蓝色来表示。

从上面这个故事中我们可以看到:任何的业务事件都会以某种数据的形式留下足迹。

所以企业的业务系统的主要目的之一,就是记录这些足迹,并将这些足迹形成一条有效的追溯链。

图7-17 在线电子书店的业务关键时刻对象

图7-18 在线电子书店的人-事-物对象

图7-19 在线电子书店的角色对象最后,把一些需要描述的信息放

(1)首先以满足管理和运营的需要为前提,寻找需要追溯的事件,或者称为关键业务时刻。

(2)根据这些需要追溯,寻找足迹以及相应的关键业务时刻对象。

(3)寻找“关键业务时刻”对象周围的“人-事-物”对象。

(4)从“人-事-物”中抽象出角色。

(5)把一些描述信息用对象补足。

7.7 模型演化

世界上唯一不变的就是变化,模型和代码一样,也需要不断地重构和演化。在每一次演化之后,开发人员应该对领域知识都会有更加清晰的认识,这使得理解上的突破成为可能。

我们得到了更符合用户需要且更加切合实际的模型,其功能性及说明性急速提升,而复杂性却随之降低。这种突破需要我们对业务有更加深刻的领悟和思考。

7.8 为什么DDD饱受争议

很多人是通过阅读《领域驱动设计:软件核心复杂性应对之道》开始入门领域驱动设计的,这本书中提到了很多概念,比如Repository、Domain和ValueObject等,但是初学者可能会误认为在项目架构中加入Repository、Domain和ValueObject就变成了DDD架构。如果没有悟出其精髓就在项目中加入这些概念,那充其量也不过是“老三层架构”的变种;反之,对于一个面向对象分析的高手而言,不使用这些概念也可以实现领域驱动设计。

不同于纯粹的技术,领域建模的确十分依赖经验,更加依赖个人的综合能力。因此,如果团队决定实施DDD,必须要有一个经验丰富的人来带领,否则,不合理的抽象还不如没有抽象。

图7-21是一个非常流行的关于DDD架构的分层结构,我们可以看到Domain是对Infrastructure有依赖的。

图7-21 DDD的架构分层

作者提出整洁的架构应该是“核心业务逻辑和技术细节相分离”的,才触发了我对Domain依赖Infrastructure合理性的重新思考

第二部分 思想

第8章 抽象

◆ 8.3 抽象是OO的基础

面向对象(Object Oriented,OO)的思考方式,就是万物皆对象。抽象帮助我们将现实世界的对象抽象成类,完成从现实世界的概念到计算机世界的模型的映射。

面向对象的思想主要包括3个方面:面向对象的分析(Object Oriented Analysis,OOA)、面向对象的设计(Object Oriented Design,OOD),以及我们经常提到的面向对象的编程(Object Oriented Programming,OOP)。OOA是根据抽象关键问题域来分解系统。OOD是一种提供符号设计系统的面向对象的实现过程,它用非常接近实际领域术语的方法把系统构造成“现实世界”的抽象。OOP可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反,传统的程序设计主张将程序看作一系列函数的集合,或者更直接些,就是一系列对计算机下达的指令。

8.4 抽象的层次性

内涵越小,外延越大;内涵越大,外延越小。不同层次的抽象有不同的用途。

这种抽象的层次性基本可以体现在任何事物上,以下是对一份报纸在多个层次上的抽象。

(1)第一层:一个出版物。

(2)第二层:一份报纸。

(3)第三层:《旧金山纪事报》

(4)第四层:5月18日出版的《旧金山纪事报》

(5)第五层:我拥有的5月18日出版的《旧金山纪事报》。如果我要统计美国有多少家出版单位,那么就要用到第一层“出版单位”的抽象。如果我要查询旧金山5月18日当天的新闻,那么就要用到第五层的抽象。

“软件领域的任何问题,都可以通过增加一个间接的中间层来解决。”

### 8.5 如何进行抽象

当我们发现有些东西无法归到一个类别中时,该怎么办呢?此时,我们可以通过上升一个抽象层次的方式,让它们在更高的抽象层次上产生逻辑关系。

例如,你可以合乎逻辑地将苹果和梨归类为水果,也可以将桌子和椅子归类为家具。但是怎样才能将苹果和椅子放在同一组中呢?仅仅提高一个抽象层次是不够的,因为上一个抽象层次是水果和家具的范畴。因此,你必须提高到更高的抽象层次,比如将其概括为“商品”。如果我们想把大肠杆菌也纳入其中,该怎么办呢?此时,“商品”这个抽象也不够用了,需要再提高一个抽象层次,比如叫“物质”(见图8-3)。但是这样的抽象太过于宽泛,难以说明思想之间的逻辑关系。类似于我们在Java中的顶层父类对象(Object),万物皆对象。

代码的坏味道 这里的代码有个问题,就是instanceof的使用。

每当我们有强制类型转换,或者使用instanceof时,都值得停下来思考一下,是否需要做抽象层次的提升。

要自下而上地思考,总结概括;自上而下地表达,结论先行。

因为我们的大脑短期记忆无法一次容纳7个以上的记忆项目并超过5个小时,我们就会开始将其归类到不同的逻辑范畴,以便记忆。

分类的作用不只是将一组中的9个概念分成每组各有4个、3个和2个概念的3组概念,因为这样还是9个概念。你所要做的是提高一个抽象层次,将大脑需要处理的9个概念变成3个概念。这样,你无须再记忆9个概念中的每一个概念,仅需记忆9个概念所属的3个组。思维的抽象程度提高了一层,由于处于较高层次的思想总是能够提示其下面一个层次的思想,因而更容易理解和记忆。

### 8.6 如何提升抽象思维

为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面时你的大脑会被铺满,较少需要抽象和想象。这也是我们不提倡小孩子过多地看电视或玩手机的原因,因为不利于锻炼其抽象思维。抽象思维的差别使孩子们的学习成绩从初中开始分化,许多不能适应这种抽象层面训练的孩子可能选择去读职业技校,因为这里比大学更加具象——车铣刨磨、零件部件等都是能够看得到、摸得到的。

小时候,我们可能不理解语文老师为什么总是要求我们总结段落大意、中心思想。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维的能力。做总结最好的方式就是写文章,小到博文,大到一本书,都是锻炼自己抽象思维和结构化思维的机会。记录也是很好的总结习惯。以读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳,这样不仅可以加深理解,还可以提升自己的抽象思维能力。现实世界纷繁复杂,只有具备较强的抽象思维能力的人,才能够具备抓住事物本质的能力。

对于技术人员来说,还有一个非常好的提升抽象能力的手段——领域建模。当我们对问题域进行分析、整理和抽象时,或对领域进行划分和建模时,实际上都是在锻炼我们的抽象能力。关于这一点,我深有感触。当开始使用第6章中介绍的建模方法论进行建模时,我会觉得无从下手,建出来的模型也很别扭。然而,经过几次锻炼之后,我很明显地感觉到自己的建模能力和抽象能力有所提升,不但分析问题的速度更快了,而且建出来的模型也更优雅了。

8.7 本章小结

“抽象”作为名词,代表着一种思维方式,它的伟大之处在于可以让我们撇开细枝末节,去把握事物更本质、更一般的特性,从而更有效地对问题域进行分析设计。“抽象”作为动词,代表着一种能力,它是我们理解概念、理清概念之间逻辑关系的基础,也是我们面向对象分析设计所要求的底层能力。归纳总结,合并同类项是进行抽象活动时最有效的方法。同时,我们也要注意到抽象是有层次性的。当一个概念无法涵盖其外延的时候,我们有必要提升一个抽象层次来减少它的内涵,让其有更大的外延。建议读者一定要多多培养自己的抽象思维。阅读、写文章,以及逻辑思维训练都是提升抽象思维能力非常好的方式。只要坚持学习和锻炼,你慢慢就能体会到一种不一样的美——抽象之美。

第9章 分治

9.2 函数分解

(1)函数长短是职责单一的充分不必要条件,也就是长函数往往意味着职责不单一,但是短函数也不一定就意味着职责单一。

(2)在使用组合函数模式时,要注意抽象层次一致性原则(Single Level of Abstration Principle,SLAP),不同抽象层次的内容放在一起会给人凌乱、逻辑不协调的感觉。

9.3 写代码的两次创造

大部分情况下要经过两次创造:第一遍实现功能,第二遍重构优化。

因此,最好的优化肯定不是等系统上线后再去做,因为这样往往就等于“再也不会去做”(later equals never)。优化工作本应该是我们编码工作的一部分,拆成两步

第10章 技术人员的素养

10.1 不教条

软件的第一性原理是“控制软件复杂度

问题的核心不在于行为和数据是否

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java技术圈子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代码精进之路:从码农到工匠
  • 序一
  • 前言
  • 第一部分 技艺
    • 第一章 命名
      • 1.3 有意义的命名
      • 1.4 保持一致性
      • 1.5 自明的代码
    • 第二章 规范
      • 2.1 认知成本
      • 2.3 代码规范
      • 2.错误码
      • 2.4 埋点规范
      • 2.6 防止破窗
      • 2.7 本章小结
    • 第三章 函数
      • 3.2 软件中的函数
      • 3.3 封装判断
      • 3.4 函数参数
      • 3.5 短小的函数
      • 3.6 职责单一
      • 3.7 精简辅助代码
      • 3.8 组合函数模式
      • 3.9 SLAP
      • 3.10 函数式编程
      • 3.11 本章小结
    • 第4章 设计原则
      • 4.1 SOLID概览
      • 4.4 LSP
      • 4.5 ISP
      • 4.6 DIP
      • 4.7 DRY
      • 4.8 YAGNI
      • 4.10 KISS原则
      • 4.11 POLA原则
    • 第5章 设计模式
      • 5.2 GoF
      • 5.3 拦截器模式
      • 5.4 插件模式
      • 5.5 管道模式
      • 5.6 本章小结
    • 第6章 模型
      • 6.1 什么是模型
      • 6.3 类图
      • 6.4 领域模型
      • 6.5 敏捷建模
      • 6.6 广义模型
      • 6.7 本章小结
    • 第7章DDD的精髓
      • 7.1 什么是DDD
      • 7.3 数据驱动和领域驱动
      • 7.4 DDD的优势
      • 7.6 领域建模方法
      • 7.7 模型演化
      • 7.8 为什么DDD饱受争议
  • 第二部分 思想
    • 第8章 抽象
      • 8.4 抽象的层次性
      • 8.7 本章小结
    • 第9章 分治
      • 9.2 函数分解
      • 9.3 写代码的两次创造
    • 第10章 技术人员的素养
      • 10.1 不教条
相关产品与服务
项目管理
CODING 项目管理(CODING Project Management,CODING-PM)工具包含迭代管理、需求管理、任务管理、缺陷管理、文件/wiki 等功能,适用于研发团队进行项目管理或敏捷开发实践。结合敏捷研发理念,帮助您对产品进行迭代规划,让每个迭代中的需求、任务、缺陷无障碍沟通流转, 让项目开发过程风险可控,达到可持续性快速迭代。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档