设计模式浅析

设计模式简介

设计模式是软件设计中常见问题的一般可重用的解决方案或模板。模式通常显示类或对象之间的关系和交互。这个想法是通过提供经过验证的开发范例来加快开发过程。

例如,在许多现实世界的情况下,我们只想创建一个类的一个实例。例如,无论个人身份如何,一次只能有一个国家主席。这种模式被称为单例模式。其他例子可以是由多个对象共享的单个数据库连接,因为为每个对象创建单独的数据库连接可能是昂贵的。类似地,在应用程序中可以有一个配置管理器或错误管理器来处理所有问题,而不是创建多个管理器。
类型的设计模式 主要有三种设计模式:
1、创建型
这些设计模式都是关于类实例化或对象创建的。这些模式可以进一步分类为类创作模式和对象创作模式。而类创建模式在实例化过程中有效地使用继承,对象创建模式可以有效地使用委托来完成工作。 创建型设计模式是工厂方法,抽象工厂,建造者,单例,对象池,原型和单例。
2、结构型
这些设计模式是关于组织不同的类和对象以形成更大的结构并提供新的功能。 结构型设计模式是适配器,桥接,组合,装饰器,外观,享元,私有类数据和代理。
3、行为型
行为模式是关于识别对象之间的常见通信模式并实现这些模式。 行为模式是责任链,命令,解释器,迭代器,中介者,备忘录,空对象,观察者,状态,策略,模板方法,访问者

常用的几个设计模式

1、工厂模式

工厂模式是一个创建型模式,它与对象的创建有关。在工厂模式中, 我们创建对象时,没有把创建的逻辑暴露给客户端。客户端使用相同的公共接口创建新类型的对象。
使用静态成员函数(静态工厂方法)来创建一个新对象的引用或指针。
工厂模式是创建一个对象的核心设计原则,允许客户端创建库的对象,使其不与库的类层次结构紧密耦合。
库是由某些第三方提供的,它暴露了一些公开的APIs,并且客户调用给这些公开的APIs来完成它的任务。一个非常简单的例子可以是android OS提供的不同种类的Views。

// A design without factory pattern
#include <iostream>
using namespace std;

// Library classes
class Vehicle {
public:
    virtual void printVehicle() = 0;
};
class TwoWheeler : public Vehicle {
public:
    void printVehicle()  {
        cout << "I am two wheeler" << endl;
    }
};
class FourWheeler : public Vehicle {
    public:
    void printVehicle()  {
        cout << "I am four wheeler" << endl;
    }
};

// Client (or user) class
class Client {
public:
    Client(int type)  {

        // Client explicitly creates classes according to type
        if (type == 1)
            pVehicle = new TwoWheeler();
        else if (type == 2)
            pVehicle = new FourWheeler();
        else
            pVehicle = NULL;
    }

    ~Client()   {
        if (pVehicle)
        {
            delete[] pVehicle;
            pVehicle = NULL;
        }
    }

    Vehicle* getVehicle() {
        return pVehicle;
    }
private:
    Vehicle *pVehicle;
};

// Driver program
int main() {
    Client *pClient = new Client(1);
    Vehicle * pVehicle = pClient->getVehicle();
    pVehicle->printVehicle();
    return 0;
}

上述设计模式的是有些问题的,客户端在创建新对象时,必须依赖于某个输入。意味着,如果库引入一个新的类ThreeWheeler。那么会发生什么呢?Client类最终需要增加一个if else判断分支来创建ThreeWheeler对象,这将导致客户端程序需要重新编译。每次库改动一次,Client类就需要做出相应的改变然后重新编译。这严重违反了对修改封闭-对扩展开放的设计原则。
为了避免这个问题,可以创建一个静态的方法,实现工厂的功能。

// C++ program to demonstrate factory method design pattern
#include <iostream>
using namespace std;

enum VehicleType {
    VT_TwoWheeler,    VT_ThreeWheeler,    VT_FourWheeler
};

