0%

《Head First 设计模式》

第一章 策略模式——灵活替换算法

引子:duck类设计

classDiagram
    Duck<|--MallardDuck
    Duck<|--RubberDuck
    class Duck {
        ...
        fly()
    }
    class MallardDuck {
        ...
        fly()
    }
    class RubberDuck {
        ...
        fly()cannot fly
    }

典型问题:想要为个别子类添加fly动作,直接添加在父类会导致对个别子类出现多余的修改。继承关系的写死导致软件难以应对变化,改动父类牵一发而动全身,容易违反开闭原则。

不论子类重写父类方法,还是提取父类接口由子类实现,子类之间重复的代码都不方便复用,修改代价大。

使用设计模式是希望软件在不断面对变化时的修改代价尽可能小。

原则:分离变化/不变的部分

我们希望系统中的某部分改变不会影响其他部分。所以需要将变化的部分与不变的部分隔离开。

在本例中,即将变化的部分抽取新的类,对新的类使用组合关系,以代替原来的继承重写的关系。Duck类中的大部分信息趋于稳定,但Fly动作一直在变,因此只将Fly的部分抽象出一个虚类,Duck仍然具体实现。

classDiagram
    Duck<|--MallardDuck
    Duck<|--RubberDuck
    Duck*--FlyBehavior
    FlyBehavior<|..FlyWithWings
    FlyBehavior<|..FlyNoWay
    class Duck {
        FlyBehavior flyBehavior
        SetFlyBehavior()
        PerformFly()
    }
    class MallardDuck {
        FlyBehavior flyBehavior
    }
    class RubberDuck {
        FlyBehavior flyBehavior
    }
    class FlyBehavior {
        <>
        fly()
    }
    class FlyWithWings {
        fly()
    }
    class FlyNoWay {
        fly() do nothing
    }

原则:针对接口编程,而不是针对实现

针对实现:动作在父类实现,子类进行重写;或者子类继承接口,动作在子类中实现。

针对接口:动作的实现不绑定于父类。利用多态针对超类型编程,例如针对抽象类或接口编程,执行时根据实际情况指定真正的行为。

针对接口编程使得原来的类、提取出的接口可以各自独立变化,作为两个生命周期去发展,在彼此需要时进行注入。

模式:策略模式

参考上述过程。

解释:从一批可以相互替换的策略中抽象出一个抽象策略,使用者包含该抽象策略,以便随时替换具体的策略。

比如,duck类的实例有很多不同的飞行行为,我们将飞行行为委托给FlyBehavior超类实现,提取出的这个行为类相当于一个算法族,即采取策略模式。duck使用该算法族的实例,可以灵活地指定采用哪个算法。

原则:多用组合,少用继承

duck使用该算法族的实例,即组合关系,可以对算法实现setter以便动态更换算法。相比于继承关系的静态实现(写死),组合关系更加灵活。

经验:使用设计模式

  • 模式让开发人员之间有共享词汇,方便沟通。
  • 使用设计模式可以建造出具有良好OO设计质量(具备可复用、可扩充、可维护性)的系统。
  • 不要过度使用设计模式,遵循简单设计原则。模式应着力于软件变化的问题。

第二章 观察者模式——解耦一对多数据依赖

模式:观察者模式

定义:观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

通俗解释:报社是主题对象,用户们是观察者对象。报社更新报纸后,会把报纸发放给订阅报纸的用户(推数据),或者通知用户去取报纸(拉数据);用户可以选择随时订阅或不订阅报社的报纸。

classDiagram
    class Subject {
        <>
        - objList : List
        NotifyObservers()
        RegisterObserver(Observer *o)
        RemoveObserver(Observer *o)
    }
    class Newspaper {
        - data 
        GetState(DataType data)
        SetState(DataType data)
    }
    class Observer {
        <>
        - subject
        Update(DataType data)
    }
    class Customer {
        Update(DataType data)
        Display(DataType data)
    }
    Subject<|..Newspaper
    Observer<|..Customer
    %% Subject..>Observer:obsv->Update(this)
    %% Customer..>Newspaper:this.state=sbj->GetState()

主题拥有一份数据/状态,观察者们作为主题的依赖者使用这些数据/状态,从而产生一对多的依赖关系,这种一对多通知的形式要比多个人控制一份数据的形式更好控制变化。主题也可以拥有多份实现,即和观察者是多对多的关系,具体注册哪一种,要看动态实现时注入了哪一个对象。

观察者模式对原则的体现:

  • 分离变化:Subject的状态在变化,Observer的订阅在变化,互不影响。
  • 针对接口编程:Subject和Observer都使用接口与彼此交互。
  • 多用组合,少用继承:Subject通过组合的方式加入Observer的订阅。

原则:松耦合设计

两个对象可以交互,但是不清楚彼此的细节。对象之间的互相依赖降到最低,从而建立有弹性的OO系统。

在观察者模式中,观察者抽象出一个接口,主题对象使用这个接口但不清楚具体有哪些观察者,实现了主题和观察者的松耦合,主题和观察者可以独立变化或复用。

相反,紧耦合是指模块之间关系太紧密,存在相互调用,一个模块修改会导致另一个模块的变化。

引子:气象站与布告板的设计

从气象站获取数据,设计WeatherData对象,更新目前状况、气象统计、天气预报的布告板。

首版设计方案:

