摘要:為了實現高內聚,低耦合的軟件設計,袁英杰提出了正交設計的方法論。正交設計正交是一個數學概念所謂正交,就是指兩個向量的內積為零。鳴謝正交設計的理論原則及其方法論出自前軟件大師袁英杰先生。
設計是什么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上述實現存在很多設計的「壞味道」:
缺乏彈性參數類型:只支持數組類型,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(Iterablestudents, 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(Iterableteachers, 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復用lambdaE find(Iterable extends E> c, Predicate super E> p) { for (E e : c) if (p.test(e)) return e; return null; } 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 Predicateage(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 PredicateageEq(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 Predicateage(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 Predicateage(Matcher m) { return s -> m.matches(s.getAge()); } } public final class TeacherPredicates { ...... public static Predicateage(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絕不返回nullage(Matcher m) { return s -> m.matches(s.getAge()); } } 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軟件大師「袁英杰」先生。英杰既是我的老師,也是我的摯友;他高深莫測的軟件設計的修為,及其對軟件設計獨特的哲學思維方式,是我等后輩學習的楷模。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65584.html
摘要:需要結合其他測試用例設計的方法進行補充。比如邊界值邊界值在軟件中邊界值測試方法是發現錯誤能力最強的一種。其中,原因是表示輸入條件,結果是對輸入執行的一系列計算后得到的輸出。與取值或,表示某狀態不出現,則表示某狀態出現。 ...
摘要:因果圖分析法是一種圖解法分析輸入的各種組合情況,從而設計測試用例的方法。工具錯誤推測法根據實際經驗或推測分析列出所有可能存在的和容易發生錯誤的情況,并有針對性的設計測試用例。 1.等價類劃分 等價類是指某個輸入域的子集合.在該子集合中,各個輸入數據對于揭露程序中的錯誤都是等效的.并合理地假定:測試某等價類的代表值就等于對這一類其它值的測試。 把輸入數據合理地劃分等價類,在每一個等價類中...
摘要:注本文內容來深入面向對象模式與實踐中節。面向對象設計與過程式編程面向對象設計和過程式編程有什么不同呢可能有些人認為最大的不同在于面向對象編程中包含對象。面向對象編程和過程式編程的一個核心區別是如何分配職責。 注:本文內容來中6.2節。 6.2 面向對象設計與過程式編程 ??面向對象設計和過程式編程有什么不同呢?可能有些人認為最大的不同在于面向對象編程中包含對象。事實上,這種說法不準確。...
摘要:為了解決以上問題,我們的分流系統選擇基于實現,通過或者協議來傳遞分流信息。正交是指用戶進入所有的實驗之間沒有必然關系。流量層內實驗分流流量層內實驗的因子有設備流量層。統計功效對于置信區間特征值等產品化功能支持。 什么是 ABTest 產品的改變不是由我們隨便「拍腦袋」得出,而是需要由實際的數據驅動,讓用戶的反饋來指導我們如何更好地改善服務。正如馬蜂窩 CEO 陳罡在接受專訪時所說:「有...
閱讀 2146·2021-10-12 10:11
閱讀 846·2021-10-09 09:41
閱讀 3760·2021-09-09 11:37
閱讀 1940·2021-09-08 10:41
閱讀 2641·2019-08-30 12:58
閱讀 2373·2019-08-30 10:58
閱讀 1278·2019-08-26 13:40
閱讀 4113·2019-08-26 13:36