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岁的学生

快速实现

public static Student findByAge(Student[] students) { for (int i=0; i<students.length; i++) if (students[i].getAge() == 18) return students[i]; return null; } 

上述实现存在很多设计的「坏味道」:

  • 缺乏弹性参数类型:只支持数组类型,List, Set都被拒之门外;
  • 容易出错:操作数组下标,往往引入不经意的错误;
  • 幻数:硬编码,将算法与配置高度耦合;
  • 返回null:再次给用户打开了犯错的大门;

使用for-each

按照「最小依赖原则」,先隐藏数组下标的实现细节,使用for-each降低错误发生的可能性。

public static Student findByAge(Student[] students) { for (Student s : students) if (s.getAge() == 18) return s; return null; } 

需求2: 查找一个名字为horance的学生

重复设计

Copy-Paste是最快的实现方法,但会产生「重复设计」。

public static Student findByName(Student[] students) { for (Student s : students) if (s.getName().equals("horance")) return s; return null; } 

为了消除重复,可以将「查找算法」与「比较准则」这两个「变化方向」进行分离。

抽象准则

首先将比较的准则进行抽象化,让其独立变化。

public interface StudentPredicate { boolean test(Student s); } 

将各个「变化原因」对象化,为此建立了两个简单的算子。

public class AgePredicate implements StudentPredicate { private int age; public AgePredicate(int age) { this.age = age; } @Override public boolean test(Student s) { return s.getAge() == age; } } 
public class NamePredicate implements StudentPredicate { private String name; public NamePredicate(String name) { this.name = name; } @Override public boolean test(Student s) { return s.getName().equals(name); } } 

此刻,查找算法的方法名也应该被「重命名」,使其保持在同一个「抽象层次」上。

public static Student find(Student[] students, StudentPredicate p) { for (Student s : students) if (p.test(s)) return s; return null; } 

客户端的调用根据场景,提供算法的配置。

assertThat(find(students, new AgePredicate(18)), notNullValue());
assertThat(find(students, new NamePredicate("horance")), notNullValue()); 

结构性重复

AgePredicateNamePredicate存在「结构型重复」,需要进一步消除重复。经分析两个类的存在无非是为了实现「闭包」的能力,可以使用lambda表达式,「Code As Data」,简明扼要。

assertThat(find(students, s -> s.getAge() == 18), notNullValue());
assertThat(find(students, s -> s.getName().equals("horance")), notNullValue());

引入Iterable

按照「向稳定的方向依赖」的原则,为了适应诸如List, Set等多种数据结构,甚至包括原生的数组类型,可以将入参重构为重构为更加抽象的Iterable类型。

public static Student find(Iterable<Student> students, StudentPredicate p) { for (Student s : students) if (p.test(s)) return s; return null; } 

需求3: 存在一个老师列表,查找第一个女老师

类型重复

按照既有的代码结构,可以通过Copy Paste快速地实现这个功能。

public interface TeacherPredicate { boolean test(Teacher t); } 
public static Teacher find(Iterable<Teacher> teachers, TeacherPredicate p) { for (Teacher t : teachers) if (p.test(t)) return t; return null; } 

用户接口依然可以使用Lambda表达式。

assertThat(find(teachers, t -> t.female()), notNullValue());

如果使用Method Reference,可以进一步地改善表达力。

assertThat(find(teachers, Teacher::female), notNullValue());

类型参数化

分析StudentMacher/TeacherPredicate, find(Iterable<Student>)/find(Iterable<Teacher>)的重复,为此引入「类型参数化」的设计。

首先消除StudentPredicateTeacherPredicate的重复设计。

public interface Predicate<E> { boolean test(E e); } 

再对find进行类型参数化设计。

public static <E> E find(Iterable<E> c, Predicate<E> p) { for (E e : c) if (p.test(e)) return e; return null; } 

型变

find的类型参数缺乏「型变」的能力,为此引入「型变」能力的支持,接口更加具有可复用性。

public static <E> E find(Iterable<? extends E> c, Predicate<? super E> p) { for (E e : c) if (p.test(e)) return e; return null; } 

复用lambda

Parameterize all the things.

观察如下两个测试用例,如果做到极致,可认为两个lambda表达式也是重复的。从「分离变化的方向」的角度分析,此lambda表达式承载的「比较算法」与「参数配置」两个职责,应该对其进行分离。

assertThat(find(students, s -> s.getName().equals("Horance")), notNullValue());
assertThat(find(students, s -> s.getName().equals("Tomas")), notNullValue());

可以通过「Static Factory Method」生产lambda表达式,将比较算法封装起来;而配置参数通过引入「参数化」设计,将「逻辑」与「配置」分离,从而达到最大化的代码复用。

public final class StudentPredicates { private StudentPredicates() { } public static Predicate<Student> age(int age) { return s -> s.getAge() == age; } public static Predicate<Student> name(String name) { return s -> s.getName().equals(name); } } 
import static StudentPredicates.*;assertThat(find(students, name("horance")), notNullValue());
assertThat(find(students, age(10)), notNullValue()); 

组合查询