1
2
3
4
5
6
7
8
9
10
11
public class WeatherData {
public void measurementsChanged() {
float temp = getTempature();
float humidity = getHumidity();
float pressure = getPressure();

currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
}
  • 没有分离变化。如果新增布告板还需要修改代码。
  • 没有针对接口编程。直接实现了有哪些布告板更新。
  • 无法动态增加或删除布告板。
  • 侵犯了WeatherData的封装。WeatherData不应该知道具体有哪些布告板。

采用观察者模式的设计方案:

classDiagram
    class Subject {
        <>
        observerList : List
        + RegisterObserver()
        + RemoveObserver()
        + NotifyObservers()
    }
    class WeatherData {
        - temperature : float
        - humidity : float
        - pressure : float
        MeasureChanged()call notify
    }
    class Observer {
        <>
        - mySubject
        Update()
    }
    class DisplayBehavior {
        <>
        Display()
    }
    class CurrentConditionBoard {
        - temperature : float
        - humidity : float
        - pressure : float
        Update(float temperature, ...)
        Display()
    }
    class SomeBoard
    Subject <|.. WeatherData
    Observer<|..CurrentConditionBoard
    DisplayBehavior<|..CurrentConditionBoard
    Observer<|..SomeBoard
    DisplayBehavior<|..SomeBoard

Subject聚合了Observer的对象实例。Observer的具体实现中依赖了Subject的数据。

  • Subject需要维护一个Observer队列,表示已注册的观察者;在Observer中保存Subject,方便取消注册。
  • WeatherData的构造器可以直接传入observerList;CurrentConditionBoard的构造器可以直接传入要订阅的subject并执行订阅。
  • 抽取DisplayBehavior相当于对具体布告板的display功能采用了策略模式。如果直接由某具体布告板实现父类的display方法,不方便display功能的复用。
    这种方式是Subject给Observer通知数据(push),也可以实现Observer通过Subject的getter方法去自行获取数据(pull)。

在拉数据的观察者模式中,Subject不需要再维护Observer队列,而要实现对数据的getter方法;Observer仍然维护自己所订阅的mySubject,通过getter自行获取自己需要的数据。订阅和取消订阅的方法变成Observer类中对mySubject的setter。

对比:观察者模式vs发布/订阅模式

观察者模式主要实现了主题与观察者之间的松耦合关系,主题和观察者可以互相察觉到彼此。

发布/订阅模式中,发布者和订阅者相互不可见,是彻底解耦的,借由中间的第三方平台沟通数据,属于异步通信。发布者将数据更新到平台方,平台方推送数据给订阅者,或者订阅者从平台方主动拉数据。

发布/订阅模式更加灵活,但也意味着代码不易维护,数据流容易混乱。而且,第三方平台需要维护一个事件队列,事件越多,内存消耗越大。

观察者模式更加方便维护,数据流简单,主题变化就会通知观察者。但主题和观察者依然存在耦合。

第三章 装饰者模式——统一类型,增加职能

引子:计算咖啡价格

不同的饮品可以包含不同种类、分量的材料,我们要按照加入的材料计算饮品价格。

classDiagram
    class Beverage {
        - description
        - milk
        - soy
        - mocha
        getDescription()
        cost()
        hasMilk()
        setMilk()
        hasSoy()
        setSoy()
        hasMocha()
        setMocha()
        ...
    }
    class Decaf {
        cost()
    }
    Beverage<|--Decaf
    Beverage<|--something
  • 直接实现了材料has/set的代码。价格变更、材料新增都会引起代码修改。
  • 子类饮品继承到了过多不需要继承的材料。
  • 无法动态指定某饮品的材料增减。

咖啡的价钱计算方法是确定的,有某种材料就加上其价格。但是不同饮品的材料组成、使用的材料份数是多变的。因此接下来应该考虑如何动态地组合对象。

原则:类应该对扩展开放,对修改关闭(开闭原则)

实现的类要容易扩展,要不修改代码就可以动态地搭配新的行为。

使用开闭原则时会引入新的抽象层次,增加代码的复杂度。因此要有取舍,过度使用开闭原则会导致代码复杂、难以维护。

装饰者模式遵循开闭原则。

模式:装饰者模式

定义:动态地将责任附加到对象上。涉及到需要扩展功能的场景,装饰者模式可以替代继承,提供更有弹性的方案。

通俗解释:饮品表示被装饰类,各种材料表示装饰类,装饰类扩展被装饰类。一杯饮品与牛奶、豆浆等装饰对象拥有相同的计算方法, 当我们计算饮品价格时,饮料会将计算行为委派给装饰对象。只要所有部件的都遵循相同的接口, 我们就可以动态指定任意自定义的装饰者来装饰对象。

classDiagram
    class Mocha {
        GetDescription()
        Cost()
    }
    class Latte {
        GetDescription()
        Cost()
    }
    class Decorated {
        GetDescription()
        Cost()
    }
    class Decorator {
        GetDescription():virtual
    }
    class Milk {
        GetDescription()
        Cost()
    }
    class Expresso {
        GetDescription()
        Cost()
    }
    Decorated<|--Mocha
    Decorated<|--Latte
    Decorated<|--Decorator
    Decorated--*Decorator:部件包裹一个被装饰者
    Decorator<|--Milk
    Decorator<|--Expresso

Mocha和Latte都是基本饮品,可以额外添加Expresso和Milk。

是否违反多用组合、少用继承原则?答:这里的继承不是为了继承行为,而是为了继承其类型。

装饰者模式让饮品制作的过程挪动到了动态流程当中。我们可以随意在饮品中添加材料,看看C++测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 测试1:只是一杯拿铁
TEST(DecoratorPatternTest, just_latte)
{
Decorated *cup = new Latte();
string expect = "Latte $1.000000";
string output = cup->GetDescription() + " $" + to_string(cup->Cost());
EXPECT_EQ(expect, output); // true
}

// 测试2:制作一杯加双份浓缩和双份奶的拿铁
TEST(DecoratorPatternTest, latte_with_2_milk_2_expresso)
{
Decorated *cup = new Latte(); // 这一杯是Latte
cup = new Milk(cup); // Milk将Latte包裹住
cup = new Milk(cup); // Milk将包裹Latte的Milk包裹住
cup = new Expresso(cup);
cup = new Expresso(cup);

string expect = "Latte, Milk, Milk, Expresso, Expresso $2.800000";
string output = cup->GetDescription() + " $" + to_string(cup->Cost());
EXPECT_EQ(expect, output); // true
}
/*
(((((Latte)Milk)Milk)Expressp)Expresso)
*/

好处:

  • 对饮品可以动态组合不同材料,为设计注入弹性。
  • 实现价格setter后,材料的价格变动不会引起代码修改。

特性:

  • 装饰者反映被装饰者的类型。
  • 装饰者之间不知道彼此存在,对于用户是透明的。
  • 装饰者可在委托给被装饰者的行为之前或之后,添加自己的行为。但被装饰者的具体对象不可以添加特别的方法。
  • 装饰者可以无数层包含被装饰者,也可以直接取代掉被装饰者。从这一点可以看出,被装饰者和装饰者本质上是一个东西,提取出装饰者主要是为了做一个逻辑上的隔离和解耦,使得装饰者可以独立管理并总是在嵌套的外层。

短板:

  • 有特殊设计的具体类采用装饰时会出问题。装饰者模式把接口设计限定死了。
  • 设计中有大量小类,容易出错,代码难以理解。

延伸:装饰者模式中基类的虚实

如果被装饰者基类是虚的:

  • 必须要实现每个具体子类的方法,达不到代码复用的目的。当子类没有实现方法时,程序会出错。
  • 不支持用装饰者取代被装饰者。

如果被装饰者基类是实的:

  • 可以把子类的重复代码提取上来,子类如非特殊可以不用实现自己的方法。

第四章 工厂模式——委托创建行为

引子:制作披萨&使用new的问题

我们尝试实现一个制作各种披萨的流水线。

如果我们new不同的披萨实例并进行的操作,难免变成针对实现编程。当披萨种类发生变化时,尽管披萨包装、烘焙操作都是统一的,还是要麻烦地修改代码,没有做到对修改关闭。

new的问题:代码中出现对具体类的太多实例化,会增加对具体类的依赖,导致不容易应对变化。

可以看出,变化的部分是不同披萨的创建,我们需要将变化的部分分离出来,需要用一个模式来应对创建对象的变化。

模式:工厂模式

简单工厂

简单工厂是一种编程习惯,不是一种设计模式。

通俗解释:简单工厂类只负责创建对象,用于应对各种创建对象过程的变化。简单工厂可以服务于多个客户对象。客户要存有一个简单工厂成员,可以将工厂作为构造参数传入。好处是:

  • 隔离变化:当创建的部分发生变化,只需要修改工厂类。
  • 对修改关闭:客户不需要new具体的实例了,只需要调用工厂的生产方法获取实例。

一个简单工厂的例子:

classDiagram
    class Pizza {
        bake()
        cut()
    }
    class PizzaFactory {
        CreatePizza(type)Pizza
    }
    class PizzaStore {
        PizzaStore(PizzaFactory factory)
        PizzaFactory factory;
    }
    class SomeCompany {
        SomeCompany(PizzaFactory factory)
        PizzaFactory factory;
    }
    Pizza<|--CheesePizza
    Pizza<|--ClamPizza
    Pizza<--PizzaFactory
    PizzaFactory*--PizzaStore
    PizzaFactory*--SomeCompany

静态工厂

用静态方法定义工厂,就是在简单工厂的基础上,给创建方法声明一个static。

这样做的好处是,相当于给全局提供一个单例工厂,不需要到处重复实例化工厂;坏处是,不能通过继承来重写创建方法的行为。

工厂模式

为了实现一套加盟店管理框架,我们抽象出一个超类客户(Pizza总店)。总店PizzaStore在OrderPizza方法实现整套要复用的制作流程,但总店不负责具体的创建,它的CreatePizza是虚的protected型,意味着下面的分店一定要实现自己的创建方法,分店的其他处理流程就继承自总店。

工厂模式定义:工厂模式定义了一个创建对象的接口,由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

通俗解释:PizzaStore是创建者,Pizza是产品。PizzaStore总店定义一套抽象工厂方法,制造抽象的产品Pizza;子类披萨分店作为具体创建者,生产具体的产品PizzaA、PizzaB。工厂方法的关键就是,具体创建者封装自家产品的生产资料,选择了哪种具体创建者,就根据哪家生产资料进行生产。

classDiagram
    class Pizza {
        <>
        Bake()
        Cut()
        Box()
    }
    class PizzaStore {
        PizzaStore(PizzaFactory factory)
        PizzaFactory factory;
        OrderPizza()
        -CreatePizza()virtual
    }
    class NYStylePizzaStore {
        CreatePizza()
    }
    class ChicagoPizzaStore {
        CreatePizza()
    }
    Pizza<--PizzaStore
    Pizza<|--PizzaA
    Pizza<|--PizzaB
    PizzaStore*--NYStylePizzaStore
    PizzaStore*--ChicagoPizzaStore

工厂方法:超类的代码(例如OrderPizza)和子类对象创建代码彻底解耦。

1
virtual Product FactoryMethod(productType)

总店OrderPizza调用CreatePizza来创建具体Pizza,具体创建哪种Pizza要看具体哪家披萨店,创建后再调用Pizza的cut、box等操作。即将创建隔离出来,并将处理流程实现复用。

好处:解耦了实现和使用。

1
2
3
4
5
Pizza PizzaStore::OrderPizza() {    // 披萨来自不同的分店,总店不可见
pizza = CreatePizza(); // 总店统一的制作流水线,可见
pizza.Bake();
pizza.Cut();
pizza.Box();}

原则:依赖倒置原则

要依赖抽象,不要依赖具体类。不能让高层组件依赖底层组件;不论高低,组件都应该依赖于抽象。

通俗解释:当PizzaStore(高层)直接根据要求生产具体的披萨(低层)时,如果披萨有变动,必然要修改PizzaStore,这时PizzaStore就依赖了Pizza的具体类,即高层依赖低层。如果采用工厂模式,PizzaStore和具体的披萨都依赖抽象的Pizza,这样变化更好控制。

graph LR
    a[高:抽象的PizzaStore]
    b[抽象的Pizza]
    c[低:具体的Pizza]
    a-->|因为PizzaStore要生产抽象Pizza|b
    c-->|因为具体类实现了抽象类|b

“倒置”体现在哪里?不让高层组件依赖底层组件,倒置了流程依赖具体类的设计,和一般的OO设计思考方式相反。

启发:不要依赖具体

