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

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());

结构性重复

AgePredicate和NamePredicate存在「结构型重复」,需要进一步消除重复。经分析两个类的存在无非是为了实现「闭包」的能力,可以使用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 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 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)/find(Iterable)的重复,为此引入「类型参数化」的设计。

首先消除StudentPredicate和TeacherPredicate的重复设计。

public interface Predicate {

boolean test(E e);

}

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

public static E find(Iterable c, Predicate p) {

for (E e : c)

if (p.test(e))

return e;

return null;

}

型变

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

public static 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 age(int age) {

return s -> s.getAge() == age;

}

public static Predicate 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 ageEq(int age) {

return s -> s.getAge() == age;

}

public static Predicate ageNe(int age) {

return s -> s.getAge() != age;

}

}

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

比较运算:==, !=

逻辑运算:&&, ||

比较语义

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

public interface Matcher {

boolean matches(T actual);

static Matcher eq(T expected) {

return actual -> expected.equals(actual);

}

static Matcher ne(T expected) {

return actual -> !expected.equals(actual);

}

}

Composition everywhere.

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

public final class StudentPredicates {

......

public static Predicate age(Matcher m) {

return s -> m.matches(s.getAge());

}

}

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

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

逻辑语义

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

public interface Predicate {

boolean test(E e);

default Predicate and(Predicate super E> other) {

return e -> test(e) && other.test(e);

}

}

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

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

重复再现

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

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;

}

级联反应

Student与Teacher的结构性重复,导致StudentPredicates与TeacherPredicates也存在「结构性重复」。

public final class StudentPredicates {

......

public static Predicate age(Matcher m) {

return s -> m.matches(s.getAge());

}

}

public final class TeacherPredicates {

......

public static Predicate age(Matcher m) {

return t -> m.matches(t.getAge());

}

}

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

提取基类

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

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;

}

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

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);

}

}

类型界定

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

public final class HumanPredicates {

......

public static

Predicate age(Matcher m) {

return s -> m.matches(s.getAge());

}

}

消灭继承关系

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

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 age(Matcher m) {

return s -> m.matches(s.getAge());

}

}

绝不返回null

Billion-Dollar Mistake

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

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

显式地表达了不存在的语义;

编译时保证错误的发生;

import java.util.Optional;

public Optional 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软件大师「袁英杰」先生。英杰既是我的老师,也是我的挚友;他高深莫测的软件设计的修为,及其对软件设计独特的哲学思维方式,是我等后辈学习的楷模。

正交设计 python算法_正交设计 - SegmentFault 思否相关推荐

  1. java 声明式编程_声明式编程 - SegmentFault 思否

    这是Bartosz Milewski关于范畴论的博客的第二部分,第一部分已经由garfileo翻译完成,建议大家先看第一部分.第一部分导言的地址是写给程序员的范畴论 第二部分的导言 在本书的第一部分我 ...

  2. 正交设计 python算法_如何表示2D正交网格(Python)

    基本上我有一套"房间"(自定义类).所有房间都是相连的,每个房间都是根据一个或多个其他房间来定义的.我正在寻找一些系统来组织2D网格中的这些房间,并指定一个任意房间作为起源. cl ...

  3. 正交设计 python算法_人人都可以掌握的正交试验设计测试用例方法

    ## 介绍 - TamanduaOATs 是测者开发并开源的生成正交计算的pyd(python库)程序(放到python下的dlls目录下) - 项目地址:https://github.com/cri ...

  4. mysql中的leading用法_登录 - SegmentFault 思否

    前面我们主要分享了MySQL中的常见知识与使用.这里我们主要分享一下MySQL中的高阶使用,主要包括:函数.存储过程和存储引擎. 对于MySQL中的基础知识,可以参见 1 函数 函数可以返回任意类型的 ...

  5. 亚马逊云科技云创计划携手 SegmentFault 思否,成就 AI 探路者

    ‍‍ AI 无疑是这个夏天最"出圈"的话题. ChatGPT 的爆红向 AI 产业释放了"走出实验室"的信号,并提供了"从通用范式落地到具体场景&qu ...

  6. 极客广州——EOS Asia郭达峰担任SegmentFault思否黑客马拉松技术顾问

    近日备受关注的 EOS 投票率超过 15%,主网激活,已正式上线.EOS Asia 联合创始人郭达峰将担任 SegmentFault 思否区块链黑客马拉松广州站技术顾问,为大赛项目提供技术咨询支持.届 ...

  7. SegmentFault 思否发布开源问答社区软件 Answer

    ONES 旗下技术问答社区 SegmentFault 思否(下称"思否")今日宣布,正式对外开源其问答社区软件 Answer. 作为国内领先的新一代技术问答社区,思否始于「聚集体智 ...

  8. 安势信息入选 SegmentFault思否「2022 中国新锐技术先锋企业」

    2023 年 1 月 4 日,中国技术先锋年度评选 | 2022 中国新锐技术先锋企业榜单正式发布.作为中国领先的新一代开发者社区,SegmentFault 思否依托数百万开发者用户数据分析,各科技企 ...

  9. 对比CSDN与开源中国、segmentFault思否

    前言:作为一个IT行业学生我遇到问题一般会通过搜索几个权威性论坛得到的结果,通常是CSDN.SegmentFault.开源中国. 1.需求测评以及对比 首先对于一个学生或者一个IT从业人员而言,相关I ...

  10. 网易云信入选《SegmentFault 思否 2019 中国技术品牌影响力企业榜单》!

    近日,SegmentFault 思否作为中国最大的新一代开发者社区,依托数百万开发者用户行为数据,及科技企业技术品牌在国内市场的大数据分析,评选出 30 家在开发者领域最具影响力的科技企业,权威发布& ...

最新文章

  1. python发声-python3-声音处理
  2. tf.stack()和tf.unstack()的用法
  3. php robots.txt,robots.txt的写法
  4. 2021 EdgeX 中国挑战赛决赛入围名单公布
  5. 【大会】嵌入式玩直播,IoT做前处理
  6. 【面向对象】可变对象和不可变对象
  7. 文档转换html6,html学习文档-6、HTML 文本格式化(示例代码)
  8. 美国大学计算机科学分支,美国大学计算机专业四大分支介绍
  9. 强力推荐素材收集和管理神器-Eagle工具
  10. linux系统怎么设任务计划,在Linux系统上设置计划任务
  11. 云模型的MATLAB实现
  12. ie浏览器 “嗯...无法访问页面 尝试此操作...”的解决办法
  13. 结合MongoDB开发LBS应用(mongodb geo)
  14. 免费顶级域名+github个人主页教程
  15. android的签名
  16. 模电电路(部分合集)
  17. 平安京服务器维护,阴阳师6月23日服务器维护更新内容一览
  18. DB2常用命令 转
  19. 如何修复损坏的word
  20. Java中键盘输入 Scanner

热门文章

  1. 985硕士程序员年薪80万!邻居眼中不如一个老师?你怎么看?
  2. 结构方程模型-中介效应检验(Amos)
  3. 饥荒dns服务器未响应,DNS服务器未响应怎么办
  4. 技术开发、产品开发和平台开发的区别
  5. java 多态(重写和重载)
  6. tewa600agm是千兆吗_请问电信天翼网关光纤猫超级用户 型号tewa-600aem/tewa600agm
  7. compute和compute by
  8. c语言jt808协议库,部标JTT808协议快速开发包
  9. 淘宝母婴购物数据分析
  10. 无线通信算法工程师知识地图