《HeadFirst設計模式》學習筆記|觀察者模式

NO IMAGE

前言

我最近在看大名鼎鼎的《Head First 設計模式》。這本「OO 聖經」用 Java 實現各類設計模式,對於我 —— 一個非 Java 愛好者而言,讀起來並不過癮。

有人讀完這本書可能會誤解設計模式就是設計 Interface,而事實並非如此。在知乎的一個問題《Python 裡沒有接口,如何寫設計模式?》中,vczh 輪子哥是這樣回答的:

設計模式搞了那麼多東西就是在告訴你如何在各種情況下解耦你的代碼,讓你的代碼在運行時可以互相組合。這就跟兵法一樣。難道有了飛機大炮,兵法就沒有用了嗎?

我覺得這個比喻很好,不同的語言就像不同的兵器,各有各的特點與使用方式,而設計模式就是那套「兵法」,無論你使用何種兵器,不過是「縱橫不出方圓,萬變不離其宗」。而只看書中一種「兵器」未免太少,不如我們多試幾樣?

本篇就來看一看第一章「兵法」 —— 策略模式(Strategy Pattern)。

定義

書中對策略模式的定義如下:

策略模式定義了算法族,分別封裝起來,讓它們之間可以互相替換,此模式讓算法的變化獨立於使用算法的客戶。

下面以書中的「模擬鴨子應用」為例。

繼承的弊端

你要設計一個鴨子游戲,遊戲裡有各種各樣的鴨子,它們會游泳(swim()),還會呱呱叫(quack()),每種鴨子擁有不同的外觀(display())。

一開始,你可能會設計一個鴨子的超類 Duck,然後讓所有不同種類的鴨子繼承它:

《HeadFirst設計模式》學習筆記|觀察者模式

如果此時我們想讓鴨子飛起來,就要在超類中增加一個 fly() 方法:

《HeadFirst設計模式》學習筆記|觀察者模式

此時,鴨子家族來了一隻擅於代碼調試工作的小黃鴨。

《HeadFirst設計模式》學習筆記|觀察者模式

此時,一切都亂套了,這位代碼調試工作者會發出「吱吱」的叫聲,但卻不會飛,然而它卻從鴨子超類繼承了 quack()fly() 方法。為了讓它尊重客觀事實,我們需要在小黃鴨類中覆蓋超類的 quack()fly() 方法,讓它變得不會叫也不會飛。

《HeadFirst設計模式》學習筆記|觀察者模式

雖然我們用「覆蓋方法」的手段解決了小黃鴨的問題,但未來我們可能還會製造更多奇奇怪怪的鴨子。例如周黑鴨或北京烤鴨,它們顯然既不會叫,也不會游泳,還不會飛,這時我們又要為它們重寫所有的行為嗎?利用繼承的方式來為不同種類的鴨子提供行為顯然不夠靈活。

抽離可變行為

不同的鴨子具有不同的行為,鴨子的行為應當是靈活可變的

設計原則一:找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。

因此,利用上述原則,我們把「鴨子的行為」從鴨子類(Duck)中抽離出來。

《HeadFirst設計模式》學習筆記|觀察者模式

實現被抽離的行為

設計原則二:針對接口編程,而不是針對實現編程。

我們將這些被抽離出的行為歸類:

  • 所有具體的飛行行為屬於飛行策略
  • 所有具體的叫聲行為屬於叫聲策略
  • 所有具體的游泳行為屬於游泳策略
  • ……

我們可以利用接口或抽象類代表這些策略,然後讓特定的具體行為來實現這些策略中的方法

例如,我們的飛行策略名為 FlyBehavior,我們將它設計為一個抽象類(當然也可以是接口)。然後,我們有兩種具體的飛行方式 FlyWithWings(會飛)和 FlyNoWay(不會飛),它們需要實現飛行策略中的 fly() 方法:

《HeadFirst設計模式》學習筆記|觀察者模式

整合

此時,我們已經將可變的行為從鴨子超類(Duck)中抽離,並把它們用具體的「行為類」進行表示。我們希望:如果鴨子要執行某個行為,它不需要自己處理,而是將這一行為委託給具體的「行為類」

因此,我們可以在鴨子超類(Duck)中加入「行為類」的實例變量,從而通過這些實例變量來調用具體的行為方法。

《HeadFirst設計模式》學習筆記|觀察者模式

Class Duckfly() 方法中,我們可以使用實例 flyBehavior 調用具體的行為方法,從而達成「委託」的目的:

public function fly() 
{
$this->flyBehavior->fly();
}

具體實現

下面來看看不同語言的具體實現:

PHP

PHP 有抽象類也有接口,語法和 Java 比較接近。實現方法中規中矩,和書中的並無二致。只不過這裡我把行為接口改成了抽象類。類圖如下:

《HeadFirst設計模式》學習筆記|觀察者模式

具體實現:

