天天看點

正交設計

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

歲的學生

快速實作

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

上述實作存在很多設計的「壞味道」:

  • 缺乏彈性參數類型:隻支援數組類型,

    List, Set

    都被拒之門外;
  • 容易出錯:操作數組下标,往往引入不經意的錯誤;
  • 幻數:寫死,将算法與配置高度耦合;
  • 傳回

    null

    :再次給使用者打開了犯錯的大門;

使用

for-each

按照「最小依賴原則」,先隐藏數組下标的實作細節,使用

for-each

降低錯誤發生的可能性。

  1. public static Student findByAge(Student[] students) {
  2. for (Student s : students)
  3. if (s.getAge() == 18)
  4. return s;
  5. return null;
  6. }
需求2: 查找一個名字為

horance

的學生

重複設計

Copy-Paste

是最快的實作方法,但會産生「重複設計」。

  1. public static Student findByName(Student[] students) {
  2. for (Student s : students)
  3. if (s.getName().equals("horance"))
  4. return s;
  5. return null;
  6. }

為了消除重複,可以将「查找算法」與「比較準則」這兩個「變化方向」進行分離。

抽象準則

首先将比較的準則進行抽象化,讓其獨立變化。

  1. public interface StudentPredicate {
  2. boolean test(Student s);
  3. }

将各個「變化原因」對象化,為此建立了兩個簡單的算子。

  1. public class AgePredicate implements StudentPredicate {
  2. private int age;
  3. public AgePredicate(int age) {
  4. this.age = age;
  5. }
  6. @Override
  7. public boolean test(Student s) {
  8. return s.getAge() == age;
  9. }
  10. }
  1. public class NamePredicate implements StudentPredicate {
  2. private String name;
  3. public NamePredicate(String name) {
  4. this.name = name;
  5. }
  6. @Override
  7. public boolean test(Student s) {
  8. return s.getName().equals(name);
  9. }
  10. }

此刻,查找算法的方法名也應該被「重命名」,使其保持在同一個「抽象層次」上。

  1. public static Student find(Student[] students, StudentPredicate p) {
  2. for (Student s : students)
  3. if (p.test(s))
  4. return s;
  5. return null;
  6. }

用戶端的調用根據場景,提供算法的配置。

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

結構性重複

AgePredicate

NamePredicate

存在「結構型重複」,需要進一步消除重複。經分析兩個類的存在無非是為了實作「閉包」的能力,可以使用

lambda

表達式,「

Code As Data

」,簡明扼要。

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

引入

Iterable

按照「向穩定的方向依賴」的原則,為了适應諸如

List, Set

等多種資料結構,甚至包括原生的數組類型,可以将入參重構為重構為更加抽象的

Iterable

類型。

  1. public static Student find(Iterable<Student> students, StudentPredicate p) {
  2. for (Student s : students)
  3. if (p.test(s))
  4. return s;
  5. return null;
  6. }
需求3: 存在一個老師清單,查找第一個女老師

類型重複

按照既有的代碼結構,可以通過

Copy Paste

快速地實作這個功能。

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

使用者接口依然可以使用

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

的重複設計。

  1. public interface Predicate<E> {
  2. boolean test(E e);
  3. }

再對

find

進行類型參數化設計。

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

型變

find

的類型參數缺乏「型變」的能力,為此引入「型變」能力的支援,接口更加具有可複用性。

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

複用

lambda

Parameterize all the things.

觀察如下兩個測試用例,如果做到極緻,可認為兩個

lambda

表達式也是重複的。從「分離變化的方向」的角度分析,此

lambda

表達式承載的「比較算法」與「參數配置」兩個職責,應該對其進行分離。

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

可以通過

「Static Factory Method」

生産

lambda

表達式,将比較算法封裝起來;而配置參數通過引入「參數化」設計,将「邏輯」與「配置」分離,進而達到最大化的代碼複用。

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

組合查詢

但是,上述将

lambda

表達式封裝在

Factory

的設計是及其脆弱的。例如,增加如下的需求:

需求4: 查找年齡不等于18歲的女生

最簡單的方法就是往

StudentPredicates

不停地增加

「Static Factory Method」

,但這樣的設計嚴重違反了

「OCP」(開放封閉)

原則。

  1. public final class StudentPredicates {
  2. ......
  3. public static Predicate<Student> ageEq(int age) {
  4. return s -> s.getAge() == age;
  5. }
  6. public static Predicate<Student> ageNe(int age) {
  7. return s -> s.getAge() != age;
  8. }
  9. }

從需求看,比較準則增加了衆多的語義,再次運用「分離變化方向」的原則,可發現存在兩類運算的規則:

  • 比較運算:

    ==, !=

  • 邏輯運算:

    &&, ||

比較語義

先處理比較運算的變化方向,為此建立一個

Matcher