// Library classes
class Vehicle {
public:
    virtual void printVehicle() = 0;
    static Vehicle* Create(VehicleType type);
};
class TwoWheeler : public Vehicle {
public:
    void printVehicle() {
        cout << "I am two wheeler" << endl;
    }
};
class ThreeWheeler : public Vehicle {
public:
    void printVehicle() {
        cout << "I am three wheeler" << endl;
    }
};
class FourWheeler : public Vehicle {
    public:
    void printVehicle() {
        cout << "I am four wheeler" << endl;
    }
};

// Factory method to create objects of different types.
// Change is required only in this function to create a new object type
Vehicle* Vehicle::Create(VehicleType type) {
    if (type == VT_TwoWheeler)
        return new TwoWheeler();
    else if (type == VT_ThreeWheeler)
        return new ThreeWheeler();
    else if (type == VT_FourWheeler)
        return new FourWheeler();
    else return NULL;
}

// Client class
class Client {
public:

    // Client doesn't explicitly create objects
    // but passes type to factory method "Create()"
    Client()
    {
        VehicleType type = VT_ThreeWheeler;
        pVehicle = Vehicle::Create(type);
    }
    ~Client() {
        if (pVehicle) {
            delete[] pVehicle;
            pVehicle = NULL;
        }
    }
    Vehicle* getVehicle()  {
        return pVehicle;
    }

private:
    Vehicle *pVehicle;
};

// Driver program
int main() {
    Client *pClient = new Client();
    Vehicle * pVehicle = pClient->getVehicle();
    pVehicle->printVehicle();
    return 0;
}

上述最大的改动就是,创建了一个静态的工厂函数,Client类不显式的创建对象,而是传递参数给静态工厂函数Create()。这样完全把类型的选择与客户端的对象创建解耦了。库现在负责根据输入来决定要创建的对象类型。客户端只需要调用库的工厂创建方法并传递所需的类型,而不必担心实际创建对象的实现。

2、单例模式

单体模式是最简单的设计模式之一。有时候,我们只需要有一个类的实例,例如由多个对象共享的单个数据库连接,因为为每个对象创建一个单独的数据库连接可能是昂贵的。类似地,在应用程序中可以有一个配置管理器或错误管理器来处理所有问题,而不是创建多个管理器。
定义:
单例模式是将类的实例化限制为一个对象的设计模式。
实现的方法
方法一:经典的实现方法是将构造函数,复制构造函数,operator=重载函数全声明为私有,声明一个静态的自身对象,然后定义一个静态方法,返回对象的引用。如果返回对象的指针,有可能客户程序会意外delete掉这个对象。补救方法是,将析构函数也声明为私有。因此,返回对象的引用被认为是较安全的实现。

// Classical Java implementation of singleton 
// design pattern
class Singleton
{
    private static Singleton obj;

    // private constructor to force use of
    // getInstance() to create Singleton object
    private Singleton() {}

    public static Singleton getInstance()
    {
        if (obj==null)
            obj = new Singleton();
        return obj;
    }
}

这里我们已经声明了getInstance()为static,以便我们可以在不实例化该类的情况下调用它。 getInstance()第一次被调用时,创建一个新的单例对象,之后它只返回相同的对象。请注意,在我们需要它并调用getInstance()方法之前,不创建单例obj。这被称为懒惰实例化。
上面的方法的主要问题是它不是线程安全的。考虑以下执行顺序。
单例模式
线程1和线程2同时访问getInstance(),这种执行顺序创建了两个Singleton对象。
可以给进程加锁来处理。这里不得不介绍一下Double-Check Locking 技术

// Classical Java implementation of singleton 
// design pattern
class Singleton
{
    private static Singleton obj;

    // private constructor to force use of
    // getInstance() to create Singleton object
    private Singleton() {}

    public static Singleton getInstance()
    {
if(obj==null)
{
lock();
        if (obj==null)
            obj = new Singleton();
        return obj;
unlock();
}
    }
}