  • 变量不可以持有具体类的引用。使用工厂替代new。
  • 不要让类派生自具体类。这样会导致高层依赖低层,应该从高层开始设计,不要从具体类入手。
  • 不要覆盖基类中已实现的方法。基类中已实现的方法应该由子类共享,否则不适合继承。

复杂性是无法根除的,只能尽量分解和加以管理。

模式:抽象工厂模式

为披萨店引入新的变化:不同的披萨店制作的同一款披萨,虽然制作方案是相同的,但是因为地域不同,所使用的配料是不同的,所以我们需要实现一个按照地域划分的原料工厂。

抽象工厂模式定义:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

classDiagram
    class Pizza {
        <>
        Bake()
        Cut()
        Box()
    }
    class PizzaStore {
        PizzaStore(PizzaFactory factory)
        PizzaFactory factory;
        OrderPizza()
        -CreatePizza()virtual
    }
    class PizzaStoreA {
        CreatePizza(FactoryA)
    }
    class PizzaStoreB {
        CreatePizza(FactoryB)
    }
    class Factory {

    }
    class FactoryA {
        AddDough(DoughA)
        AddSource(SauceA)
    }
    class FactoryB {
        AddDough(DoughB)
        AddSource(SauceB)
    }
    Pizza<--PizzaStore
    Pizza<|--PizzaA
    Pizza<|--PizzaB
    PizzaStore*--PizzaStoreA
    PizzaStore*--PizzaStoreB
    Factory<|--FactoryA
    Factory<|--FactoryB
    PizzaStore*--Factory:抽象工厂注入

通俗解释:将具体披萨店中根据地域条件的创建委托给了抽象Factory,抽象Factory再将具体的创建下发给对应地域的工厂,具体工厂实现一系列产品家族,相当于组合了两层工厂方法来隔离变化。通过组合实现各种具体工厂的注入就是抽象工厂的精髓。

对于Factory来说,PizzaStore是他在抽象工厂模式中的客户。

对于FactoryA来说,Factory是他的工厂方法中的客户。对于PizzaStore来说,PizzaStoreA是他的工厂方法中的客户。

对比:简单工厂、工厂方法、抽象工厂

从创建动作委托的角度

简单工厂:客户将创建委托给一个具体的工厂类,获取具体产品,实现变化隔离。

工厂方法:客户将创建下移给低层的具体类,客户直接生产抽象的产品,由子类作为工厂实现具体产品,实现依赖倒置。

抽象工厂:客户将创建委托给一个抽象的工厂类,由具体的工厂子类实现产品家族,具体客户与具体工厂一一对应。

从类图特点的角度

classDiagram
    class 简单工厂客户 {
        创建抽象产品
    }
    简单工厂客户<|--客户1
    简单工厂客户<|--客户2
    class 简单工厂 {
        创建具体产品
    }
    简单工厂客户*--简单工厂:为客户隔离变化

    class 工厂方法客户 {
        <>
        创建抽象产品
    }
    class 工厂1 {
        创建产品1
    }
    class 工厂2 {
        创建产品2
    }
    工厂方法客户<|--工厂1:创建下移,重在继承
    工厂方法客户<|--工厂2

classDiagram
    class 抽象工厂客户 {
        <>
    }
    class 客户a {
        委托给工厂a
    }
    class 客户b {
        委托给工厂b
    }
    class 抽象工厂 {
        <>
    }
    class 工厂a {
        创建产品a
    }
    class 工厂b {
        创建产品b
    }
    抽象工厂客户<|--客户a
    抽象工厂客户<|--客户b
    抽象工厂客户*--抽象工厂:创建注入,重在组合
    抽象工厂<|--工厂a
    抽象工厂<|--工厂b

从解决问题的角度

简单工厂:只是将变化隔离出来,让客户对变化不可见。

工厂方法:相当于简单工厂中工厂种类太多时的管理方式。从依赖的高层工厂决定具体创建,变为依赖低层子类决定具体创建。

抽象工厂:解决了工厂方法中的子类创建太复杂的问题,将其中共同的创建条件提取出一个抽象工厂类,再由将创建委托给子类,将更多的选择留给他们外层的客户。

第五章 单例模式——全局唯一实例

也叫单件模式。

许多场景中,一个类只允许有一个实例,比如数据库链接、线程池等共享资源。

定义:确保一个类只有一个实例,并提供一个全局访问点。

通俗解释:单件类的构造器是私有的,每次使用的时候需要通过单件类的getInstance申请获取这唯一的实例,第一次申请会先创建再返回实例。单件模式常常用在对资源敏感的场景中。

classDiagram
    class Singleton {
        static instance
        static getInstance()
    }
1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton() {}

public static Singleton GetInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

相比全局变量,单例模式的优点在于:

  • 第一次需要用到的时候才创建实例,延迟实例化,不会造成浪费。
  • 能够确定只有一个实例,不会像全局变量那样,对命名空间产生污染。

第六章 命令模式——隔离调用者和执行者

引子:餐馆运营中的命令模式

客户(Client)点餐:客户创建命令对象,产生命令。

订单(Command)上写着客户订购的餐点:存储命令。

服务员(Invoker)拿着订单(SetCommand),通知厨师准备餐点(OrderUp):取命令,调用命令执行入口,命令触发命令接收者的动作。

厨师(Receiver)根据餐点指示进行烹调(Execute):命令接收者收到触发,将命令转化为具体动作,达到目的。

如果有的顾客点中餐、有的顾客点西餐,那么服务员拿着订单找厨师的时候,中餐订单触发中餐厨师去烹饪,西餐订单触发西餐厨师去烹饪。

在这个过程中,服务员触发菜肴的烹饪,但是他不需要知道这些菜怎么做、交给哪个厨师去做,他只负责通知“这个菜要做了”。相当于实现了服务员和厨师的解耦,也就是触发者和执行者的解耦。

模式:命令模式

定义:将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

classDiagram
    class Invoker {
        Command slot
        SetCommand(Command cmd)
        TriggerButton()
    }
    class Command {
        <>
        Execute()
        Undo()
    }
    class ConcreteCommand1 {
        Receiver receiver
        ConcreteCommand1(receiver1)
        Execute()
        Undo()
    }
    class ConcreteCommand2 {
        Receiver receiver
        ConcreteCommand2(receiver2)
        Execute()
        Undo()
    }
    class Receiver1 {
        Act()
    }
    class Receiver2 {
        Act()
    }

    Command--*Invoker
    Command <|.. ConcreteCommand1
    Command <|.. ConcreteCommand2
    Receiver1--*ConcreteCommand1
    Receiver2--*ConcreteCommand2

通俗解释:隔离调用者和执行者。

  • Client负责创建命令实例,产生命令的同时要关联命令的Receiver,即命令执行者。
  • Invoker不知道自己执行的具体是什么、接收对象是谁,但他保存着命令实例的队列/集合,通过SetCommand决定要执行哪个命令,他负责触发/撤销这些命令的执行。
  • Command命令实例对外暴露Execute或Undo的动作,由Invoker调用者调用。命令应当知道自己处理的接收对象是谁,因此它要保存着接收对象的实例。命令会在自己被执行时触发接收对象的动作,但他也不知道接收对象会具体做什么。为了实现命令Undo功能,ConcreteCommand要在触发Execute操作前存储当前状态,下次调用Undo时可以恢复到之前的状态。
  • Receiver对象定义自己的行为,这些行为可以被命令的Execute触发。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// TEST
Invoker controller = new Invoker();
Light light = new Light(); // Receiver 1
Command lightOnCommand = new Command(light);
Command lightOffCommand = new Command(light);
Door door = new Door(); // Receiver 2
Command doorOnCommand = new Command(door);
Command doorOffCommand = new Command(door);
controller.SetCommand(lightOnCommand); // 开灯
controller.TriggerButton(); // controller.TriggerButton()-->lightOnCommand.Execute()->light.On()
controller.SetCommand(lightOffCommand); // 关灯
controller.TriggerButton();
controller.SetCommand(doorOnCommand); // 开门
controller.TriggerButton();
controller.SetCommand(doorOffCommand); // 关门
controller.TriggerButton();

对比:命令模式vs策略模式

命令类只在乎对命令的操作。命令模式可以处理完全不同的请求,但不在乎命令触发了什么,比如命令是由谁调用的、是给谁处理的,统统不关心。它的重点在于隔离了调用者和执行者,让调用者可以通过命令触发具体执行者做动作。

策略模式概括一系列行为,这些行为在做什么是很重要的。它的重点在于将一系列行为抽象出来,使用者包含一个抽象策略,以便做到随时更改替换。

第七章 适配器模式——适配不同的接口

模式:适配器模式

定义:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可已合作无间。

通俗解释:客户只希望使用一个Target类,如果之后还想要用到Duck类,则需要实现一个Duck适配器,将Duck接口转换成Target接口,以符合客户的期望。

classDiagram
    class Duck {
    代码不变
    }
    class Target {
        <>
        代码不变
    }
    class ConcreteTarget {
        代码不变
    }
    class DuckAdapter {
        TargetAdapter(Duck duck)
        Target把duck接口包装起来()
    }

    Duck..*DuckAdapter:组合Duck对象
    Target<|..DuckAdapter
    Target<|..ConcreteTarget

好处:

  • Target类和Duck类都不需要修改代码,只需要新增一个DuckAdapter。通过对象的组合,将需要被适配的Duck类的接口包装起来。
  • 被适配者的子类也可以搭配适配器使用。

对比:对象适配器vs类适配器

对象适配器:通过组合关系封装Adaptee的接口。

优点:灵活有弹性,可以给每个对象都实现一个适配器。对象的子类也可以使用适配器。

缺点:单向适配,需要区分被适配者是谁。

classDiagram
    class Adaptee {

    }
    class Adapter {
        Adapter(adaptee)
        封装adaptee的接口()
    }
    class Target {
        客户使用
    }
    Adaptee..*Adapter:组合
    Target<|--Adapter

类适配器:通过继承关系封装Adaptee的接口。

优点:可以实现双向适配;不需要给每一个对象都实现一个适配器,代码少。

缺点:涉及多重继承;继承关系不够灵活。

classDiagram
    class Adaptee {

    }
    class Adapter {
        Adapter(adaptee)
        封装adaptee的接口()
    }
    class Target {
        客户使用
    }
    Adaptee<|--Adapter:继承
    Target<|--Adapter

对比:装饰者模式vs适配器模式

装饰者模式:多层的装饰者和被装饰者的接口相同,装饰者利用一致的接口给被装饰者增加职能。

适配器模式:客户使用目标类和被适配者接口不同,需要适配者进行接口转换。

第八章 外观模式——对外提供简化接口

模式:外观模式

定义:提供一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

通俗解释:外观模式将一个系统中的接口封装起来,对外提供简化的接口。例如,一体机的开关可以一键打开主机、屏幕、音响等等。

flowchart TB
    Client-->Facade
    subgraph 系统
        a---b---c
        b---d---c---a
    end
    Facade---系统

优点:这样更方便客户操作,将客户从系统中解耦出来。一个系统也可以具有多个外观。

对比:适配器模式vs外观模式

对比适配器模式,外观模式的重点在于简化接口,并不在乎简化了多少个接口,他们的意图是不同的。

原则:最少知识原则(迪米特法则)

最少知识原则,也就是迪米特法则(Law of Demeter)。

要减少对象之间的交互,只留下最少的、必要的交互。这样做的目的是,避免太多类耦合在一起的情况,免得修改的影响太大,牵一发而动全身,难以维护。

实践方法——在一个对象的方法内,我们只应该调用以下范围的方法:

  1. 该对象本身
  2. 以方法参数形式传入的对象
  3. 此方法所创建或实例化的对象
  4. 对象的任何组件(HAS-A关系)

要求别人满足我们的请求,而不是我按照自己的需求一步一步申请别人给我做事。要求别人满足我们的要求的时候,别人做具体处理,我们不需要接触中间的各种类,只获取自己最想要的东西。这样就做到了最少的对外依赖。比如,外观模式中客户最终只接触外观模式,不需要知道更多系统细节。

缺点:按照上面的做法,虽然对外的依赖会减少,但是会多出一些用来处理沟通的“包装”类,导致软件复杂度提升、开发时间增加、降低运行性能。

因此使用时还是要考虑全局的因素,寻找一个抽象和速度、空间和时间之间的平衡。

第九章 模板方法模式——提取公有算法骨架

模式:模板方法模式

定义:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算发中的某些步骤。

classDiagram
    class Procedure {
        ProcedureRecipt()
        BoilWater()
        Brew()
        PourInCup()
        AddCondiments()
    }
    class MakeCoffee {
        Brew()
        AddCondiments()
    }
    class MakeTea {
        Brew()
        AddCondiments()
    }
    Procedure<|--MakeCoffee
    Procedure<|--MakeTea

通俗解释:煮咖啡和泡茶的两个对象中存在相似的算法步骤,但又有具体处理、用料上的细微差异,可以考虑将他们公共的逻辑提取出一个超类的方法,形成一个模板。其中,子类可以直接共用的方法(比如煮沸水方法)写成具体方法,子类之间存在差异的方法(比如萃取和用料)实现为抽象方法,具体的差异交由对象自己实现。

提取出的公有流程就是模板方法(对应图中的ProcedureRecipt),它定义了一个算法的步骤,允许子类为一个或多个算法提供实现。这样做减少了对象之间的重复代码,如果流程需要修改,只需要修改这个模板方法,不需要打开每个对象。

在模板方法中添加钩子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class MakeBeverage {
void ProcedureRecipt() {
BoilWater();
Brew();
if (Hook()) {
AddCondiments();
}
}
void BoilWater(); // 子类共用
abstract void Brew(); // 子类实现差异
abstract void AddCondiments();
boolean void Hook() { // 钩子函数,子类可覆盖实现。
return true;
}
}

钩子的作用

  1. 钩子函数容纳了子类使用模板方法的可能变数。如果这个步骤是必须执行的,那么就使用抽象方法,如果这个步骤是可选的,那么就要使用钩子。
  2. 钩子让子类有机会对模板方法中某些即将发生的、或刚刚发生的事情作出反应。比如在模板方法中一些函数内某些动作前后增加Hook调用,子类实现Hook的功能。

关于钩子的思考:模板方法中的钩子和父类中的具体方法本质是相同的,只是为了强调钩子方法的用途。比如提供可选、实现回调等。

模板方法中的步骤划分粒度很重要。如果划分太细,子类实现工作量很大;如果划分太大,会没有弹性。需要折中考虑。

原则:好莱坞原则

高层组件对底层组件说:别调用我,我会调用你。即,保证组件之间依赖的单向性,低层不要依赖高层。

这个原则的重点不在于禁止低层依赖高层,而是避免形成高低层之间的明显环状依赖。

在模板方法中,好莱坞原则体现在:高层组件实现模板方法,只有在需要子类具体的方法时,才会使用子类的覆盖(调用子类)。

对比:好莱坞原则vs依赖倒置原则

共同点:都是为了解决依赖,都在避免高层依赖低层。

依赖倒置原则强调避免使用具体类,多使用抽象,依赖抽象而不是具体。相比好莱坞原则,更加注重如何避免依赖。

好莱坞原则强调创建框架或组件上的思想,可以让底层挂钩进计算中,又不会让高层依赖低层。在解除环形依赖的情况下,更加注重架构的弹性,让低层结构的功能更加灵活。

对比:模板方法模式vs策略模式

共同点:都是对方法的抽象,都是推迟到子类实现具体。

策略模式:通过组合的形式使用一组策略,策略模板对算法具体步骤没有控制,子类实现的算法实体都是完整的。

模板方法模式:模板方法提供一个固定的算法框架,其中有些步骤是实现好的,有些是需要填补实现的。

如果模板方法中的步骤全部都是抽象方法,完全交由子类实现一整个算法,那么这个模板方法就和抽象的策略族一样了。

所以,策略模式相当于给了子类更大的实现自由度,子类要实现的代码更多,比较适合实现不拘泥于步骤的算法;而模板方法仍将算法的步骤掌控起来,子类实现的代码较少,适合实现重复步骤较多的算法。

第十章 迭代器模式——封装遍历元素

当我们对多个不同的数据结构进行遍历处理时,我们很容易针对不同数据结构写不同的循环。但是这样的设计有两个问题:

  1. 没有做到对修改关闭——如果再新增一种数据结构,我们还要加一个循环;如果处理逻辑变更,我们要修改多个循环。
  2. 遍历处理看到了数据的内部细节。

迭代器模式就适用于解决这样的问题。

模式:迭代器模式

不论要访问哪一种数据结构,我们通过一个Iterator类去封装遍历元素,使我们拥有一个统一的遍历框架。

1
2
3
4
Iterator iterator = menu.createIterator();
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem)iterator.next();
}

定义:迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

