摘要:為了實(shí)現(xiàn)高內(nèi)聚,低耦合的軟件設(shè)計(jì),袁英杰提出了正交設(shè)計(jì)的方法論。正交設(shè)計(jì)正交是一個(gè)數(shù)學(xué)概念所謂正交,就是指兩個(gè)向量的內(nèi)積為零。鳴謝正交設(shè)計(jì)的理論原則及其方法論出自前軟件大師袁英杰先生。
設(shè)計(jì)是什么Design is there to enable you to keep changing the software easily in the long term. -- Kent Beck.
正如Kent Beck所說(shuō),軟件設(shè)計(jì)是為了「長(zhǎng)期」更加容易地適應(yīng)未來(lái)的變化。正確的軟件設(shè)計(jì)方法是為了長(zhǎng)期地、更好更快、更容易地實(shí)現(xiàn)軟件價(jià)值的交付。
軟件設(shè)計(jì)的目標(biāo)軟件設(shè)計(jì)就是為了完成如下目標(biāo),其可驗(yàn)證性、重要程度依次減低。
實(shí)現(xiàn)功能
易于重用
易于理解
沒(méi)有冗余
實(shí)現(xiàn)功能實(shí)現(xiàn)功能的目標(biāo)壓倒一起,這也是軟件設(shè)計(jì)的首要標(biāo)準(zhǔn)。如何判定系統(tǒng)功能的完備性呢?通過(guò)所有測(cè)試用例。
從TDD的角度看,測(cè)試用例就是對(duì)需求的闡述,是一個(gè)閉環(huán)的反饋系統(tǒng),保證其系統(tǒng)的正確性;及其保證設(shè)計(jì)的合理性,恰如其分,不多不少;當(dāng)然也是理解系統(tǒng)行為最重要的依據(jù)。
易于理解好的設(shè)計(jì)應(yīng)該能讓其他人也能容易地理解,包括系統(tǒng)的行為,業(yè)務(wù)的規(guī)則。那么,什么樣的設(shè)計(jì)才算得上易于理解的呢?
Clean Code
Implement Patterns
Idioms
沒(méi)有冗余沒(méi)有冗余的系統(tǒng)是最簡(jiǎn)單的系統(tǒng),恰如其分的系統(tǒng),不做任何過(guò)度設(shè)計(jì)的系統(tǒng)。
Dead Code
YAGNI: You Ain"t Gonna Need It
KISS: Keep it Simple, Stupid
易于重用易于重用的軟件結(jié)構(gòu),使得其應(yīng)對(duì)變化更具彈性;可被容易地修改,具有更加適應(yīng)變化的能力。
最理想的情況下,所有的軟件修改都具有局部性。但現(xiàn)實(shí)并非如此,軟件設(shè)計(jì)往往需要花費(fèi)很大的精力用于依賴(lài)的管理,讓組件之間的關(guān)系變得清晰、一致、漂亮。
那么軟件設(shè)計(jì)的最高準(zhǔn)則是什么呢?「高內(nèi)聚、低耦合」原則是提高可重用性的最高原則。為了實(shí)現(xiàn)高內(nèi)聚,低耦合的軟件設(shè)計(jì),袁英杰提出了「正交設(shè)計(jì)」的方法論。
正交設(shè)計(jì)「正交」是一個(gè)數(shù)學(xué)概念:所謂正交,就是指兩個(gè)向量的內(nèi)積為零。簡(jiǎn)單的說(shuō),就是這兩個(gè)向量是垂直的。在一個(gè)正交系統(tǒng)里,沿著一個(gè)方向的變化,其另外一個(gè)方向不會(huì)發(fā)生變化。為此,Bob大叔將「職責(zé)」定義為「變化的原因」。
「正交性」,意味著更高的內(nèi)聚,更低的耦合。為此,正交性可以用于衡量系統(tǒng)的可重用性。那么,如何保證設(shè)計(jì)的正交性呢?袁英杰提出了「正交設(shè)計(jì)的四個(gè)基本原則」,簡(jiǎn)明扼要,道破了軟件設(shè)計(jì)的精髓所在。
正交設(shè)計(jì)原則消除重復(fù)
分離關(guān)注點(diǎn)
縮小依賴(lài)范圍
向穩(wěn)定的方向依賴(lài)
實(shí)戰(zhàn)快速實(shí)現(xiàn)需求1: 存在一個(gè)學(xué)生的列表,查找一個(gè)年齡等于18歲的學(xué)生
public static Student findByAge(Student[] students) { for (int i=0; i上述實(shí)現(xiàn)存在很多設(shè)計(jì)的「壞味道」:
缺乏彈性參數(shù)類(lèi)型:只支持?jǐn)?shù)組類(lèi)型,List, Set都被拒之門(mén)外;
容易出錯(cuò):操作數(shù)組下標(biāo),往往引入不經(jīng)意的錯(cuò)誤;
幻數(shù):硬編碼,將算法與配置高度耦合;
返回null:再次給用戶(hù)打開(kāi)了犯錯(cuò)的大門(mén);
使用for-each按照「最小依賴(lài)原則」,先隱藏?cái)?shù)組下標(biāo)的實(shí)現(xiàn)細(xì)節(jié),使用for-each降低錯(cuò)誤發(fā)生的可能性。
public static Student findByAge(Student[] students) { for (Student s : students) if (s.getAge() == 18) return s; return null; }重復(fù)設(shè)計(jì)需求2: 查找一個(gè)名字為horance的學(xué)生
Copy-Paste是最快的實(shí)現(xiàn)方法,但會(huì)產(chǎn)生「重復(fù)設(shè)計(jì)」。
public static Student findByName(Student[] students) { for (Student s : students) if (s.getName().equals("horance")) return s; return null; }為了消除重復(fù),可以將「查找算法」與「比較準(zhǔn)則」這兩個(gè)「變化方向」進(jìn)行分離。
抽象準(zhǔn)則首先將比較的準(zhǔn)則進(jìn)行抽象化,讓其獨(dú)立變化。
public interface StudentPredicate { boolean test(Student s); }將各個(gè)「變化原因」對(duì)象化,為此建立了兩個(gè)簡(jiǎn)單的算子。
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); } }此刻,查找算法的方法名也應(yīng)該被「重命名」,使其保持在同一個(gè)「抽象層次」上。
public static Student find(Student[] students, StudentPredicate p) { for (Student s : students) if (p.test(s)) return s; return null; }客戶(hù)端的調(diào)用根據(jù)場(chǎng)景,提供算法的配置。
assertThat(find(students, new AgePredicate(18)), notNullValue()); assertThat(find(students, new NamePredicate("horance")), notNullValue());結(jié)構(gòu)性重復(fù)AgePredicate和NamePredicate存在「結(jié)構(gòu)型重復(fù)」,需要進(jìn)一步消除重復(fù)。經(jīng)分析兩個(gè)類(lèi)的存在無(wú)非是為了實(shí)現(xiàn)「閉包」的能力,可以使用lambda表達(dá)式,「Code As Data」,簡(jiǎn)明扼要。
assertThat(find(students, s -> s.getAge() == 18), notNullValue()); assertThat(find(students, s -> s.getName().equals("horance")), notNullValue());引入Iterable按照「向穩(wěn)定的方向依賴(lài)」的原則,為了適應(yīng)諸如List, Set等多種數(shù)據(jù)結(jié)構(gòu),甚至包括原生的數(shù)組類(lèi)型,可以將入?yún)⒅貥?gòu)為重構(gòu)為更加抽象的Iterable類(lèi)型。
public static Student find(Iterablestudents, StudentPredicate p) { for (Student s : students) if (p.test(s)) return s; return null; } 類(lèi)型重復(fù)需求3: 存在一個(gè)老師列表,查找第一個(gè)女老師
按照既有的代碼結(jié)構(gòu),可以通過(guò)Copy Paste快速地實(shí)現(xiàn)這個(gè)功能。
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; } 用戶(hù)接口依然可以使用Lambda表達(dá)式。
assertThat(find(teachers, t -> t.female()), notNullValue());如果使用Method Reference,可以進(jìn)一步地改善表達(dá)力。
assertThat(find(teachers, Teacher::female), notNullValue());類(lèi)型參數(shù)化分析StudentMacher/TeacherPredicate, find(Iterable
)/find(Iterable 的重復(fù),為此引入「類(lèi)型參數(shù)化」的設(shè)計(jì)。) 首先消除StudentPredicate和TeacherPredicate的重復(fù)設(shè)計(jì)。
public interface Predicate{ boolean test(E e); } 再對(duì)find進(jìn)行類(lèi)型參數(shù)化設(shè)計(jì)。
public static型變E find(Iterable c, Predicate p) { for (E e : c) if (p.test(e)) return e; return null; } 但find的類(lèi)型參數(shù)缺乏「型變」的能力,為此引入「型變」能力的支持,接口更加具有可復(fù)用性。
public static復(fù)用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.
觀察如下兩個(gè)測(cè)試用例,如果做到極致,可認(rèn)為兩個(gè)lambda表達(dá)式也是重復(fù)的。從「分離變化的方向」的角度分析,此lambda表達(dá)式承載的「比較算法」與「參數(shù)配置」兩個(gè)職責(zé),應(yīng)該對(duì)其進(jìn)行分離。
assertThat(find(students, s -> s.getName().equals("Horance")), notNullValue()); assertThat(find(students, s -> s.getName().equals("Tomas")), notNullValue());可以通過(guò)「Static Factory Method」生產(chǎn)lambda表達(dá)式,將比較算法封裝起來(lái);而配置參數(shù)通過(guò)引入「參數(shù)化」設(shè)計(jì),將「邏輯」與「配置」分離,從而達(dá)到最大化的代碼復(fù)用。
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());組合查詢(xún)但是,上述將lambda表達(dá)式封裝在Factory的設(shè)計(jì)是及其脆弱的。例如,增加如下的需求:
需求4: 查找年齡不等于18歲的女生
最簡(jiǎn)單的方法就是往StudentPredicates不停地增加「Static Factory Method」,但這樣的設(shè)計(jì)嚴(yán)重違反了「OCP」(開(kāi)放封閉)原則。
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; } } 從需求看,比較準(zhǔn)則增加了眾多的語(yǔ)義,再次運(yùn)用「分離變化方向」的原則,可發(fā)現(xiàn)存在兩類(lèi)運(yùn)算的規(guī)則:
比較運(yùn)算:==, !=
邏輯運(yùn)算:&&, ||
比較語(yǔ)義先處理比較運(yùn)算的變化方向,為此建立一個(gè)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的設(shè)計(jì)運(yùn)用了「函數(shù)式」的思維,其行為表現(xiàn)為「高階函數(shù)」的特性,通過(guò)函數(shù)的「組合式設(shè)計(jì)」完成功能的自由拼裝組合,簡(jiǎn)單、直接、漂亮。
public final class StudentPredicates { ...... public static Predicateage(Matcher m) { return s -> m.matches(s.getAge()); } } 查找年齡不等于18歲的學(xué)生,可以如此描述。
assertThat(find(students, age(ne(18))), notNullValue());邏輯語(yǔ)義為了使得邏輯「謂詞」變得更加人性化,可以引入「流式接口」的「DSL」設(shè)計(jì),增強(qiáng)表達(dá)力。
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());重復(fù)再現(xiàn)仔細(xì)的讀者可能已經(jīng)發(fā)現(xiàn)了,Student和Teacher兩個(gè)類(lèi)也存在「結(jié)構(gòu)型重復(fù)」的問(wèn)題。
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; }級(jí)聯(lián)反應(yīng)Student與Teacher的結(jié)構(gòu)性重復(fù),導(dǎo)致StudentPredicates與TeacherPredicates也存在「結(jié)構(gòu)性重復(fù)」。
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()); } } 為此需要進(jìn)一步消除重復(fù)。
提取基類(lèi)第一個(gè)直覺(jué),通過(guò)「提取基類(lèi)」的重構(gòu)方法,消除Student和Teacher的重復(fù)設(shè)計(jì)。
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; }從而實(shí)現(xiàn)了進(jìn)一步消除了Student和Teacher之間的重復(fù)設(shè)計(jì)。
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); } }類(lèi)型界定此時(shí),可以通過(guò)引入「類(lèi)型界定」的泛型設(shè)計(jì),使得StudentPredicates與TeacherPredicates合二為一,進(jìn)一步消除重復(fù)設(shè)計(jì)。
public final class HumanPredicates { ...... public static消滅繼承關(guān)系Predicate age(Matcher m) { return s -> m.matches(s.getAge()); } } Student和Teacher依然存在「結(jié)構(gòu)型重復(fù)」的問(wèn)題,可以通過(guò)Static Factory Method的設(shè)計(jì)方法,并讓Human的構(gòu)造函數(shù)「私有化」,刪除Student和Teacher兩個(gè)子類(lèi),徹底消除兩者之間的「重復(fù)設(shè)計(jì)」。
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); } ...... }消滅類(lèi)型界定Human的重構(gòu),使得HumanPredicates的「類(lèi)型界定」變得多余,從而進(jìn)一步簡(jiǎn)化了設(shè)計(jì)。
public final class HumanPredicates { ...... public static Predicate絕不返回nullage(Matcher m) { return s -> m.matches(s.getAge()); } } Billion-Dollar Mistake
在最開(kāi)始,我們遺留了一個(gè)問(wèn)題:find返回了null。用戶(hù)調(diào)用返回null的接口時(shí),常常忘記null的檢查,導(dǎo)致在運(yùn)行時(shí)發(fā)生NullPointerException異常。
按照「向穩(wěn)定的方向依賴(lài)」的原則,find的返回值應(yīng)該設(shè)計(jì)為Optional
,使用「類(lèi)型系統(tǒng)」的特長(zhǎng),取得如下方面的優(yōu)勢(shì): 顯式地表達(dá)了不存在的語(yǔ)義;
編譯時(shí)保證錯(cuò)誤的發(fā)生;
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(); } 通過(guò)4個(gè)需求的迭代和演進(jìn),通過(guò)運(yùn)用「正交設(shè)計(jì)」和「組合式設(shè)計(jì)」的基本思想,加深對(duì)「正交設(shè)計(jì)基本原則」的理解。
鳴謝「正交設(shè)計(jì)」的理論、原則、及其方法論出自前ThoughtWorks軟件大師「袁英杰」先生。英杰既是我的老師,也是我的摯友;他高深莫測(cè)的軟件設(shè)計(jì)的修為,及其對(duì)軟件設(shè)計(jì)獨(dú)特的哲學(xué)思維方式,是我等后輩學(xué)習(xí)的楷模。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/65584.html
摘要:需要結(jié)合其他測(cè)試用例設(shè)計(jì)的方法進(jìn)行補(bǔ)充。比如邊界值邊界值在軟件中邊界值測(cè)試方法是發(fā)現(xiàn)錯(cuò)誤能力最強(qiáng)的一種。其中,原因是表示輸入條件,結(jié)果是對(duì)輸入執(zhí)行的一系列計(jì)算后得到的輸出。與取值或,表示某狀態(tài)不出現(xiàn),則表示某狀態(tài)出現(xiàn)。 ...
摘要:因果圖分析法是一種圖解法分析輸入的各種組合情況,從而設(shè)計(jì)測(cè)試用例的方法。工具錯(cuò)誤推測(cè)法根據(jù)實(shí)際經(jīng)驗(yàn)或推測(cè)分析列出所有可能存在的和容易發(fā)生錯(cuò)誤的情況,并有針對(duì)性的設(shè)計(jì)測(cè)試用例。 1.等價(jià)類(lèi)劃分 等價(jià)類(lèi)是指某個(gè)輸入域的子集合.在該子集合中,各個(gè)輸入數(shù)據(jù)對(duì)于揭露程序中的錯(cuò)誤都是等效的.并合理地假定:測(cè)試某等價(jià)類(lèi)的代表值就等于對(duì)這一類(lèi)其它值的測(cè)試。 把輸入數(shù)據(jù)合理地劃分等價(jià)類(lèi),在每一個(gè)等價(jià)類(lèi)中...
摘要:注本文內(nèi)容來(lái)深入面向?qū)ο竽J脚c實(shí)踐中節(jié)。面向?qū)ο笤O(shè)計(jì)與過(guò)程式編程面向?qū)ο笤O(shè)計(jì)和過(guò)程式編程有什么不同呢可能有些人認(rèn)為最大的不同在于面向?qū)ο缶幊讨邪瑢?duì)象。面向?qū)ο缶幊毯瓦^(guò)程式編程的一個(gè)核心區(qū)別是如何分配職責(zé)。 注:本文內(nèi)容來(lái)中6.2節(jié)。 6.2 面向?qū)ο笤O(shè)計(jì)與過(guò)程式編程 ??面向?qū)ο笤O(shè)計(jì)和過(guò)程式編程有什么不同呢?可能有些人認(rèn)為最大的不同在于面向?qū)ο缶幊讨邪瑢?duì)象。事實(shí)上,這種說(shuō)法不準(zhǔn)確。...
摘要:為了解決以上問(wèn)題,我們的分流系統(tǒng)選擇基于實(shí)現(xiàn),通過(guò)或者協(xié)議來(lái)傳遞分流信息。正交是指用戶(hù)進(jìn)入所有的實(shí)驗(yàn)之間沒(méi)有必然關(guān)系。流量層內(nèi)實(shí)驗(yàn)分流流量層內(nèi)實(shí)驗(yàn)的因子有設(shè)備流量層。統(tǒng)計(jì)功效對(duì)于置信區(qū)間特征值等產(chǎn)品化功能支持。 什么是 ABTest 產(chǎn)品的改變不是由我們隨便「拍腦袋」得出,而是需要由實(shí)際的數(shù)據(jù)驅(qū)動(dòng),讓用戶(hù)的反饋來(lái)指導(dǎo)我們?nèi)绾胃玫馗纳品?wù)。正如馬蜂窩 CEO 陳罡在接受專(zhuān)訪(fǎng)時(shí)所說(shuō):「有...
閱讀 2158·2021-10-12 10:11
閱讀 855·2021-10-09 09:41
閱讀 3778·2021-09-09 11:37
閱讀 1957·2021-09-08 10:41
閱讀 2652·2019-08-30 12:58
閱讀 2379·2019-08-30 10:58
閱讀 1289·2019-08-26 13:40
閱讀 4133·2019-08-26 13:36