lock确保当一个进程位于代码的临界区时,另一个进程不能进入临界区。这样就保证了在同一时刻,加了锁的那部分代码只有一个线程可以进入。第一个if是为了不让多个线程访问Singleton类时每次都加锁,而只是在实例未创建的情况下才加锁,减少调用花销。第二个if是为了让最先进入临界区的线程来创建对象实例,后面进入的线程进入时不会再去创建实例,也就是说,当obj为null并且有两个线程同时调用getInstance()时,第二个if就起作用了。

单例类的实现应具有以下属性:
单例模式

1、 它应该只有一个实例:这是通过从类中提供类的实例来完成的。应该防止外部类或子类创建实例。这是通过使构造函数私有化的,以便没有类可以访问构造函数,因此无法实例化。
2、实例应该是全局可访问的:单例类的实例应该是全局可访问的,以便每个类都可以使用它。在java中,可以通过使public instance的访问说明符来实现。

单例模式的应用程序很多,其中一些主要是:
1、硬件接口访问:使用单例取决于要求。单例类也用于防止对类的并发访问。实际上单例可以用于例如限制外部硬件资源使用。硬件打印机可以将打印后台处理程序作为单例,以避免多次并发访问并产生死锁。
2、日志:单例类用于日志文件生成。日志文件由logger类对象创建。假设一个应用程序,其中日志工具必须基于从用户接收到的消息来生成一个日志文件。如果有多个客户端应用程序使用此日志实用程序类,它们可能会创建此类的多个实例,并且可能会在并发访问同一个日志记录文件时导致问题。我们可以使用日志实用程序类作为单例,并提供全局引用点,以便每个用户可以使用此实用程序,并且没有2个用户在同一时间访问它。
3、配置文件:这是单例模式的另一个潜在候选者,因为它具有性能优势,因为它可以防止多个用户重复访问和读取配置文件或属性文件。它创建一个配置文件的单个实例,可以同时由多个调用访问,因为它将提供加载到内存对象中的静态配置数据。应用程序只能从第一次从配置文件中读取,并且从第二次调用开始,客户端应用程序从内存中的对象读取数据。
4、缓存:我们可以将缓存用作单例对象,因为它可以具有全局引用点,并且对于将来对缓存对象的所有调用,客户端应用程序将使用内存中对象。

3、装饰器模式

假设我们正在为比萨饼商店建立一个应用程序,我们需要对比萨饼类进行建模。假设他们提供四种类型的比萨饼,即胡椒饼,农舍,玛格丽特和鸡节。最初我们只是使用继承,并抽象出披萨类中的常用功能。
这里写图片描述
每个比萨饼都有不同的成本。我们已经子类中的getcost()来找到合适的成本。现在假设一个新的要求,除了比萨,客户还可以要求几个浇头,如新鲜的番茄,面具,辣椒,辣椒,烧烤等。让我们考虑一下,我们如何适应上述课程的变化使客户可以选择比萨饼与浇头,我们得到客户选择的比萨饼和浇头的总成本。
选项1
为每个浇头的匹萨创建一个新的子类。类图如下
这里写图片描述
这看起来很复杂。有太多的类,是一个维护噩梦。如果我们想添加一个新的浇头或比萨饼,我们必须添加这么多类。这显然是非常糟糕的设计。

选项2:
让我们将实例变量添加到比萨饼基类中,以表示每个比萨饼是否有浇头。类图将如下所示:
这里写图片描述

这个设计首先看起来不错,但是让我们来看看与之相关的问题。 补品价格变动将导致现有代码的变更。 新的浇头将迫使我们添加新方法并在超类中更改getcost()方法。 对于一些比萨饼,一些浇头可能不合适,但子类继承它们。 如果客户想要双辣椒或双重乳酪呢?

1、针对一个匹萨对象
这里写图片描述
2、用辣椒对象“装饰”它。
这里写图片描述
3、用奶酪对象“装饰”它。
这里写图片描述
4、调用getcost()并使用委托代替继承来计算浇头成本。
这里写图片描述
我们最后得到的是一个有奶酪和辣椒浇头的匹萨。可视化“装饰器”对象,如包装器。这里是装饰器的一些属性:
● 装饰器具有与其装饰物体相同的超类型。
● 您可以使用多个装饰器来包装对象。
● 因为装饰器与对象具有相同的类型,所以我们可以传递装饰对象而不是原始对象。
● 我们可以在运行时装饰对象。

