實戰(zhàn)正交設計

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

設計是什么

正如Kent Beck所說协屡,軟件設計是為了「長期」更加容易地適應未來的變化哎垦。正確的軟件設計方法是為了長期地牍帚、更好更快、更容易地實現(xiàn)軟件價值的交付狰挡。

軟件設計的目標

軟件設計就是為了完成如下目標泽西,其可驗證性九串、重要程度依次減低囤官。

  • 實現(xiàn)功能
  • 易于重用
  • 易于理解
  • 沒有冗余

實現(xiàn)功能

實現(xiàn)功能的目標壓倒一起,這也是軟件設計的首要標準颤霎。如何判定系統(tǒng)功能的完備性呢媳谁?通過所有測試用例涂滴。

TDD的角度看,測試用例就是對需求的闡述晴音,是一個閉環(huán)的反饋系統(tǒng)柔纵,保證其系統(tǒng)的正確性;及其保證設計的合理性锤躁,恰如其分首量,不多不少;當然也是理解系統(tǒng)行為最重要的依據(jù)进苍。

易于理解

好的設計應該能讓其他人也能容易地理解,包括系統(tǒng)的行為鸭叙,業(yè)務的規(guī)則觉啊。那么,什么樣的設計才算得上易于理解的呢沈贝?

  • Clean Code
  • Implement Patterns
  • Idioms

沒有冗余

沒有冗余的系統(tǒng)是最簡單的系統(tǒng)杠人,恰如其分的系統(tǒng),不做任何過度設計的系統(tǒng)宋下。

  • Dead Code
  • YAGNI: You Ain't Gonna Need It
  • KISS: Keep it Simple, Stupid

易于重用

易于重用的軟件結構嗡善,使得其應對變化更具彈性;可被容易地修改学歧,具有更加適應變化的能力罩引。

最理想的情況下,所有的軟件修改都具有局部性枝笨。但現(xiàn)實并非如此袁铐,軟件設計往往需要花費很大的精力用于依賴的管理,讓組件之間的關系變得清晰横浑、一致剔桨、漂亮。

那么軟件設計的最高準則是什么呢徙融?「高內聚洒缀、低耦合」原則是提高可重用性的最高原則。為了實現(xiàn)高內聚欺冀,低耦合的軟件設計树绩,袁英杰提出了「正交設計」的方法論。

正交設計

「正交」是一個數(shù)學概念:所謂正交脚猾,就是指兩個向量的內積為零葱峡。簡單的說,就是這兩個向量是垂直的龙助。在一個正交系統(tǒng)里砰奕,沿著一個方向的變化蛛芥,其另外一個方向不會發(fā)生變化。為此军援,Bob大叔將「職責」定義為「變化的原因」仅淑。

「正交性」,意味著更高的內聚胸哥,更低的耦合涯竟。為此,正交性可以用于衡量系統(tǒng)的可重用性空厌。那么庐船,如何保證設計的正交性呢?袁英杰提出了「正交設計的四個基本原則」嘲更,簡明扼要筐钟,道破了軟件設計的精髓所在。

正交設計原則

  • 消除重復
  • 分離關注點
  • 縮小依賴范圍
  • 向穩(wěn)定的方向依賴

實戰(zhàn)

需求1: 存在一個學生的列表赋朦,查找一個年齡等于18歲的學生

快速實現(xiàn)

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

上述實現(xiàn)存在很多設計的「壞味道」:

  • 缺乏彈性參數(shù)類型:只支持數(shù)組類型篓冲,List, Set都被拒之門外;
  • 容易出錯:操作數(shù)組下標宠哄,往往引入不經意的錯誤壹将;
  • 幻數(shù):硬編碼,將算法與配置高度耦合毛嫉;
  • 返回null:再次給用戶打開了犯錯的大門诽俯;

使用for-each

按照「最小依賴原則」,先隱藏數(shù)組下標的實現(xiàn)細節(jié)狱庇,使用for-each降低錯誤發(fā)生的可能性惊畏。

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

需求2: 查找一個名字為horance的學生

重復設計

Copy-Paste是最快的實現(xiàn)方法,但會產生「重復設計」密任。

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

客戶端的調用根據(jù)場景济炎,提供算法的配置。

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

結構性重復

AgePredicateNamePredicate存在「結構型重復」辐真,需要進一步消除重復须尚。經分析兩個類的存在無非是為了實現(xiàn)「閉包」的能力崖堤,可以使用lambda表達式,「Code As Data」耐床,簡明扼要密幔。

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

引入Iterable

按照「向穩(wěn)定的方向依賴」的原則,為了適應諸如List, Set等多種數(shù)據(jù)結構撩轰,甚至包括原生的數(shù)組類型胯甩,可以將入?yún)⒅貥嫗橹貥嫗楦映橄蟮?code>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快速地實現(xiàn)這個功能。

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

類型參數(shù)化