的抽象:

  1. public interface Matcher<T> {
  2. boolean matches(T actual);
  3. static <T> Matcher<T> eq(T expected) {
  4. return actual -> expected.equals(actual);
  5. }
  6. static <T> Matcher<T> ne(T expected) {
  7. return actual -> !expected.equals(actual);
  8. }
  9. }
Composition everywhere.

此刻,

age

的設計運用了「函數式」的思維,其行為表現為「高階函數」的特性,通過函數的「組合式設計」完成功能的自由拼裝組合,簡單、直接、漂亮。

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

查找年齡不等于18歲的學生,可以如此描述。

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

邏輯語義

為了使得邏輯「謂詞」變得更加人性化,可以引入「流式接口」的

「DSL」

設計,增強表達力。

  1. public interface Predicate<E> {
  2. boolean test(E e);
  3. default Predicate<E> and(Predicate<? super E> other) {
  4. return e -> test(e) && other.test(e);
  5. }
  6. }

查找年齡不等于18歲的女生,可以表述為:

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

重複再現

仔細的讀者可能已經發現了,

Student

Teacher

兩個類也存在「結構型重複」的問題。

  1. public class Student {
  2. public Student(String name, int age, boolean male) {
  3. this.name = name;
  4. this.age = age;
  5. this.male = male;
  6. }
  7. ......
  8. private String name;
  9. private int age;
  10. private boolean male;
  11. }
  1. public class Teacher {
  2. public Teacher(String name, int age, boolean male) {
  3. this.name = name;
  4. this.age = age;
  5. this.male = male;
  6. }
  7. ......
  8. private String name;
  9. private int age;
  10. private boolean male;
  11. }

級聯反應

Student

Teacher

的結構性重複,導緻

StudentPredicates

TeacherPredicates

也存在「結構性重複」。

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

為此需要進一步消除重複。

提取基類

第一個直覺,通過「提取基類」的重構方法,消除

Student

Teacher

的重複設計。

  1. class Human {
  2. protected Human(String name, int age, boolean male) {
  3. this.name = name;
  4. this.age = age;
  5. this.male = male;
  6. }
  7. ...
  8. private String name;
  9. private int age;
  10. private boolean male;
  11. }

進而實作了進一步消除了

Student

Teacher

之間的重複設計。

  1. public class Student extends Human {
  2. public Student(String name, int age, boolean male) {
  3. super(name, age, male);
  4. }
  5. }
  6. public class Teacher extends Human {
  7. public Teacher(String name, int age, boolean male) {
  8. super(name, age, male);
  9. }
  10. }

類型界定

此時,可以通過引入「類型界定」的泛型設計,使得

StudentPredicates

TeacherPredicates

合二為一,進一步消除重複設計。

  1. public final class HumanPredicates {
  2. ......
  3. public static <E extends Human>
  4. Predicate<E> age(Matcher<Integer> m) {
  5. return s -> m.matches(s.getAge());
  6. }
  7. }

消滅繼承關系

Student

Teacher

依然存在「結構型重複」的問題,可以通過

Static Factory Method

的設計方法,并讓

Human

的構造函數「私有化」,删除

Student

Teacher

兩個子類,徹底消除兩者之間的「重複設計」。

  1. public class Human {
  2. private Human(String name, int age, boolean male) {
  3. this.name = name;
  4. this.age = age;
  5. this.male = male;
  6. }
  7. public static Human student(String name, int age, boolean male) {
  8. return new Human(name, age, male);
  9. }
  10. public static Human teacher(String name, int age, boolean male) {
  11. return new Human(name, age, male);
  12. }
  13. ......
  14. }

消滅類型界定

Human

的重構,使得

HumanPredicates

的「類型界定」變得多餘,進而進一步簡化了設計。

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

絕不傳回

null

Billion-Dollar Mistake

在最開始,我們遺留了一個問題:

find

傳回了

null

。使用者調用傳回

null

的接口時,常常忘記

null

的檢查,導緻在運作時發生

NullPointerException

異常。

按照「向穩定的方向依賴」的原則,

find

的傳回值應該設計為

Optional<E>

,使用「類型系統」的特長,取得如下方面的優勢:

  • 顯式地表達了不存在的語義;
  • 編譯時保證錯誤的發生;
  1. import java.util.Optional;
  2. public <E> Optional<E> find(Iterable<? extends E> c, Predicate<? super E> p) {
  3. for (E e : c) {
  4. if (p.test(e)) {
  5. return Optional.of(e);
  6. }
  7. }
  8. return Optional.empty();
  9. }

回顧

通過

4

個需求的疊代和演進,通過運用「正交設計」和「組合式設計」的基本思想,加深對「正交設計基本原則」的了解。

鳴謝

「正交設計」的理論、原則、及其方法論出自前

ThoughtWorks

軟體大師「袁英傑」先生。英傑既是我的老師,也是我的摯友;他高深莫測的軟體設計的修為,及其對軟體設計獨特的哲學思維方式,是我等後輩學習的楷模。

原文轉自: https://segmentfault.com/a/1190000004552525

https://blog.csdn.net/basonson/article/details/50924466

正交設計