定义: 装饰器模式动态地附加对象的附加责任。装饰器为扩展功能提供了子类化的灵活替代方法。
这里写图片描述
每个组件可以单独使用,也可以由装饰器包装。
每个装饰器都有一个实例变量,它保存对其装饰的组件的引用(has-a 的关系)。
该混合组件是我们要动态装饰的对象。

优点:
可以使用装饰器模式来在运行时扩展(装饰)特定对象的功能。
装饰器模式是子类化的替代方案。子类在编译时添加行为,更改影响原始类的所有实例;装饰可以在运行时为单个对象提供新的行为。
装饰器提供按现收现付的方式增加责任。而不是尝试支持复杂的可定制类中的所有可预见的功能,您可以定义一个简单的类,并使用装饰器对象逐步添加功能。
缺点:
装饰器可能使实例化组件的过程变得复杂,因为您不仅必须实例化组件,而且将其包装在多个装饰器中。
装饰器跟踪其他装饰工具可能很复杂,因为回顾装饰链的多层开始让装饰器模式超出其真实意图。

最终使用了装饰器模式的匹萨成本计算类图如下
这里写图片描述

4、适配器模式

考虑一个usb到以太网适配器。当我们有一个以太网接口和另一端的usb时,我们需要这个。因为他们是不相容的。这个例子非常类似于面向对象的适配器。在设计中,当我们有一个类(客户端)期望某种类型的对象时,使用适配器,并且我们有一个提供相同功能但暴露不同接口的对象(adapttee)。
这里写图片描述
使用适配器:
1、客户端通过使用目标接口调用其上的方法向适配器发出请求。
2、适配器使用适配器接口将该请求转换为被适配者。
3、客户端接收到调用返回的结果,并不知道适配器的存在。

定义: 适配器模式将类的接口转换为客户端期望的另一个接口。适配器使类可以协同工作,不受不兼容的接口影响。
这里写图片描述
客户端只看到目标接口而不是适配器。适配器实现目标接口。适配器将所有请求委托给被适配者。

假设你有一个带有fly()和bird()方法的鸟类。还有一个具有squeak()方法的玩具类。让我们假设你缺少玩具对象,而你想在使用玩具对象的地方使用鸟类对象或者鸟类对象的方法。鸟类具有一些类似的功能,但实现了不同的界面,所以我们不能直接使用它们。所以我们将使用适配器模式。这里我们的客户将是玩具,被适应者将是鸟。我们要将鸟类对象的接口转换为玩具对象的接口。

#include <iostream>
using namespace std;

class Bird
{
    //Bird is a base class,the method is implemented by its subclass
public:
    virtual void fly(){};
    virtual void makeSound(){};
};

class Sparrow:public Bird
{
    // a concrete implementation of bird
public: 
    void fly()
    {
        cout<<"Flying"<<endl;
    }
    void makeSound()
    {
        cout<<"Chirp Chirp"<<endl;
    }
};

class ToyDuck
{
    // target interface
    // toyducks don't fly they just make
    // squeaking sound
public: 
    virtual void squeak() = 0;
};

class PlasticToyDuck:public ToyDuck
{
public: 
    void squeak()
    {
        cout<<"Squeak"<<endl;
    }
};

class BirdAdapter:public PlasticToyDuck
{
    // You need to implement the interface your
    // client expects to use.
    Bird* bird;
public: 
    BirdAdapter(Bird* b)
    {
        // we need reference to the object we
        // are adapting
        bird = b;
    }

    void squeak()
    {
        // translate the methods appropriately
        bird->makeSound();
    }
};