<?php
// 飛行行為類
abstract class FlyBehavior 
{
abstract public function fly();
}
// 「飛」的具體行為
class FlyWithWings extends FlyBehavior 
{
public function fly() 
{
echo "會飛\n";
}
}
class FlyNoWay extends FlyBehavior 
{
public function fly()
{
echo "不會飛\n";
}
}
// 叫聲行為類
abstract class QuackBehavior
{
abstract public function quack();
}
// 「叫」的具體行為
class Quack extends QuackBehavior 
{
public function quack()
{
echo "呱呱\n";
}
}
class Squeak extends QuackBehavior
{
public function quack()
{
echo "吱吱\n";
}
}
class MuteQuack extends QuackBehavior
{
public function quack()
{
echo "不會叫\n";
}
}
// 鴨子類
abstract class Duck
{
protected $flyStrategy;
protected $quackStrategy;
public function fly()
{
$this->flyStrategy->fly();
}
public function quack()
{
$this->quackStrategy->quack();
}
}
// 有隻小黃鴨
class YellowDuck extends Duck 
{
public function __construct($flyStrategy, $quackStrategy)
{
$this->flyStrategy = $flyStrategy;
$this->quackStrategy = $quackStrategy;
}
}
$yellowDuck = new YellowDuck(new FlyNoWay(), new Squeak());
$yellowDuck->fly();
$yellowDuck->quack();
/* Output:
不會飛
吱吱
*/
?>

Python

Python 就沒有所謂的抽象類和接口了,當然你也可以通過 abc 模塊來實現這些功能。

比較簡單的做法是:將具體行為直接定義為函數,在初始化鴨子時通過構造函數傳入行為函數,賦值給對應的變量。當執行具體行為時,將直接調用被賦值的變量,這時具體的行為動作就被委託給了傳入的行為函數,達到了「委託」的效果。

class Duck:
def __init__(self, fly_strategy, quack_strategy):
self.fly_strategy = fly_strategy
self.quack_strategy = quack_strategy
def fly(self):
self.fly_strategy()
def quack(self):
self.quack_strategy()
def fly_with_wings():
print("會飛")
def fly_no_way():
print("不會飛")
def quack():
print("呱呱")
def squeak():
print("吱吱")
def mute_quack():
print("不會叫")
# 一隻會飛也不會叫的小黃鴨
yellow_duck = Duck(fly_no_way, mute_quack)
yellow_duck.fly()
yellow_duck.quack()
# Output:
# 不會飛
# 不會叫

Golang

在 Go 語言中沒有 extends 關鍵字,但可以通過在結構體中內嵌匿名類型的方式實現繼承關係。此處,將 FlyBehavior 飛行行為和 QuackBehavior 行為聲明為接口。

package main
import "fmt"
// FlyBehavior 飛行行為接口
type FlyBehavior interface {
fly()
}
// QuackBehavior 呱呱叫行為接口
type QuackBehavior interface {
quack()
}
// FlyWithWings 會飛的類
type FlyWithWings struct {
}
func (flyWithWings FlyWithWings) fly() {
fmt.Println("會飛")
}
// FlyWithWings 不會飛的類
type FlyNoWay struct{}
func (flyNoWay FlyNoWay) fly() {
fmt.Println("不會飛")
}
// Quack 呱呱叫
type Quack struct{}
func (quack Quack) quack() {
fmt.Println("呱呱")
}
// Squeak 吱吱叫
type Squeak struct{}
func (squeak Squeak) quack() {
fmt.Println("吱吱")
}
// MuteQuack 不會叫
type MuteQuack struct{}
func (muteQuack MuteQuack) quack() {
fmt.Println("不會叫")
}
// Duck 鴨子類
type Duck struct {
FlyBehavior   FlyBehavior
QuackBehavior QuackBehavior
}
func (d *Duck) fly() {
d.FlyBehavior.fly() // 委託給飛行行為
}
func (d *Duck) quack() {
d.QuackBehavior.quack() // 委託給呱呱叫行為
}
func main() {
yellowDuck := Duck{FlyNoWay{}, Squeak{}}
yellowDuck.fly()
yellowDuck.quack()
}
/* Output:
不會飛
吱吱
*/

總結

三種設計原則:

  1. 封裝變化
  2. 多用組合,少用繼承
  3. 針對接口編程,不針對實現編程

注意此處的「針對接口編程」,書中也有強調:

「針對接口編程」真正的意思是「針對超類型(supertype)編程」。這裡所謂的「接口」有多個含義,接口是一個「概念」,也是一種 Java 的 interface 構造。你可以在不涉及 Java interface 的情況下「針對接口編程」,關鍵就在多態。利用多態,程序可以針對超類型編程,執行時會根據實際狀況執行到真正的行為。

因此,你不用拘泥於 interface,你所用的語言就算沒有 interface 也能實現設計模式。


《HeadFirst設計模式》學習筆記|觀察者模式

相關文章

徹底弄清元素的offsetHeight、scrollHeight、clientHeight…

代碼模板|我的代碼沒有else

自己動手開發一個Android持續集成工具5

React源碼解析之commitRoot整體流程概覽