OO makes code understandable by encapsulating moving parting, but FP makes code understandable by minimizing moving parts. -Michael Feathers
Product Repository

First Attempt: The Worst Implement
需求1:在倉庫中查找所有顏色為紅色的產(chǎn)品
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)
- 缺乏編譯時類型安全性檢查
- 實現(xiàn)類型
- 硬編碼
- 重復設計
Second Attempt: Using 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;
}
Third Attempt: Parameterizing
需求2:在倉庫中查找所有顏色為綠色的產(chǎn)品
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和重復代碼,得到可重用的代碼,可以引入簡單的參數(shù)化設計。
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;
}
Forth Attempt: Parameterizing with Every Attribute You Can Think Of
需求3:查找所有重量小于10的所有產(chǎn)品
大部分程序員依然會使用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;
}
為了消除兩者重復的代碼,通過簡單的參數(shù)化往往不能完美解決這類問題,相反會引入額外的復雜度。
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;
}
日常工作中這樣的實現(xiàn)手法非常普遍,函數(shù)的參數(shù)列表隨著需求增加不斷增加,函數(shù)邏輯承擔的職責越來越多,邏輯也變得越來越難以控制。
Firth Attempt: Abstracting over Criteria
為此需要抽取出隱藏的概念,使其遍歷的算法與查找的標準能夠獨立地變化,將行為參數(shù)化。
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來封裝各種變化,讓變化的因素控制在最小的范圍內(nèi)。
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;
}
}
用戶的接口也變得簡單多了,而且富有表現(xiàn)力。
List<Product> products = findProducts(repo, new ColorSpec(RED));

這是經(jīng)典的OO設計,如果熟悉設計模式的讀者對此已經(jīng)習以為常了。設計模式是好東西,但常常被人依葫蘆畫瓢,死板照抄,甚至被濫用。事實上,引入或去除設計模式是一個很自然的過程。與大師們交流,問究此處為何引入設計模式,得到的答案:直覺。忘記所有設計模式吧,管它是不是模式,如果設計是簡單的,它這就是模式。
至此,代碼另外還有一個明顯的壞味道,ColorSpec和BelowWeightSpec都需要繼承ProductSpec,都需要定義一個構(gòu)造函數(shù)和一個私有的字段,并重寫satisfy方法,這是一種典型的重復現(xiàn)象:重復型結(jié)構(gòu)。
因Java缺乏閉包的支持,程序員不得不承受這樣的煩惱,但此刻暫時不關心,繼續(xù)前進。
Sixth Attempt: Composite Criteria
需求4:查找所有顏色為紅色或者綠色,并且重量小于10的產(chǎn)品
按照既有的代碼結(jié)構(gòu),往往易于設計出類似ColorAndBelowWeightSpec的實現(xiàn)。
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的實現(xiàn)與ColorSpec,BelowWeightSpec之間存在明顯的重復
此刻,需要尋找更本質(zhì)的抽象來表達設計,引入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;
}
}
可以通過AndSpec組合ColorSpec, BelowWeightSpec來實現(xiàn)需求,簡單漂亮,并且富有表達力。
List<Product> products = findProducts(repo, new AndSpec(
new OrSpec(new ColorSpec(RED), new ColorSpec(Greeen)), new BelowWeightSpec(10));
此時設計存在兩個嚴重的壞味道:
-
AndSpec與OrSpec存在明顯的代碼重復 - 大堆的
new讓人眼花繚亂
Seventh Attempt: Extract Parent
先嘗試消除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;
}
}
通過參數(shù)化配置,復用CombinableSpec的實現(xiàn)。
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接口的可理解性關鍵依賴于調(diào)用點與函數(shù)接口之間的距離,如果在同一個文件,同一個類,并能在一個頁面顯示的,是完全可以接受的。
Eighth Attempt: Decorate Criteria
需求5:查找所有顏色為不是紅色的產(chǎn)品
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)));