迭代器模式把在元素之间游走的责任交给迭代器,而不是聚合对象。让聚合对象更专注在它需要关注的事情上,做到责任各得其所。

而且迭代器可以实现用于遍历无次序的集合。

classDiagram
    class Iterator{
        <>
        hasNext()
        next()
    }
    class menu1Iterator{
        hasNext()
        next()
    }
    class menu2Iterator{
        hasNext()
        next()
    }
    Iterator<..menu1Iterator
    Iterator<..menu2Iterator
    menu1Iterator*..menu1
    menu2Iterator*..menu2

迭代器接口定义:

1
2
3
4
5
public interface Iterator {
boolean hasNext();
Object next();
void remove(); /* 可选 */
}

修改后的好处:

  • 遍历者通过迭代器接口调用对象元素,不需要了解对象的实现细节。
  • 修改代码时,只需要实现对应的迭代器,就可以使用一个遍历多态的处理任何集合。

内部迭代器和外部迭代器:

  • 上面实现的是外部迭代器——客户自己调用next方法取得下一个元素。
  • 内部迭代器是指,游走的动作由迭代器自己进行,客户无法控制遍历的过程,只需要将操作传递给迭代器。
  • 内部迭代器相比外部迭代器更没有弹性,但是可能更易用。

这本书多次告诉大家:易用的东西总是不灵活。

原则:单一职责

一个类应该只有一个引起变化的原因。

类的每一个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。单一职责原则主要是在开发阶段就限制修改行为的扩散。

内聚:用来度量一个类或模块紧密地达到单一目的或责任。当一个模块或一个类被设计成只支持一组相关的功能时,就是高内聚,反之就是低内聚。

内聚的概念相比单一职责原则,是一个更普遍的概念。

第十一章 组合模式——树结构

定义:允许将对象们组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

使用组合结构,可以将相同的操作应用在组合和个别对象上,忽略对象组合和个体的差异。用来解决一些具有嵌套形式的相同结构的问题。

classDiagram
    class Component {
        add(Component)
        remove(Component)
        getChild(Component)
        operation()
    }
    class Leaf {
        operation()
    }
    class Composite {
        add(Component)
        remove(Component)
        getChild(Component)
        operation()
    }
    Client..>Component
    Component<|--Leaf
    Component<|--Composite

组合包含组件。组件包括组合、叶节点元素。

我们实现的时候需要维护一个递归结构,最末端是叶节点,而且组合内所有的对象都必须实现相同的接口。

第十二章 状态模式——分解复杂条件

状态机

一个普通状态机的实现:

  1. 定义多个状态。
  2. 维护一个“当前状态”的变量。
  3. 实现状态机逻辑:根据输入,执行动作,切换状态。

如果状态比较多,那么状态机的逻辑就会有很多if-else,每个动作都要遍历判断所有的状态,修改起来很麻烦,也不好扩展。

这种将处理逻辑集中在一个类的各个方法中的实现方式更像面向过程开发。并没有发挥出面向对象的优势。

下来我们要运用设计模式对这种情况重构:

  • 封装变化:我们将状态和动作交叉排列拆开。我们为每一个状态实现一个状态类,在状态类中实现这个状态下的各个动作。
  • 多用组合,少用继承:我们通过组合关系,把状态要执行的动作委托给当前具体的状态对象。

这样就实现了状态模式。

模式:状态模式

定义:当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

通俗解释:书上这个定义很难看懂,但参考其实现就比较明晰了。当状态发生切换时,将新的状态注入。接下来我们执行相同的方法时,实际上执行的动作是不同的——即“这个对象看起来像是改变了其类”。

比如,在状态1执行动作2后要把状态切换到状态3,在状态2执行动作2后要把状态切换到状态5……相当于执行动作2和切换状态的动作委托给了状态对象1和2去执行,执行结果是不同的。

状态模式主要解决:当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。

classDiagram
    class State {
        <>
        Machine machine;
        InitMachine(Machine machine)
        Do1()
        Do2()
    }
    class State1 {

    }
    class State2 {

    }
    class State3 {

    }
    class Machine {
        State state1;
        State state2;
        State state3;
        State currentState
        SetState(State state)
        Do1():currentState.Do1()
        Do2():currentState.Do2()
    }
    State<|..State1
    State<|..State2
    State<|..State3
    State*..Machine:注入状态机

实现一个抽象的状态类用于封装代表Machine当前状态的行为。

当Machine执行当前状态的行为时,实际上执行的是某个具体状态对象的行为。

好处:

  • 将每个状态的行为局部化到自己的类中,让每个状态对修改关闭。让Machine对扩展开放(随时加入新的类)。
  • 方便维护,状态和行为的排列组合if-else。
  • 良好的隔离:业务逻辑执行状态机,但是无法接触到具体状态。

坏处:

  • 获得弹性变化的代价是增加了类的数目。如果状态的转换比较固定的话,还是放在主类中比较划算。

共享状态:多个Machine实例可以共享状态对象,但需要将状态对象中对于具体某Machine的内部信息去掉,做到可以通用,并指定到静态的实例变量中。

对比:状态模式vs策略模式

策略模式:是继承的一种弹性替代方案。动态地改变动作的执行方案。

状态模式:是许多条件判断的替代(分解)方案。利用不同的状态对象,改变状态进而改变动作。

两个方案都是通过组合关系注入一系列具体方案,但是意图不同。

第十三章 代理模式——包装对象,控制访问

变体1:远程代理

远程代理的角色,就像是远程对象的本地代表。

graph LR
    subgraph 本地
        a[目标对象监视器]
        b((本地代理))
    end
    subgraph 远程
        c[目标对象]
    end
    a-->b
    b-->|网络通信|c
    client[客户]
    client-->|调用|a

远程对象在不通的地址空间运行;

本地代表可以由本地方法调用,其行为会转发到远程对象中。

远程代理只是一般代理的一种实现。

模式:代理模式