但是,上述将lambda表达式封装在Factory的设计是及其脆弱的。例如,增加如下的需求:

需求4: 查找年龄不等于18岁的女生

最简单的方法就是往StudentPredicates不停地增加「Static Factory Method」,但这样的设计严重违反了「OCP」(开放封闭)原则。

public final class StudentPredicates { ...... public static Predicate<Student> ageEq(int age) { return s -> s.getAge() == age; } public static Predicate<Student> ageNe(int age) { return s -> s.getAge() != age; } } 

从需求看,比较准则增加了众多的语义,再次运用「分离变化方向」的原则,可发现存在两类运算的规则:

  • 比较运算:==, !=
  • 逻辑运算:&&, ||

比较语义

先处理比较运算的变化方向,为此建立一个Matcher的抽象:

public interface Matcher<T> { boolean matches(T actual); static <T> Matcher<T> eq(T expected) { return actual -> expected.equals(actual); } static <T> Matcher<T> ne(T expected) { return actual -> !expected.equals(actual); } } 

Composition everywhere.

此刻,age的设计运用了「函数式」的思维,其行为表现为「高阶函数」的特性,通过函数的「组合式设计」完成功能的自由拼装组合,简单、直接、漂亮。

public final class StudentPredicates { ...... public static Predicate<Student> age(Matcher<Integer> m) { return s -> m.matches(s.getAge()); } } 

查找年龄不等于18岁的学生,可以如此描述。

assertThat(find(students, age(ne(18))), notNullValue());

逻辑语义

为了使得逻辑「谓词」变得更加人性化,可以引入「流式接口」的「DSL」设计,增强表达力。

public interface Predicate<E> { boolean test(E e); default Predicate<E> and(Predicate<? super E> other) { return e -> test(e) && other.test(e); } } 

查找年龄不等于18岁的女生,可以表述为:

assertThat(find(students, age(ne(18)).and(Student::female)), notNullValue());

重复再现

仔细的读者可能已经发现了,StudentTeacher两个类也存在「结构型重复」的问题。

public class Student { public Student(String name, int age, boolean male) { this.name = name; this.age = age; this.male = male; } ...... private String name; private int age; private boolean male; } 
public class Teacher { public Teacher(String name, int age, boolean male) { this.name = name; this.age = age; this.male = male; } ...... private String name; private int age; private boolean male; } 

级联反应

StudentTeacher的结构性重复,导致StudentPredicatesTeacherPredicates也存在「结构性重复」。

public final class StudentPredicates { ...... public static Predicate<Student> age(Matcher<Integer> m) { return s -> m.matches(s.getAge()); } } 
public final class TeacherPredicates { ...... public static Predicate<Teacher> age(Matcher<Integer> m) { return t -> m.matches(t.getAge()); } } 

为此需要进一步消除重复。

提取基类

第一个直觉,通过「提取基类」的重构方法,消除StudentTeacher的重复设计。

class Human {protected Human(String name, int age, boolean male) { this.name = name; this.age = age; this.male = male; } ... private String name; private int age; private boolean male; } 

从而实现了进一步消除了StudentTeacher之间的重复设计。

public class Student extends Human { public Student(String name, int age, boolean male) { super(name, age, male); } } public class Teacher extends Human { public Teacher(String name, int age, boolean male) { super(name, age, male); } } 

类型界定

此时,可以通过引入「类型界定」的泛型设计,使得StudentPredicatesTeacherPredicates合二为一,进一步消除重复设计。

public final class HumanPredicates { ...... public static <E extends Human> Predicate<E> age(Matcher<Integer> m) { return s -> m.matches(s.getAge()); } } 

消灭继承关系

StudentTeacher依然存在「结构型重复」的问题,可以通过Static Factory Method的设计方法,并让Human的构造函数「私有化」,删除StudentTeacher两个子类,彻底消除两者之间的「重复设计」。

public class Human { private Human(String name, int age, boolean male) { this.name = name; this.age = age; this.male = male; } public static Human student(String name, int age, boolean male) { return new Human(name, age, male); } public static Human teacher(String name, int age, boolean male) { return new Human(name, age, male); } ...... } 

消灭类型界定

Human的重构,使得HumanPredicates的「类型界定」变得多余,从而进一步简化了设计。

public final class HumanPredicates { ...... public static Predicate<Human> age(Matcher<Integer> m) { return s -> m.matches(s.getAge()); } } 

绝不返回null

Billion-Dollar Mistake

在最开始,我们遗留了一个问题:find返回了null。用户调用返回null的接口时,常常忘记null的检查,导致在运行时发生NullPointerException异常。

按照「向稳定的方向依赖」的原则,find的返回值应该设计为Optional<E>,使用「类型系统」的特长,取得如下方面的优势:

  • 显式地表达了不存在的语义;
  • 编译时保证错误的发生;
import java.util.Optional;public <E> Optional<E> find(Iterable<? extends E> c, Predicate<? super E> p) { for (E e : c) { if (p.test(e)) { return Optional.of(e); } } return Optional.empty(); } 

回顾

通过4个需求的迭代和演进,通过运用「正交设计」和「组合式设计」的基本思想,加深对「正交设计基本原则」的理解。

