摘要:為了消除重復,可以將查找算法與比較準則這兩個變化方向進行分離。此刻,查找算法的方法名也應該被重命名,使其保持在同一個抽象層次上。結構性重復和存在結構型重復,需要進一步消除重復。
Refactoring to DSL
軟件設計的目標OO makes code understandable by encapsulating moving parting, but FP makes code understandable by minimizing moving parts. -Michael Feathers
實現功能
易于重用
易于理解
沒有冗余
正交設計軟件設計是一個「守破離」的過程。 -- 袁英杰
消除重復
分離變化方向
縮小依賴范圍
向穩(wěn)定的方向依賴
實戰(zhàn)快速實現需求1: 存在一個學生的列表,查找一個年齡等于18歲的學生
public static Student findByAge(Student[] students) { for (int i=0; i上述實現存在很多設計的「壞味道」:
缺乏彈性參數類型:只支持數組類型,List, Set都被拒之門外;
容易出錯:操作數組下標,往往引入不經意的錯誤;
幻數:硬編碼,將算法與配置高度耦合;
返回null:再次給用戶打開了犯錯的大門;
使用for-each按照「最小依賴原則」,先隱藏數組下標的實現細節(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是最快的實現方法,但會產生「重復設計」。
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按照「向穩(wěn)定的方向依賴」的原則,為了適應諸如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; } } 從需求看,比較準則增加了眾多的語義,再次運用「分離變化方向」的原則,可發(fā)現存在兩類運算的規(guī)則:
比較運算:==, !=
邏輯運算:&&, ||
比較語義先處理比較運算的變化方向,為此建立一個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());重復再現仔細的讀者可能已經發(fā)現了,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的檢查,導致在運行時發(fā)生NullPointerException異常。
按照「向穩(wěn)定的方向依賴」的原則,find的返回值應該設計為Optional
,使用「類型系統(tǒng)」的特長,取得如下方面的優(yōu)勢: 顯式地表達了不存在的語義;
編譯時保證錯誤的發(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(); } 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); } } 將所有的Static Factory方法都放在接口中,雖然簡單,也很自然。但如果方法之間產生重復代碼,需要「提取函數」,設計將變得非常不靈活,因為接口內所有方法都將默認為public,這往往不是我們所期望的,為此可以將這些Static Factory方法搬遷到Matchers實用類中去。
public final class Matchers { public static實現大于Matcher eq(T expected) { return actual -> expected.equals(actual); } public static Matcher ne(T expected) { return actual -> !expected.equals(actual); } private Matchers() { } } 需求5: 查找年齡大于18歲的學生
assertThat(find(students, age(gt(18)).isPresent(), is(true));public final class Matchers { ...... public static> Matcher gt(T expected) { return actual -> Ordering. natural().compare(actual, expected) > 0; } } 其中,natural代表了一種自然的比較規(guī)則。
public final class Ordering { public static實現小于> Comparator natural() { return (t1, t2) -> t1.compareTo(t2); } } 需求6: 查找年齡小于18歲的學生
assertThat(find(students, age(lt(18)).isPresent(), is(true));依次類推,「小于」的規(guī)則實現如下:
public final class Matchers { ...... public static提取函數> Matcher gt(T expected) { return actual -> Ordering. natural().compare(actual, expected) > 0; } public static > Matcher lt(T expected) { return actual -> Ordering. natural().compare(actual, expected) < 0; } } 設計產生了明顯的重復,可以通過「提取函數」來消除重復。
public final class Matchers { ...... public static> Matcher gt(T expected) { return actual -> compare(actual, expected) > 0; } public static > Matcher lt(T expected) { return actual -> compare(actual, expected) < 0; } private static > int compare(T actual, T expected) { return Ordering. natural().compare(actual, expected); } } 其余比較操作,例如大于等于,小于等于的設計和實現依此類推,在此不再重述。
包含子串需求7: 查找名字中包含horance的學生
assertThat(find(students, name(contains("horance")).isPresent(), is(true));public final class Matchers { ...... public static Matcher子串開頭contains(String substr) { return str -> str.contains(substr); } } 需求8: 查找名字以horance開頭的學生
assertThat(find(students, name(starts("horance")).isPresent(), is(true));public final class Matchers { ...... public static Matcherstarts(String substr) { return str -> str.startsWith(substr); } } 「子串結尾」的邏輯,可以設計ends的關鍵字,實現依此類推,在此不再重述。
不區(qū)分大小寫需求9: 查找名字以horance開頭,但不區(qū)分大小寫的學生
assertThat(find(students, name(starts_ignoring_case("horance")).isPresent(), is(true));public final class Matchers { ...... public static Matcherstarts(String substr) { return str -> str.startsWith(substr); } public static Matcher starts_ignoring_case(String substr) { return str -> lower(str).startsWith(lower(substr)); } private static String lower(String s) { return s.toLowerCase(); } } starts與starts_ignoring_case之間存在微妙的重復設計,為此需要進一步消除重復。
組合式設計assertThat(find(students, name(ignoring_case(Matchers::starts, "Horance"))).isPresent(), is(true));運用函數的「組合式設計」,達到代碼的最大可復用性。從OO的角度看,ignoring_case是對starts, ends, contains的功能增強,是一種典型的「修飾」關系。
public static Matcherignoring_case( Function > m, String substr) { return str -> m.apply(lower(substr)).matches(lower(str)); } 其中,Function
> 是一個一元函數,參數為String,返回值為Matcher。 @FunctionalInterface public interface Function強迫用戶{ R apply(T t); } 雖然ignoring_case的設計高度可復用性,可由用戶根據實際情況,自由拼裝組合各種算子。但「方法引用」的語法,給用戶給造成了不必要的負擔。
assertThat(find(students, name(ignoring_case(Matchers::starts, "Horance"))).isPresent(), is(true));可以提供starts_ignoring_case的語法糖,將用戶犯錯的幾率降至最低,但要保證實現不存在重復設計。
assertThat(find(students, name(starts_ignoring_case("Horance"))).isPresent(), is(true));此時,ignoring_case也應該重構為private,變?yōu)橐粋€「可重用」的函數。
public static Matcher修飾語義starts_ignoring_case(String substr) { return ignoring_case(Matchers::starts, substr); } private static Matcher ignoring_case( Function > m, String substr) { return str -> m.apply(lower(substr)).matches(lower(str)); } 需求13: 查找名字中不包含horance的第一個學生
assertThat(find(students, name(not_contains("horance")).isPresent(), is(true));public final class Matchers { ...... public static Matchernot_contains(String substr) { return str -> !str.contains(substr); } } 在這之前,也曾遇到過類似的「反義」的操作。例如,查找年齡不等于18歲的學生,可以如此描述。
assertThat(find(students, age(ne(18))).isPresent(), is(true));public final class Matchers { ...... public staticMatcher ne(T expected) { return actual -> !expected.equals(actual); } } 兩者對「反義」的描述存在兩份不同的表示,是一種隱晦的「重復設計」,需要一種巧妙的設計消除重復。
提取反義為此,應該刪除not_contains, ne的關鍵字,并提供統(tǒng)一的not關鍵字。
assertThat(find(students, name(not(contains("horance")))).isPresent(), is(true));not的實現是一種「修飾」的手法,對既有的Matcher功能的增強,巧妙地取得了「反義」功能。
public final class Matchers { ...... public static語法糖Matcher not(Matcher matcher) { return actual -> !matcher.matches(actual); } } 對于not(eq(18))可以設計類似于not(18)的語法糖,使其更加簡單。
assertThat(find(students, age(not(18))).isPresent(), is(true));其實現就是對eq的一種修飾操作。
public final class Matchers { ...... public static邏輯或Matcher not(T expected) { return not(eq(expected)); } } 需求13: 查找名字中包含horance,或者以liu結尾的學生
assertThat(find(students, name(anyof(contains("horance"), ends("liu")))).isPresent(), is(true));public final class Matchers { ...... @SafeVarargs public static邏輯與Matcher anyof(Matcher super T>... matchers) { return actual -> { for (Matcher super T> matcher : matchers) if (matcher.matches(actual)) return true; return false; }; } } 需求14: 查找名字中以horance開頭,并且以liu結尾的學生
assertThat(find(students, name(allof(starts("horance"), ends("liu")))).isPresent(), is(true));public final class Matchers { ...... @SafeVarargs public static短路Matcher allof(Matcher super T>... matchers) { return actual -> { for (Matcher super T> matcher : matchers) if (!matcher.matches(actual)) return false; return true; }; } } allof與anyof之間的實現存在重復設計,可以通過提取函數消除重復。
public final class Matchers { ...... @SafeVarargs private static占位符Matcher combine( boolean shortcut, Matcher super T>... matchers) { return actual -> { for (Matcher super T> matcher : matchers) if (matcher.matches(actual) == shortcut) return shortcut; return !shortcut; }; } @SafeVarargs public static Matcher allof(Matcher super T>... matchers) { return combine(false, matchers); } @SafeVarargs public static Matcher anyof(Matcher super T>... matchers) { return combine(true, matchers); } } 需求15: 查找算法始終失敗或成功
assertThat(find(students, age(always(false))).isPresent(), is(false));public final class Matchers { ...... public static回顧Matcher always(boolean bool) { return e -> bool; } } 通過15個需求的迭代和演進,通過運用「正交設計」和「組合式設計」的基本思想,得到了一套接口豐富、表達力極強的DSL。
這一套簡單的DSL是一個高度可復用的Matcher集合,其設計既包含了OO的方法論,也涉及到了FP的思維,整體性設計保持高度的一致性和統(tǒng)一性。
鳴謝「正交設計」的理論、原則、及其方法論出自前ThoughtWorks軟件大師「袁英杰」先生。英杰既是我的老師,也是我的摯友;其高深莫測的軟件設計的修為,及其對軟件設計獨特的哲學思維方式,是我等后輩學習的楷模。
思考軟件設計的本質是什么?
OO與FP的本質區(qū)別是什么?
組合式設計的精髓是什么?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65596.html
摘要:例如中的操作數據庫時,先取得數據庫的連接,操作數據后確保釋放連接當操作文件時,先打開文件流,操作文件后確保關閉文件流。例如遍歷文件中所有行,并替換制定模式為其他的字符串。使用實現行為的參數化。 OO makes code understandable by encapsulating moving parting, but FP makes code understandable by...
摘要:獲取試讀文章高階函數高階函數就是參數為可以為,并且返回值也可為的函數。比方說,我們現在有顧客名單,但我們需要得到他們的郵箱地址我們現在不用高階函數用一個來實現它,代碼如下。 《Refactoring To Collection》 本文是翻譯Adam Wathan 的《Collection To Refactoring》的試讀篇章,這篇文章內容不多,但是可以為我們Laraver使用者能更...
摘要:注本文是翻譯寫的關于調試技巧,讀完以后很實用,分享給大家閱讀過程中,翻譯有錯誤的希望大家指正原文鏈接最近我一直在使用的,如果你還不了解,我簡單說下一個集合就是一個功能強大的數組有很多強大處理其內部數據的函數但是唯一讓我頭疼的地方是如何調試的 注:本文是翻譯Freek Van der Herten寫的關于Collection調試技巧,,讀完以后很實用,分享給大家.閱讀過程中,翻譯有錯誤的...
摘要:配置安裝插件安裝包用于代碼的格式化將的安裝路徑添加到環(huán)境變量中。生成文件,編輯調試將文件中的改為,這樣無斷點時按或,直接運行程序不調試。方法提取選中要提取的表達式,,輸入或選擇命令。可對選項設置快捷鍵。 VSCode Python 配置 安裝 Python 插件:ext install python 安裝包(用于 Python 代碼的格式化): pip install pep8 ...
閱讀 653·2021-11-23 09:51
閱讀 3600·2021-11-15 11:38
閱讀 927·2021-10-14 09:42
閱讀 3163·2021-09-29 09:35
閱讀 2104·2021-09-03 10:33
閱讀 769·2021-07-30 16:33
閱讀 1558·2019-08-30 15:55
閱讀 1841·2019-08-30 14:04