int main()
{
        Sparrow* sparrow = new Sparrow();
        PlasticToyDuck* toyDuck = new PlasticToyDuck();

        // Wrap a bird in a birdAdapter so that it 
        // behaves like toy duck
        ToyDuck* birdAdapter = new BirdAdapter(sparrow);

        cout<<"Sparrow..."<<endl;
        sparrow->fly();
        sparrow->makeSound();

        cout<<"ToyDuck..."<<endl;
        toyDuck->squeak();

        // bird behaving like a toy duck
        cout<<"BirdAdapter..."<<endl;
        birdAdapter->squeak();
        delete sparrow;
        delete toyDuck;
        delete birdAdapter;
    return 0;
}

对象适配器vs类适配器
们上面实现的适配器模式称为对象适配器模式,因为适配器持有适配器的实例。还有另一种类型称为类适配器模式,它使用继承而不是组合,但是需要多个继承来实现它。
类适配器模式的类图:
这里写图片描述
不是使用适配器(组合)中的被适配者对象,而是使用支配器继承被适配者的功能。 由于许多语言(包括java)不支持多重继承,并且与许多问题相关联,我们还没有使用类适配器模式显示实现。
优点: 有助于实现可重用性和灵活性。 客户端类不再需要使用不同的接口,并且可以使用多态来在适配器的不同实现之间进行交换。
缺点: 所有请求都被转发,所以开销略有增加。 有时需要沿着适配器链进行许多修改以达到所需的类型。

5、策略模式

假设我们正在建立一个游戏“街头霸王”。为了简单起见,假设角色可能有四个移动,即踢,打,滚,跳。每个角色都有踢和打的动作,但是滚动和跳跃是可选的。你如何建立你的类?假设最初你使用继承并抽出一个Fighter类中的共同特征,并让其他角色子类继承Fighter类。
我们的Fighter类我们会默认执行正常的动作。任何具有专门移动的角色都可以在其子类中覆盖该动作。类图将如下:
这里写图片描述
以上设计有什么问题?
如果一个角色不执行jump呢?它仍然继承了父类的jump行为。虽然在这种情况下你可以重写jump,但是您可能需要为许多现有的类执行此操作,也可以为未来的类进行处理。这也将使维护困难。所以我们不能在这里使用继承。
如果使用接口呢?
这里写图片描述
它更简洁。我们从Fighter中取出了一些动作(某些角色可能无法执行),并为它们创建了接口。这样只有应该jump的角色才能实现jump行为。以上设计有什么问题? 上述设计的主要问题是代码重用。因为没有默认的跳和滚动行为实现,我们可能会有代码重复。您可能需要在许多子类中重复重写相同的跳行为。
我们该如何避免呢?
如果我们做出JumpBehavior类和RollBehavior类而不是接口呢?那么我们必须使用许多语言不支持的多重继承,因为它有许多问题。

策略模式定义:
“在计算机编程中,策略模式是一种软件设计模式,可以在运行时选择算法的行为。战略格局 定义了一系列算法, 封装每个算法, 使算法在该家族内互换。“
这里写图片描述
这里我们依靠组合而不是继承来重用。上下文由策略组成。上下文将执行行为委托给策略。上下文将是需要改变行为的类。我们可以动态地改变行为。策略被实现为接口,以便我们可以改变行为而不影响我们的上下文。
优点:
一系列算法可以定义为类层次结构,可以互换使用,以改变应用程序行为而不改变其架构。
通过分别封装算法,可以很容易地引入符合相同接口的新算法。
应用程序可以在运行时切换策略。
策略使客户端可以选择所需的算法,而不使用“switch”语句或一系列“if-else”语句。
用于实现算法的数据结构完全封装在策略类中。因此,可以改变算法的实现而不影响上下文类。

缺点:
应用程序必须意识到所有的策略才能为正确的情况选择正确的策略。
上下文和策略类通常通过抽象策略基类指定的接口进行通信。策略基类必须暴露所有必需行为的接口,有些具体的策略类可能无法实现。
在大多数情况下,应用程序使用所需的策略对象配置上下文。因此,应用程序需要创建和维护两个对象来代替一个对象。

