正交設計

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

結構性重複

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軟件大師「袁英傑」先生。英傑既是個人老師,也是個人摯友;他高深莫測的軟件設計的修爲,及其對軟件設計獨特的哲學思惟方式,是我等後輩學習的楷模。

相關文章
相關標籤/搜索