Ninth Attempt: Using Static Factory to DSL
之前遺留了一個問題,一大堆眼花繚亂的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的設計手段簡單實現(xiàn)。按照慣例,可以建立類似于ProductSpecs的工具類,將這些工廠方法搬遷到工具類中去。
接口與對應工具類的對稱性設計在Java社區(qū)中應用非常廣泛,例如標準庫中的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");
}
}
此外,使用匿名內(nèi)部類,可以得到意外的驚喜。通過有限地引入閉包的概念,從而避免了類似Firth Attempt/Sixth Attempt的設計中引入多余的構(gòu)造函數(shù)和成員變量的復雜度,從而消除了部分的結(jié)構(gòu)性重復的壞味道。
當然,要讓這些static factory可見,需要import static導入這些方法。
import static practical.programming.overview.ProductSpec.*;
List<Product> products = findProducts(repo, not(and(color(RED), belowWeight(10)));
Tenth Attempt: Moving Static Factory into Interface
使用Java8可以將這些工廠方法直接搬遷到ProductSpec的接口中去,這樣做至少得到兩個好處。
- 可以刪除
ProductSpecs的工具類 - 使的接口和靜態(tài)方法(尤其靜態(tài)工廠方法)關系更加緊密
Java8并沒有因為comparing等靜態(tài)工廠方法的增強而建立Comparators的工具類,而是直接將它們集成在Comparator的接口中,這是自Java8之后思維的一個新的轉(zhuǎn)變(Comparator.comparing的實現(xiàn)留作作業(yè)鞏固今天所學知識)。
對于本例,可以將ProductSpecs刪除,將所有靜態(tài)工廠方法搬遷到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);
}
};
}
}
Eleventh Attempt: Using Null Object
需求6:無條件過濾掉或不過濾查找所有產(chǎn)品
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
Twelfth Attempt: Using Lambda Expression
Java8可以使用Lambda表達式改善設計,增強表達力。
List<Product> products = findProducts(repo, (Product p) -> p.getColor() == RED);
通過類型推演,可以進一步省略Labmda表達式中參數(shù)的類型信息。
List<Product> products = findProducts(repo, p -> p.getColor() == RED);
當然,你可以通過提取static factory,構(gòu)造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是一個函數(shù)式接口,其抽象方法boolean satisfy(Product p)的原型描述了lambda表達式的Function Descriptor。
Thirteenth Attempt: Chaining Speciafications
遺留了一個問題: 如何替換匿名內(nèi)部類,使用lambda實現(xiàn) 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,簡單漂亮,并巧妙地實現(xiàn)DSL的設計,用戶接口變得更加流暢、友好。
List<Product> products = findProducts(repo, color(RED).and(belowWeight(10)));
Java8支持default method,擴展了interface原來的語義,從而隱式地支持了組合式設計,使的OO的設計更加完善和強大。
Fourteenth attempt: Using Method Reference
需求7:查找所有偽劣的產(chǎn)品
List<Product> products = findProducts(repo, p -> p.fake());
可以使用Method Reference進一步改善lambda的表達力。
List<Product> products = findProducts(repo, Product::fake);
Fifteenth attempt: Abstracting over Type
泛化類型信息,讓算法更具有通用性,并進一步增強代碼的可復用性。
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;
}
這樣的實現(xiàn)存在一個明顯的問題:泛型參數(shù)缺乏型變的能力。通過對泛型參數(shù)實施無限定類型通配符的修飾,從而使的算法實現(xiàn)更加具有彈性和通用性。
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;
}
Sixteenth: Maximize Reusability
and, or, not, always在代數(shù)系統(tǒng)中具有穩(wěn)定的抽象,為此需要進一步重構(gòu),以便最大化代碼的可復用性。這樣當需要建立諸如NumberSpec, FruitSpec時無需重復地再寫一遍and, or, not, always的實現(xiàn)。
為此,建立更為抽象的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;
}
}
同時,將領域內(nèi)的color, belowWeight等原子放回ProductSpecs工具類中去(因為不存在ProductSpec的接口了),讓領域內(nèi)的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");
}
}
至此,可復用的基礎設施便從領域中剝離出來,使其具有更高度的可重用性。
Seventeenth Attempt: Using Stream API
Java8可以使用集合庫的Stream復用代碼。
import static java.util.stream.Collectors.toList;
repo.stream()
.filter(p -> p.getColor() == RED && p.getPrice() < 10)
.collect(toList());
如果要支持并發(fā),則可以構(gòu)建parallelStream。
import static java.util.stream.Collectors.toList;
repo.parallelStream()
.filter(p -> p.getColor() == RED && p.getPrice() < 10)
.collect(toList());
集合類通過stream, parallelStream工廠方法創(chuàng)建Stream之后,其操作可分為2種基本類型:
- Transformation:其返回值為
Stream類型 - Action:其返回值不是
Stream類型
通過Stream的機制,實現(xiàn)了集合類的惰性求值,直至Action才真正地開始執(zhí)行計算。Transformation從某種意義上,可以看成是Stream的Builder,直至Action啟動執(zhí)行。
Eighteenth attempt: Replace Java with Scala
Scala語言是一門跨越OO和FP的一個混血兒,可以方便地與Java進行互操作。在Scala中,函數(shù)作為一等公民,使用Lambda是一個很自然的過程。當你熟悉了Scala,我相信你絕對會放棄Java,放棄Java8,猶如作者本人一樣。
repo.filter(p => p.color == RED && p.weight < 10)
遺留了三個問題:
- 如何復用
lambda表達式? - 如何實現(xiàn)
and/or/not的語義? - 如何實現(xiàn)
always的語義?
Nineteenth Attempt: Abstracting Control Structure
引入靜態(tài)工廠方法及其操作符重載的機制構(gòu)造內(nèi)部DSL。
import ProductSpec._
repo.filter(color(RED) && belowWeight(10))
object ProductSpec {
def color(color: Color) = ???
def bebowWeight(limit: Int) = ???
}
如何替換實現(xiàn)???,并讓其具有&&, ||, !的語義呢?
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一個擴展匿名函數(shù)A => Boolean的子類,其中,從面向?qū)ο蟮慕嵌瓤矗?code>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))
}
其中!是一個一元操作符。
Twentieth Attempt: Using Companion Object
always靜態(tài)工廠方法,可以搬遷到Predicate的伴生對象中去。
object Predicate {
def always[A](bool: Boolean) = new Predicate[A](_ => bool)
}
Predicate的設計既使用了OO的特性,又引入了FP的思維,Scala使其兩者如此和諧、完美,簡直不可思議。
Conclusion
世界是多樣性的,計算機工業(yè)也不僅僅只存在一種方法論。在我的哲學觀里,OO和FP之間并不矛盾,而是一個和諧的,相互補充的統(tǒng)一體。
除了C++語言之外,使得我最偏愛Scala,多范式,一個問題存在多種解決方案等等思維習慣,給了程序員最靈活、最自由的空間。
Review 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);
}
}
使用匿名內(nèi)部類是Collectins.sort最經(jīng)典的使用方法之一。
Collections.sort(products, new Comparator() {
@Override
public int compare(Product p1, Product p2) {
return p1.getName().compareTo(p2.getName);
}
});
可以通過lambda表達式替代匿名內(nèi)部類,簡化設計。
Collections.sort(products, (Product p1, Product p2) -> p1.getName().compareTo(p2.getName));
通過類型推演,但依然得到編譯器類型安全的保護。
Collections.sort(products, (p1, p2) -> p1.getName().compareTo(p2.getName));
通過Comprator.compring的靜態(tài)工廠方法,改善表達力。
Collections.sort(persons, comparing(p -> p.getName()))
通過Function Reference的機制,進一步改善表達力。
Collections.sort(persons, comparing(Person::getName()))
其中,Comprator.compring的實現(xiàn)為:
@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));
}
}