OO makes code understandable by encapsulating moving parting, but FP makes code understandable by minimizing moving parts. -Michael Feathersjava
劉光聰,程序員,敏捷教練,開源軟件愛好者,具備多年大型遺留系統的重構經驗,對OO
,FP
,DSL
等領域具備濃厚的興趣。git
GitHub: https://github.com/horance-liu程序員
Email: horance@outlook.comgithub
需求1:在倉庫中查找全部顏色爲紅色的產品算法
public ArrayList findAllRedProducts(ArrayList repo) { ArrayList result = new ArrayList(); for (int i=0; i<repo.size(); i++) { Product product = (Product)repo[i]; if (product.getColor() == Color.RED) { result.add(product); } } return result; }
指令式(Imperative)設計模式
缺少編譯時類型安全性檢查安全
實現類型閉包
硬編碼併發
重複設計app
for-each
public List<Product> findAllRedProducts(List<Product> repo) { List<Product> result = new ArrayList<>(); for (Product p : repo) { if (p.getColor() == Color.RED) { result.add(p); } } return result; }
需求2:在倉庫中查找全部顏色爲綠色的產品
Copy-Paste
是大部分程序員最容易犯的毛病,爲此引入了大量的重複代碼。
public List<Product> findAllGreenProducts(List<Product> repo) { List<Product> result = new ArrayList<>(); for (Product p : repo) { if (p.getColor() == Color.GREEN) { result.add(p); } } return result; }
爲了消滅Hard Code
和重複代碼,獲得可重用的代碼,能夠引入簡單的參數化設計。
public List<Product> findProductsByColor(List<Product> repo, Color color) { List<Product> result = new ArrayList<>(); for (Product p : repo) { if (p.getColor() == color) { result.add(p); } } return result; }
需求3:查找全部重量小於10的全部產品
大部分程序員依然會使用Copy-Paste
解決這個問題,拒絕Copy-Paste
的陋習,最具實效的一個辦法就是把Copy-Paste
的快捷鍵失效,當每次嘗試Copy-Paste
時提醒本身作更好的設計。
public List<Product> findProductsBelowWeight(List<Product> repo, int weight) { List<Product> result = new ArrayList<>(); for (Product p : repo) { if (p.getWeight() < weight) { result.add(p); } } return result; }
爲了消除二者重複的代碼,經過簡單的參數化每每不能完美解決這類問題,相反會引入額外的複雜度。
public List<Product> findProducts(List<Product> repo, Color color, int weight, boolean flag) { List<Product> result = new ArrayList<>(); for (Product p : repo) { if ((flag && p.getColor() == color) || (!flag && p.getWeight() < weight)) { result.add(p); } } return result; }
平常工做中這樣的實現手法很是廣泛,函數的參數列表隨着需求增長不斷增長,函數邏輯承擔的職責愈來愈多,邏輯也變得愈來愈難以控制。
爲此須要抽取出隱藏的概念,使其遍歷的算法與查找的標準可以獨立地變化,將行爲參數化。
public interface ProductSpec { boolean satisfy(Product product); }
此刻findProducts
的算法邏輯獲得封閉。
public List<Product> findProducts(List<Product> repo, ProductSpec spec) { List<Product> result = new ArrayList<>(); for (Product p : repo) { if (spec.satisfy(p)) { result.add(p); } } return result; }
經過可複用的Functor
來封裝各類變化,讓變化的因素控制在最小的範圍內。
public class ColorSpec implements ProductSpec { private Color color; public ColorSpec(Color color) { this.color = color; } @Override public boolean satisfy(Product product) { return product.getColor() == color; } }
public class BelowWeightSpec implements ProductSpec { private int limit; public BelowWeightSpec(int limit) { this.limit = limit; } @Override public boolean satisfy(Product product) { return product.getWeight() < limit; } }
用戶的接口也變得簡單多了,並且富有表現力。
List<Product> products = findProducts(repo, new ColorSpec(RED));
這是經典的OO
設計,若是熟悉設計模式的讀者對此已經習覺得常了。設計模式是好東西,但經常被人依葫蘆畫瓢,死板照抄,甚至被濫用。事實上,引入或去除設計模式是一個很天然的過程。與大師們交流,問究此處爲什麼引入設計模式,獲得的答案:直覺。忘記全部設計模式吧,管它是否是模式,若是設計是簡單的,它這就是模式。
至此,代碼另外還有一個明顯的壞味道,ColorSpec
和BelowWeightSpec
都須要繼承ProductSpec
,都須要定義一個構造函數和一個私有的字段,並重寫satisfy
方法,這是一種典型的重複現象:重複型結構。
因Java
缺少閉包的支持,程序員不得不承受這樣的煩惱,但此刻暫時不關心,繼續前進。
需求4:查找全部顏色爲紅色或者綠色,而且重量小於10的產品
按照既有的代碼結構,每每易於設計出相似ColorAndBelowWeightSpec
的實現。
public class ColorAndBelowWeightSpec implements ProductSpec { private Color color1; private Color color2; private int limit; public ColorAndBelowWeightSpec(Color color1, Color color2, int limit) { this.color1 = color1; this.color2 = color2; this.limit = limit; } @Override public boolean satisfy(Product p) { return (p.getColor() == color1 || p.getColor() == color2) && (p.getWeight() < limit); } }
存在兩個明顯的壞味道:
類名中包含And
每每是違背單一職責的信號燈
ColorAndBelowWeightSpec
的實現與ColorSpec
,BelowWeightSpec
之間存在明顯的重複
此刻,須要尋找更本質的抽象來表達設計,引入and/or
的語義模型。
Composite Spec: AndSpec, OrSpec
Atomic Spec:ColorSpec, BeblowWeightSpec
publc class AndSpec implements ProductSpec { private List<ProductSpec> specs = new ArrayList<>(); public AndSpec(ProductSpec... specs) { this.specs.addAll(Arrays.asList(specs)); } @Override public boolean satisfy(Product p) { for (ProductSpec spec : specs) { if (!spec.satisfy(p)) return false; } return true; } }
publc class OrSpec implements ProductSpec { private List<ProductSpec> specs = new ArrayList<>(); public OrSpec(ProductSpec... specs) { this.specs.addAll(Arrays.asList(specs)); } @Override public boolean satisfy(Product p) { for (ProductSpec spec : specs) { if (spec.satisfy(p)) return true; } return false; } ![clipboard.png](/img/bVtn06) }
能夠經過AndSpec
組合ColorSpec, BelowWeightSpec
來實現需求,簡單漂亮,而且富有表達力。
List<Product> products = findProducts(repo, new AndSpec( new OrSpec(new ColorSpec(RED), new ColorSpec(Greeen)), new BelowWeightSpec(10));
此時設計存在兩個嚴重的壞味道:
AndSpec
與OrSpec
存在明顯的代碼重複
大堆的new
讓人眼花繚亂
先嚐試消除AndSpec
與OrSpec
存在的代碼重複,OO
設計的第一個直覺就是經過抽取基類。
class CombinableSpec implements ProductSpec { private List<ProductSpec> specs = new ArrayList<>(); private boolean shortcut; protected CombinableSpec(List<ProductSpec> specs, boolean shortcut) { this.specs.addAll(specs); this.shortcut = shortcut; } @Override public boolean satisfy(Product p) { for (ProductSpec spec : specs) { if (spec.satisfy(p) == shortcut) return shortcut; } return !shortcut; } }
經過參數化配置,複用CombinableSpec
的實現。
publc class AndSpec extends CombinableSpec { public AndSpec(ProductSpec... specs) { super(Arrays.asList(specs), false); } }
publc class OrSpec extends CombinableSpec { public OrSpec(ProductSpec... specs) { super(Arrays.asList(specs), true); } }
如何評判boolean
接口的使用呢?在不損傷可理解性的前提下,爲了消除重複的設計是值得推薦的。boolean
接口的可理解性關鍵依賴於調用點與函數接口之間的距離,若是在同一個文件,同一個類,並能在一個頁面顯示的,是徹底能夠接受的。
需求5:查找全部顏色爲不是紅色的產品
publc class NotSpec implements ProductSpec { private ProductSpec spec; public NotSpec(ProductSpec spec) { this.spec = spec; } @Override public boolean satisfy(Product p) { return !spec.satisfy(p); } }
NotSpec
是一種修飾了的ProductSpec
,同時也使得用戶的接口也變得更加人性化了。
List<Product> products = findProducts(repo, new NotSpec(new ColorSpec(RED)));
以前遺留了一個問題,一大堆眼花繚亂的new
使得代碼失去了部分的可讀性。
List<Product> products = findProducts(repo, new AndSpec( new OrSpec(new ColorSpec(RED), new ColorSpec(Greeen)), new BelowWeightSpec(10));
能夠引入DSL
改善程序的可讀性,讓代碼更具表達力。
List<Product> products = findProducts(repo, and(or(color(RED), color(GREEN)), belowWeight(10)));
上述的DSL可使用static factory
的設計手段簡單實現。按照慣例,能夠創建相似於ProductSpecs
的工具類,將這些工廠方法搬遷到工具類中去。
接口與對應工具類的對稱性設計在Java
社區中應用很是普遍,例如標準庫中的java.util.Collection/java.util.Collections
的設計。
public interface ProductSpec { boolean satisfy(Product p); }
public final class ProductSpecs { public static ProductSpec color(final Color color) { return new ProductSpec() { @Override public boolean satisfy(Product p) { return p.getColor() == color; } }; } public static ProductSpec belowWeight(final int limit) { return new ProductSpec() { @Override public boolean satisfy(Product p) { return p.getWeight() < limit; } }; } public static ProductSpec and(ProductSpec... specs) { return new CombinableSpec(Arrays.asList(specs), false); } public static ProductSpec or(ProductSpec... specs) { return new CombinableSpec(Arrays.asList(specs), true); } public static ProductSpec not(final ProductSpec spec) { return new ProductSpec() { @Override public boolean satisfy(Product p) { return !spec.satisfy(p); } }; } private ProductSpecs() { throw new AssertionError("no instances"); } }
此外,使用匿名內部類,能夠獲得意外的驚喜。經過有限地引入閉包的概念,從而避免了相似Firth Attempt/Sixth Attempt的設計中引入多餘的構造函數和成員變量的複雜度,從而消除了部分的結構性重複的壞味道。
固然,要讓這些static factory
可見,須要import static
導入這些方法。
import static practical.programming.overview.ProductSpec.*; List<Product> products = findProducts(repo, not(and(color(RED), belowWeight(10)));
使用Java8
能夠將這些工廠方法直接搬遷到ProductSpec
的接口中去,這樣作至少獲得兩個好處。
能夠刪除ProductSpecs
的工具類
使的接口和靜態方法(尤爲靜態工廠方法)關係更加緊密
Java8
並無由於comparing
等靜態工廠方法的加強而創建Comparators
的工具類,而是直接將它們集成在Comparator
的接口中,這是自Java8
以後思惟的一個新的轉變(Comparator.comparing
的實現留做做業鞏固今天所學知識)。
對於本例,能夠將ProductSpecs
刪除,將全部靜態工廠方法搬遷到ProductSpec
中去。
public interface ProductSpec { boolean satisfy(Product p); static ProductSpec color(Color color) { return new ProductSpec() { @Override public boolean satisfy(Product p) { return p.getColor() == color; } }; } static ProductSpec belowWeight(int limit) { return new ProductSpec() { @Override public boolean satisfy(Product p) { return p.getWeight() < limit; } }; } static ProductSpec and(ProductSpec... specs) { return new CombinableSpec(Arrays.asList(specs), false); } static ProductSpec or(ProductSpec... specs) { return new CombinableSpec(Arrays.asList(specs), true); } static ProductSpec not(ProductSpec spec) { return new ProductSpec() { @Override public boolean satisfy(Product p) { return !spec.satisfy(p); } }; } }
需求6:無條件過濾掉或不過濾查找全部產品
import static practical.programming.overview.ProductSpec.*; List<Product> products = findProducts(repo, always(false));
public interface ProductSpec { boolean satisfy(Product p); static ProductSpec always(boolean bool) { return new ProductSpec() { @Override public boolean satisfy(Product p) { return bool; } }; } }
至此,ProductSpec
存在以下一些類型:
Composite Specs: and, or
Decorator Specs: not
Atomic Specs: always, color, beblowWeight
Java8
可使用Lambda
表達式改善設計,加強表達力。
List<Product> products = findProducts(repo, (Product p) -> p.getColor() == RED);
經過類型推演,能夠進一步省略Labmda
表達式中參數的類型信息。
List<Product> products = findProducts(repo, p -> p.getColor() == RED);
固然,你能夠經過提取static factory
,構造DSL複用這些Lambda
表達式。
@FunctionalInterface public interface ProductSpec { boolean satisfy(Product p); static ProductSpec color(Color color) { return p -> p.getColor() == color; } static ProductSpec weightBelow(int limit) { return p -> p.getWeight() < limit; } }
List<Product> products = findProducts(repo, color(RED));
其中,@FunctionalInterface
註解標註了ProductSpec
是一個函數式接口,其抽象方法boolean satisfy(Product p)
的原型描述了lambda
表達式的Function Descriptor
。
遺留了一個問題: 如何替換匿名內部類,使用lambda
實現 and/or/not/always
的語義?
@FunctionalInterface public interface ProductSpec { boolean satisfy(Product p); default ProductSpec negate() { return p -> !satisfy(p); } default ProductSpec and(ProductSpec other) { return (p) -> satisfy(p) && other.satisfy(p); } default ProductSpec or(ProductSpec other) { return (p) -> satisfy(p) || other.satisfy(p); } static ProductSpec always(boolean bool) { return p -> bool; } static ProductSpec color(Color color) { return p -> p.getColor() == color; } static ProductSpec belowWeight(int limit) { return p -> p.getWeight() < limit; } }
這裏引入了Java8
一個重要的設計工具:default method
,簡單漂亮,並巧妙地實現DSL
的設計,用戶接口變得更加流暢、友好。
List<Product> products = findProducts(repo, color(RED).and(belowWeight(10)));
Java8
支持default method
,擴展了interface
原來的語義,從而隱式地支持了組合式設計,使的OO
的設計更加完善和強大。
需求7:查找全部僞劣的產品
List<Product> products = findProducts(repo, p -> p.fake());
可使用Method Reference
進一步改善lambda
的表達力。
List<Product> products = findProducts(repo, Product::fake);
泛化類型信息,讓算法更具備通用性,並進一步加強代碼的可複用性。
public static <T> List<T> filter(List<T> list, Predicate<T> p) { List<T> result = new ArrayList<>(); for (T e : list) { if (p.test(e)) { result.add(e); } } return result; }
這樣的實現存在一個明顯的問題:泛型參數缺少型變的能力。經過對泛型參數實施無限定類型通配符的修飾,從而使的算法實現更加具備彈性和通用性。
public static <T> List<T> filter(List<? extends T> list, Predicate<? super T> p) { List<T> result = new ArrayList<>(); for (T e : list) { if (p.test(e)) { result.add(e); } } return result; }
and, or, not, always
在代數系統中具備穩定的抽象,爲此須要進一步重構,以便最大化代碼的可複用性。這樣當須要創建諸如NumberSpec, FruitSpec
時無需重複地再寫一遍and, or, not, always
的實現。
爲此,創建更爲抽象的Predicate
的概念,並將通用的、抽象的negate, and, or, always
搬遷到Predicate
中去,使其具備更大的可複用性。
@FunctionalInterface public interface Predicate<T> { boolean test(T t); default Predicate negate() { return p -> !satisfy(p); } default Predicate<T> and(Predicate<? super T> other) { return p -> satisfy(p) && other.satisfy(p); } default Predicate<T> or(Predicate<? super T> other) { return p -> satisfy(p) || other.satisfy(p); } static Predicate<T> always(boolean bool) { return p -> bool; } }
同時,將領域內的color, belowWeight
等原子放回ProductSpecs
工具類中去(由於不存在ProductSpec
的接口了),讓領域內的lambda
表達式具備更大的複用性。
public final class ProductSpecs { public static Predicate<Product> color(Color color) { return p -> p.getColor() == color; } public static Predicate<Product> belowWeight(int limit) { return p -> p.getWeight() < limit; } private ProductSpecs() { throw new AssertionError("no instances"); } }
至此,可複用的基礎設施便從領域中剝離出來,使其具備更高度的可重用性。
Java8
可使用集合庫的Stream
複用代碼。
import static java.util.stream.Collectors.toList; repo.stream() .filter(p -> p.getColor() == RED && p.getPrice() < 10) .collect(toList());
若是要支持併發,則能夠構建parallelStream
。
import static java.util.stream.Collectors.toList; repo.parallelStream() .filter(p -> p.getColor() == RED && p.getPrice() < 10) .collect(toList());
集合類經過stream, parallelStream
工廠方法建立Stream
以後,其操做可分爲2
種基本類型:
Transformation:其返回值爲Stream
類型
Action:其返回值不是Stream
類型
經過Stream
的機制,實現了集合類的惰性求值,直至Action
才真正地開始執行計算。Transformation
從某種意義上,能夠當作是Stream
的Builder
,直至Action
啓動執行。
Scala
語言是一門跨越OO
和FP
的一個混血兒,能夠方便地與Java
進行互操做。在Scala
中,函數做爲一等公民,使用Lambda
是一個很天然的過程。當你熟悉了Scala
,我相信你絕對會放棄Java
,放棄Java8
,猶如做者本人同樣。
repo.filter(p => p.color == RED && p.weight < 10)
遺留了三個問題:
如何複用lambda
表達式?
如何實現 and/or/not
的語義?
如何實現 always
的語義?
引入靜態工廠方法及其操做符重載的機制構造內部DSL
。
import ProductSpec._ repo.filter(color(RED) && belowWeight(10))
object ProductSpec { def color(color: Color) = ??? def bebowWeight(limit: Int) = ??? }
如何替換實現???
,並讓其具備&&, ||, !
的語義呢?
object ProductSpec { def color(color: Color) = new Predicate((p: Product) => p.color == color) def bebowWeight(limit: Int) = new Predicate((p: Product) => p.weight < limit) }
Predicate
一個擴展匿名函數A => Boolean
的子類,其中,從面向對象的角度看,A => Boolean
的類型爲Function[A, Boolean]
。
class Predicate[A](pred: A => Boolean) extends (A => Boolean) { override def apply(a: A) = pred(a) def &&(that: A => Boolean) = new Predicate[A](x => pred(x) && that(x)) def ||(that: A => Boolean) = new Predicate[A](x => pred(x) || that(x)) def unary_! = new Predicate[A](x => !pred(x)) }
其中!
是一個一元操做符。
always
靜態工廠方法,能夠搬遷到Predicate
的伴生對象中去。
object Predicate { def always[A](bool: Boolean) = new Predicate[A](_ => bool) }
Predicate
的設計既使用了OO
的特性,又引入了FP
的思惟,Scala
使其二者如此和諧、完美,簡直難以想象。
世界是多樣性的,計算機工業也不只僅只存在一種方法論。在個人哲學觀裏,OO和FP之間並不矛盾,而是一個和諧的,相互補充的統一體。
除了C++語言以外,使得我最偏心Scala,多範式,一個問題存在多種解決方案等等思惟習慣,給了程序員最靈活、最自由的空間。
Comprator.comparing
以標準庫Collections.sort
,及其Comparator
在Java8
中的加強,及其Comparator.comparing
的泛型定義複習今天所學知識。
public final class Collectins { private Collectins() { } public static <T> void sort(List<T> list, Comparator<? super T> c) { list.sort(c); } }
使用匿名內部類是Collectins.sort
最經典的使用方法之一。
Collections.sort(products, new Comparator() { @Override public int compare(Product p1, Product p2) { return p1.getName().compareTo(p2.getName); } });
能夠經過lambda
表達式替代匿名內部類,簡化設計。
Collections.sort(products, (Product p1, Product p2) -> p1.getName().compareTo(p2.getName));
經過類型推演,但依然獲得編譯器類型安全的保護。
Collections.sort(products, (p1, p2) -> p1.getName().compareTo(p2.getName));
經過Comprator.compring
的靜態工廠方法,改善表達力。
Collections.sort(persons, comparing(p -> p.getName()))
經過Function Reference
的機制,進一步改善表達力。
Collections.sort(persons, comparing(Person::getName()))
其中,Comprator.compring
的實現爲:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> extractor) { return (c1, c2) -> extractor.apply(c1).compareTo(extractor.apply(c2)); } }