标签:point 准则 ast params 概念 ken iter asi 独立
Design is there to enable you to keep changing the software easily in the long term. -- Kent Beck.
正如Kent Beck
所说,软件设计是为了「长期」更加容易地适应未来的变化。正确的软件设计方法是为了长期地、更好更快、更容易地实现软件价值的交付。
软件设计就是为了完成如下目标,其可验证性、重要程度依次减低。
实现功能
易于重用
易于理解
没有冗余
实现功能的目标压倒一起,这也是软件设计的首要标准。如何判定系统功能的完备性呢?通过所有测试用例。
从TDD
的角度看,测试用例就是对需求的阐述,是一个闭环的反馈系统,保证其系统的正确性;及其保证设计的合理性,恰如其分,不多不少;当然也是理解系统行为最重要的依据。
好的设计应该能让其他人也能容易地理解,包括系统的行为,业务的规则。那么,什么样的设计才算得上易于理解的呢?
Clean Code
Implement Patterns
Idioms
没有冗余的系统是最简单的系统,恰如其分的系统,不做任何过度设计的系统。
Dead Code
YAGNI: You Ain‘t Gonna Need It
KISS: Keep it Simple, Stupid
易于重用的软件结构,使得其应对变化更具弹性;可被容易地修改,具有更加适应变化的能力。
最理想的情况下,所有的软件修改都具有局部性。但现实并非如此,软件设计往往需要花费很大的精力用于依赖的管理,让组件之间的关系变得清晰、一致、漂亮。
那么软件设计的最高准则是什么呢?「高内聚、低耦合」原则是提高可重用性的最高原则。为了实现高内聚,低耦合的软件设计,袁英杰提出了「正交设计」的方法论。
「正交」是一个数学概念:所谓正交,就是指两个向量的内积为零。简单的说,就是这两个向量是垂直的。在一个正交系统里,沿着一个方向的变化,其另外一个方向不会发生变化。为此,Bob
大叔将「职责」定义为「变化的原因」。
「正交性」,意味着更高的内聚,更低的耦合。为此,正交性可以用于衡量系统的可重用性。那么,如何保证设计的正交性呢?袁英杰提出了「正交设计的四个基本原则」,简明扼要,道破了软件设计的精髓所在。
消除重复
分离关注点
缩小依赖范围
向稳定的方向依赖
需求1: 存在一个学生的列表,查找一个年龄等于
18
岁的学生
上述实现存在很多设计的「坏味道」:
缺乏弹性参数类型:只支持数组类型,List, Set
都被拒之门外;
容易出错:操作数组下标,往往引入不经意的错误;
幻数:硬编码,将算法与配置高度耦合;
返回null
:再次给用户打开了犯错的大门;
for-each
按照「最小依赖原则」,先隐藏数组下标的实现细节,使用for-each
降低错误发生的可能性。
需求2: 查找一个名字为
horance
的学生
Copy-Paste
是最快的实现方法,但会产生「重复设计」。
为了消除重复,可以将「查找算法」与「比较准则」这两个「变化方向」进行分离。
首先将比较的准则进行抽象化,让其独立变化。
将各个「变化原因」对象化,为此建立了两个简单的算子。
此刻,查找算法的方法名也应该被「重命名」,使其保持在同一个「抽象层次」上。
客户端的调用根据场景,提供算法的配置。
AgePredicate
和NamePredicate
存在「结构型重复」,需要进一步消除重复。经分析两个类的存在无非是为了实现「闭包」的能力,可以使用lambda
表达式,「Code As Data
」,简明扼要。
Iterable
按照「向稳定的方向依赖」的原则,为了适应诸如List, Set
等多种数据结构,甚至包括原生的数组类型,可以将入参重构为重构为更加抽象的Iterable
类型。
需求3: 存在一个老师列表,查找第一个女老师
按照既有的代码结构,可以通过Copy Paste
快速地实现这个功能。
用户接口依然可以使用Lambda
表达式。
assertThat(find(teachers, t -> t.female()), notNullValue());
如果使用Method Reference
,可以进一步地改善表达力。
assertThat(find(teachers, Teacher::female), notNullValue());
分析StudentMacher/TeacherPredicate
, find(Iterable<Student>)/find(Iterable<Teacher>)
的重复,为此引入「类型参数化」的设计。
首先消除StudentPredicate
和TeacherPredicate
的重复设计。
再对find
进行类型参数化设计。
但find
的类型参数缺乏「型变」的能力,为此引入「型变」能力的支持,接口更加具有可复用性。
lambda
Parameterize all the things.
观察如下两个测试用例,如果做到极致,可认为两个lambda
表达式也是重复的。从「分离变化的方向」的角度分析,此lambda
表达式承载的「比较算法」与「参数配置」两个职责,应该对其进行分离。
可以通过「Static Factory Method」
生产lambda
表达式,将比较算法封装起来;而配置参数通过引入「参数化」设计,将「逻辑」与「配置」分离,从而达到最大化的代码复用。
但是,上述将lambda
表达式封装在Factory
的设计是及其脆弱的。例如,增加如下的需求:
需求4: 查找年龄不等于18岁的女生
最简单的方法就是往StudentPredicates
不停地增加「Static Factory Method」
,但这样的设计严重违反了「OCP」(开放封闭)
原则。
从需求看,比较准则增加了众多的语义,再次运用「分离变化方向」的原则,可发现存在两类运算的规则:
比较运算:==, !=
逻辑运算:&&, ||
先处理比较运算的变化方向,为此建立一个Matcher
的抽象:
Composition everywhere.
此刻,age
的设计运用了「函数式」的思维,其行为表现为「高阶函数」的特性,通过函数的「组合式设计」完成功能的自由拼装组合,简单、直接、漂亮。
查找年龄不等于18岁的学生,可以如此描述。
assertThat(find(students, age(ne(18))), notNullValue());
为了使得逻辑「谓词」变得更加人性化,可以引入「流式接口」的「DSL」
设计,增强表达力。
查找年龄不等于18岁的女生,可以表述为:
assertThat(find(students, age(ne(18)).and(Student::female)), notNullValue());
仔细的读者可能已经发现了,Student
和Teacher
两个类也存在「结构型重复」的问题。
Student
与Teacher
的结构性重复,导致StudentPredicates
与TeacherPredicates
也存在「结构性重复」。
为此需要进一步消除重复。
第一个直觉,通过「提取基类」的重构方法,消除Student
和Teacher
的重复设计。
从而实现了进一步消除了Student
和Teacher
之间的重复设计。
此时,可以通过引入「类型界定」的泛型设计,使得StudentPredicates
与TeacherPredicates
合二为一,进一步消除重复设计。
Student
和Teacher
依然存在「结构型重复」的问题,可以通过Static Factory Method
的设计方法,并让Human
的构造函数「私有化」,删除Student
和Teacher
两个子类,彻底消除两者之间的「重复设计」。
Human
的重构,使得HumanPredicates
的「类型界定」变得多余,从而进一步简化了设计。
null
Billion-Dollar Mistake
在最开始,我们遗留了一个问题:find
返回了null
。用户调用返回null
的接口时,常常忘记null
的检查,导致在运行时发生NullPointerException
异常。
按照「向稳定的方向依赖」的原则,find
的返回值应该设计为Optional<E>
,使用「类型系统」的特长,取得如下方面的优势:
显式地表达了不存在的语义;
编译时保证错误的发生;
通过4
个需求的迭代和演进,通过运用「正交设计」和「组合式设计」的基本思想,加深对「正交设计基本原则」的理解。
「正交设计」的理论、原则、及其方法论出自前ThoughtWorks
软件大师「袁英杰」先生。英杰既是我的老师,也是我的挚友;他高深莫测的软件设计的修为,及其对软件设计独特的哲学思维方式,是我等后辈学习的楷模。
原文转自: https://segmentfault.com/a/1190000004552525
https://blog.csdn.net/basonson/article/details/50924466
标签:point 准则 ast params 概念 ken iter asi 独立
原文地址:https://www.cnblogs.com/softidea/p/9609897.html