分析StudentMacher/TeacherPredicate, find(Iterable<Student>)/find(Iterable<Teacher>)的重復愚战,為此引入「類型參數(shù)化」的設計。

首先消除StudentPredicateTeacherPredicate的重復設計齐遵。

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

再對find進行類型參數(shù)化設計寂玲。

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

型變

find的類型參數(shù)缺乏「型變」的能力,為此引入「型變」能力的支持梗摇,接口更加具有可復用性拓哟。

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表達式承載的「比較算法」與「參數(shù)配置」兩個職責糜烹,應該對其進行分離违诗。

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

可以通過「Static Factory Method」生產lambda表達式,將比較算法封裝起來疮蹦;而配置參數(shù)通過引入「參數(shù)化」設計诸迟,將「邏輯」與「配置」分離,從而達到最大化的代碼復用愕乎。

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

從需求看比肄,比較準則增加了眾多的語義快耿,再次運用「分離變化方向」的原則囊陡,可發(fā)現(xiàn)存在兩類運算的規(guī)則:

  • 比較運算:==, !=
  • 邏輯運算:&&, ||

比較語義

先處理比較運算的變化方向,為此建立一個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的設計運用了「函數(shù)式」的思維关斜,其行為表現(xiàn)為「高階函數(shù)」的特性,通過函數(shù)的「組合式設計」完成功能的自由拼裝組合铺浇,簡單痢畜、直接、漂亮鳍侣。

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

重復再現(xiàn)

仔細的讀者可能已經發(fā)現(xiàn)了授账,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;
}

級聯(lián)反應

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

從而實現(xiàn)了進一步消除了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的構造函數(shù)「私有化」呜投,刪除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的檢查洒扎,導致在運行時發(fā)生NullPointerException異常辑甜。

按照「向穩(wěn)定的方向依賴」的原則,find的返回值應該設計為Optional<E>袍冷,使用「類型系統(tǒng)」的特長磷醋,取得如下方面的優(yōu)勢:

  • 顯式地表達了不存在的語義;
  • 編譯時保證錯誤的發(fā)生胡诗;
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軟件大師「袁英杰」先生瑰抵。英杰既是我的老師你雌,也是我的摯友;他高深莫測的軟件設計的修為二汛,及其對軟件設計獨特的哲學思維方式婿崭,是我等后輩學習的楷模。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末肴颊,一起剝皮案震驚了整個濱河市逛球,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苫昌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幸海,死亡現(xiàn)場離奇詭異祟身,居然都是意外死亡物独,警方通過查閱死者的電腦和手機袜硫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挡篓,“玉大人婉陷,你說我怎么就攤上這事」傺校” “怎么了秽澳?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長戏羽。 經常有香客問我担神,道長,這世上最難降的妖魔是什么始花? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任妄讯,我火速辦了婚禮孩锡,結果婚禮上,老公的妹妹穿的比我還像新娘亥贸。我一直安慰自己躬窜,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布炕置。 她就那樣靜靜地躺著荣挨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪讹俊。 梳的紋絲不亂的頭發(fā)上垦沉,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音仍劈,去河邊找鬼厕倍。 笑死,一個胖子當著我的面吹牛贩疙,可吹牛的內容都是我干的讹弯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼这溅,長吁一口氣:“原來是場噩夢啊……” “哼组民!你這毒婦竟也來了?” 一聲冷哼從身側響起悲靴,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤臭胜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后癞尚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耸三,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年浇揩,在試婚紗的時候發(fā)現(xiàn)自己被綠了仪壮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡胳徽,死狀恐怖积锅,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情养盗,我是刑警寧澤缚陷,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站往核,受9級特大地震影響蹬跃,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一蝶缀、第九天 我趴在偏房一處隱蔽的房頂上張望丹喻。 院中可真熱鬧,春花似錦翁都、人聲如沸碍论。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳍悠。三九已至,卻和暖如春坐搔,著一層夾襖步出監(jiān)牢的瞬間藏研,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工概行, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蠢挡,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓凳忙,卻偏偏與公主長得像业踏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涧卵,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容

  • OO makes code understandable by encapsulating moving part...
    劉光聰閱讀 2,248評論 0 18
  • 1. Java基礎部分 基礎部分的順序:基本語法勤家,類相關的語法,內部類的語法柳恐,繼承相關的語法伐脖,異常的語法,線程的語...
    子非魚_t_閱讀 31,581評論 18 399
  • 一個出發(fā)點 當談起軟件設計的目的時乐设,能夠獲得所有人認同的答案只有一個:功能實現(xiàn)讼庇。 因為這是一個軟件存在的根本原因。...
    _袁英杰_閱讀 20,674評論 8 86
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,499評論 25 707
  • 寶寶暑假回老家玩兒伤提,家里只有我和老婆2人,趁此良機想去海邊露營回味下二人世界认烁,露營自然是海邊首選肿男,深圳有很多海灘,...
    北極熊不吃企鵝28閱讀 2,765評論 1 49