定义:为另一个对象提供一个替身或占位符以控制对这个对象的访问。

使用代理模式创建代表对象,让代表对象控制某对象的访问。

classDiagram
    class Subject {
        <>
        Request()
    }
    class RealSubject {
        Request()
    }
    class Proxy {
        Request()
    }
    Subject<|..Proxy
    Subject<|..RealSubject
    Proxy-->RealSubject:创建RealSubject对象,转发请求

远程代理是一般代理模式的一种实现,其实变体有很多。

  • 虚拟代理:创建开销大的对象的代表。当真正需要的时候,代理才创建一个真实的对象。其他时间由虚拟对象来扮演真实对象的替身。
  • 动态代理:运行时才将它的代理类创建出来。
  • 保护代理:可以根据客户的角色来决定是否允许客户访问特定的方法看,所以保护代理可能只提供给客户部分接口。(和适配器很像,但适配器的重点是改变对外的接口)

还有其他类型的代理:智能引用代理、缓存代理、防火墙代理等。代理模式相比其他包装类模式,重点更偏向于对内部功能的过滤、改造,使得对外提供的接口符合特定场景的要求。

变体2:保护代理

classDiagram
    class Subject{
        <>
    }
    class RealSubject {
        Request()
    }
    class Proxy {
        Request()
    }
    Subject<|..RealSubject
    Subject<|..Proxy

    class InvocationHandler {
        <>
    }
    class InvocationHandlerA {
        Invoke()
    }
    class InvocationHandlerB {
        Invoke()
    }
    Proxy-->InvocationHandlerA:转发
    InvocationHandler<|..InvocationHandlerA
    InvocationHandler<|..InvocationHandlerB

proxy.Request()会将当前的代理对象、方法、参数传入对应的InvocationHandler——调用处理器——来决定RealSubject具体做什么动作。

通过不同的InvocationHandler帮助代理Proxy分配方法,实现不同的角色拥有不同的权限。

对比:装饰者、外观、代理、适配器

  • 装饰者:包装另一个对象,并提供额外行为。
  • 外观:包装许多对象以简化接口。
  • 适配器:包装另一个对象,并提供不同的接口
  • 代理:包装另一个对象并控制对它的访问。

第十四章 复合模式——模式的模式

模式:复合模式

复合模式在一个解决方案中结合两个或多个模式,以解决一般或重复发生的问题。

复合模式就是针对具体场景让多个模式合作的模式。相比其他模式更加贴近抽象业务。

警惕!不要对所有的问题都套用设计模式,有时候只需要运用好OO设计原则就可以解决问题,就不要套用设计模式。在采用设计模式的时候要平衡利弊,观察当下真正的需求和变化在哪里。遵循简单设计。

经典复合模式:MVC模式

Model-View-Controller(模型-视图-控制器)复合模式。

flowchart LR
    v((viewer 视图))
    c((controller 控制器))
    m((model 模型))
    v-->|1 用户输入|c
    c-->|2 改变状态|m
    c-->|3 改变显示|v
    m-->|4 刷新显示|v
    v-->|5 获取业务数据|m

个人理解:

  • 视图是用户交互模型数据的窗口。
  • 模型描述的是业务数据。
  • 控制器的作用是用户语言和业务语言之间的转换器。

MVC模式就是多种设计模式的复合。

  • 策略模式:控制器为视图对象提供了策略集,一个视图可以对应多个控制器。
  • 组合模式:视图包括多个窗口和按钮,相当于组合与叶节点。
  • 观察者模式:模型作为被观察对象,视图是观察者,当模型状态改变时通知视图更新。
  • 适配器模式:可以用来将新的模型适配给已有的视图和控制器。

程序员要为架构抗争!

随着软件周期的推移,软件修改要付出的代价会越来越大。软件架构的目标是希望以最少的人力满足构建和维护该系统的需求,延缓软件腐化的趋势。

关于对架构的误解澄清:高层的架构并不能脱离细节实现的设计。高层架构和低层设计不分你我。

Read more »

第一章 重构概述

重构的定义

重构的定义:在不改变软件可观察行为的前提下,调整其结构,提高其可理解性,降低其修改成本。

重构的关键在于运用大量微小保持软件行为的步骤,一步一步完成大规模的修改。即使重构没有完成,也可以随时停下来,保证代码的运作。

Read more »

  1. 观察哪个变量可以使用大数组描述。
  2. 考虑使用排序对数据进行预处理。
  3. 多条件排序的问题考虑使用qsort。
  4. 考虑使用位运算来原地处理数据、节省空间。
  5. 考虑使用位移运算替代除法、取余、乘法,提升性能。
  6. 永远考虑是否有负值、大小写、取值范围问题需要处理。
  7. 利用好字符串的字典序比较。
  8. 输出字符串的问题,要考虑是否加’\0’。
  9. 遍历输入更新最优答案时,减除没有必要的遍历。例:
Read more »

插入排序

思路与实现

使用插入排序算法解决排序问题:输入一个待排序的序列,期待输出一个非递减的序列。

插入排序的思路是从左到右遍历序列,不断为遍历到的数(key)安排到左边(或原地)的合适位置。在遍历过程中,左边已遍历过的序列是已排序的,右边未遍历过的序列是未排序的。

Read more »

算法

问题与算法

现实中有很多需要解决的问题,一个问题可以看作是对{输入}期待某一种{输出}的过程。算法是用来解决一个良好定义的计算问题的工具,在现实中有着实际应用。正确的算法描述了{输入}{输出}的转换关系。

Read more »

关于IPv4

IP简述

IP包通过路由器向正确的方向跳转,以达到转发数据的效果。为了完成正确的效果,数据包必须包含源地址和目的地址。

IP支持数据分片。如果数据过长,则将其分成一片一片的分片再发送。分片到达目的地的顺序可能与发送顺序不同,因此IP需要将分片重新组成原来的数据段。

Read more »