Design is there to enable you to keep changing the software easily in the long term. -- Kent Beck.java
正如Kent Beck
所说,软件设计是为了「长期」更加容易地适应将来的变化。正确的软件设计方法是为了长期地、更好更快、更容易地实现软件价值的交付。算法
软件设计就是为了完成以下目标,其可验证性、重要程度依次减低。数组
实现功能数据结构
易于重用闭包
易于理解ide
没有冗余函数
实现功能的目标压倒一块儿,这也是软件设计的首要标准。如何断定系统功能的完备性呢?经过全部测试用例。学习
从TDD
的角度看,测试用例就是对需求的阐述,是一个闭环的反馈系统,保证其系统的正确性;及其保证设计的合理性,恰如其分,很少很多;固然也是理解系统行为最重要的依据。测试
好的设计应该能让其余人也能容易地理解,包括系统的行为,业务的规则。那么,什么样的设计才算得上易于理解的呢?this
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());
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<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>)
的重复,为此引入「类型参数化」的设计。
首先消除StudentPredicate
和TeacherPredicate
的重复设计。
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());
仔细的读者可能已经发现了,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<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()); } }
为此须要进一步消除重复。
第一个直觉,经过「提取基类」的重构方法,消除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 <E extends Human> Predicate<E> age(Matcher<Integer> 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<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
软件大师「袁英杰」先生。英杰既是个人老师,也是个人挚友;他高深莫测的软件设计的修为,及其对软件设计独特的哲学思惟方式,是我等后辈学习的楷模。