使用策略模式实现“接头霸王”模型
第一步是确定未来不同类别可能会有所不同的行为,并将其与其他类别分开。这个例子中是指踢和跳的行为。为了分离这些行为,我们将把这两种方法从Fighter类中拉出来并创建一组新的类来表示每个行为。
这里写图片描述
Fighter类现在委托其踢和跳跃行为,而不是使用在Fighter类或其子类中定义的踢和跳方法
这里写图片描述

将我们的设计与策略模式的定义进行比较,封装的踢和跳行为是两个系列的算法。并且这些算法在实现中是可以互换的。
这里写图片描述

6、观察者模式

要了解观察者模式,首先需要了解主题和观察者对象。 主题和观察者之间的关系可以很容易被理解为类似于杂志订阅。
杂志出版社(主题)在商业中出版杂志(数据)。
如果您(数据/观察者的用户)对您订阅(注册)的杂志感兴趣,并且如果发布了新版本,则会将其发送给您。
如果您取消订阅(注销),您将停止获取新版本。
发行商不知道你是谁,以及你如何使用这本杂志,它只是把它交给你,因为你是订阅者(松耦合)。

定义: 观察者模式定义对象之间的一对多依赖关系,以便一个对象改变状态,所有的依赖项都会被自动通知和更新。
说明 :
1、一对多依赖关系是在主题(一)和观察者(多)之间。
2、依赖关系是由于观察者们自己无法访问数据,他们依赖主题提供数据。
观察者模式
● 这里的观察者和主题是接口(可以是任何抽象超类)。
● 所有需要数据的观察者都需要重新实现观察者接口。
● 观察者界面中的notify()方法定义当主题提供数据时要采取的操作。
● 该主题维持一个观察者聚集,这是当前注册(订阅)观察员的列表。
● registerobserver(观察者)和unregisterobserver(观察者)分别是添加和删除观察者的方法。
● 当数据更改并且观察者需要新数据时,调用notifyobservers()
优点: 在交互的对象之间提供松散耦合的设计。松散耦合的对象灵活随需求变化。这里松耦合意味着相互作用的对象应该有更少的关于彼此的信息。
观察者模式提供了这种松耦合:
● 主体只知道观察者实现观察者界面。
● 没有必要修改主题以添加或删除观察者。
● 我们可以互相重用主题和观察者类。
缺点: 由于监视器故障导致的内存泄漏,因为显式注册和注销观察者。当观察者不再需要订阅,但却无法取消订阅主题时,会发生泄漏。因此,主体仍然持有对观察者的引用,阻止它被垃圾回收 - 包括所指向的所有其他对象 - 只要主体是活着的,直到应用结束。

何时使用这种模式?
当多个对象依赖于一个对象的状态时,您应该考虑在应用程序中使用此模式,因为它为同一个对象提供了一个整洁且经过良好测试的设计

现实生活用途:
它在gui工具包和事件监听器中大量使用。
在java中,按钮(subject)和onclicklistener(观察者)用观察者模式建模。
社交媒体,RSS订阅,电子邮件订阅,您可以选择关注或订阅,并收到最新通知。
播放商店上的应用程式的所有使用者都会收到通知,如果有更新。

7、命令模式和状态模式

命令模式
定义:命令模式将请求封装为对象,从而让我们用不同请求,队列或日志请求参数化其他对象,并支持可撤销操作。
主要特点是将一个函数或者对象作为一个参数传递一个动作给接收者,并将做些动作放入动作队列,因此实现上,经常会使用迭代器,遍历动作对象的队列

优点: 使我们的代码可扩展,因为我们可以添加新的命令而不改变现有的代码。 减少了命令的调用者和接收者的耦合。
缺点: 增加每个命令的类数

状态模式
状态模式产生一个可以改变其类的对象,当发现在大多数或者所有函数中都存在条件的代码时,这种模式就派上用场。
前端对象将对应分支的操作委派给状态对象。

猜你喜欢

转载自blog.csdn.net/chifredhong/article/details/73776367