鸣谢

「正交设计」的理论、原则、及其方法论出自前ThoughtWorks软件大师「袁英杰」先生。英杰既是我的老师,也是我的挚友;他高深莫测的软件设计的修为,及其对软件设计独特的哲学思维方式,是我等后辈学习的楷模。

作者:刘光聪
链接:https://www.jianshu.com/p/a95f67ed714d
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

正交设计——实战【转】相关推荐

  1. 一次正交设计和组合设计之旅

    引言 Design is there to enable you to keep changing the software easily in the long term. – Kent Beck ...

  2. 既然Talk is cheap, 那么就用代码教你如何进行正交设计

    软件设计的目标 实现功能 易于重用 易于理解 没有冗余 正交设计 软件设计是一个「守破离」的过程. -- 袁英杰 消除重复 分离变化方向 缩小依赖范围 向稳定的方向依赖 实战 需求1: 存在一个学生的 ...

  3. IDEA的Docker插件实战(Dockerfile篇)

    IDEA的Docker插件实战(Dockerfile篇) IntelliJ IDEA的Docker插件能帮助我们将当前工程制作成Docker镜像.运行在指定的远程机器上,是学习和开发阶段的好帮手,本文 ...

  4. 数据结构(04)— 线性顺序表实战

    1. 设计思路 本实战的实质是完成对学生成绩信息的建立.查找.插入.修改.删除等功能,可以首先定义项目的数据结构,然后将每个功能写成一个函数来完成对数据的操作,最后完成主函数以验证各个函数功能并得出运 ...

  5. 【置顶】利用 NLP 技术做简单数据可视化分析教程(实战)

    置顶 本人决定将过去一段时间在公司以及日常生活中关于自然语言处理的相关技术积累,将在gitbook做一个简单分享,内容应该会很丰富,希望对你有所帮助,欢迎大家支持. 内容介绍如下 你是否曾经在租房时因 ...

  6. 2 用python进行OpenCV实战之图像基本知识

    前言 在这一节,我们将学习图像的基本构成单元--像素,我们将详细的探讨什么是像素?像素是如何使用来构成图像的?然后学习如何通过OpenCV来获取和操纵像素. 1 什么是像素 所有的图像都包含一组像素, ...

  7. PyTorch 高级实战教程:基于 BI-LSTM CRF 实现命名实体识别和中文分词

    20210607 https://blog.csdn.net/u011828281/article/details/81171066 前言:译者实测 PyTorch 代码非常简洁易懂,只需要将中文分词 ...

  8. 实战清除电脑上恶意弹出广告窗口

    实战清除电脑上恶意弹出广告窗口 当你碰到电脑桌面右下角时不时弹出广告,如游戏推广.商品广告等,怎么删也删不掉,这是因为用户不小心安装有捆绑广告推广的软件,系统被静默安装了恶意木马广告,这不仅仅是影响用 ...

  9. deeplearning模型量化实战

    deeplearning模型量化实战 MegEngine 提供从训练到部署完整的量化支持,包括量化感知训练以及训练后量化,凭借"训练推理一体"的特性,MegEngine更能保证量化 ...

最新文章

  1. linux设置外接显示器的分辨率
  2. android 得到毫秒时间戳,android – Location.getTime()总是返回没有毫秒的时间戳
  3. 【Go API 开发实战 7】基础 3:记录和管理 API 日志
  4. 为什么volatile不能保证原子性而Atomic可以?
  5. NodeJs两个简单调试技巧
  6. PAT_B_1038_Java(14分)_C++(20分)
  7. rvm的安装, 使用rvm, 安装ruby, 以及gem的使用 (转)
  8. android Toast五种特效
  9. Xdebug的优点!php代码开发
  10. android平台支付宝快捷支付
  11. 微信小程序之根据经纬度反查地址
  12. 微信录音arm格式转换为mp3(亲测解决 Java linux centos 环境)
  13. iOS软件开发实现类似微信上传图片选择
  14. 【学习笔记】深蓝学院-三维点云处理
  15. 百度地图--绘制自定义铁路线
  16. 如何将网络上的共享文件映射到本地
  17. java duplicate entry_java向数据库插入数据时的错误: Duplicate entry '' for key 'PRIMARY' 问题解决...
  18. Bootstrap使用前必须在head 标签内导入bootstrap的链接,否则bootstrap无效
  19. 观‘锤子手机发布会’
  20. flinto导出html,Sketch绝佳搭配!30秒制作交互原型的次时代神器Flinto

热门文章

  1. 老司机揭秘手机定位技术,实在是精彩!
  2. 115.滑动菜单指示器特效
  3. 一篇文章带你搞懂微信小程序的开发过程
  4. java word模版填充_java向word模板中填充数据(总结)
  5. live2d_碧蓝航线:老婆们在外服都长啥样?她惊艳四座,Live2D独此一家
  6. 微信公众号实现消息推送
  7. 消息传递,生产者消费者
  8. 1. Python环境搭建
  9. 【JqGrid】JqGrid API 中文说明及用法
  10. wxpython下载安装过程