设计模式总结

概述

在这里插入图片描述

设计模式的五项原则

1、单一职责原则
单一职责有2个含义,一个是避免相同的职责分散到不同的类中,另一个是避免一个类承担太多职责。减少类的耦合,提高类的复用性。

2、接口隔离原则
表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中额方法分组,然后用多个接口代替它,每个接口服务于一个子模块。简单说,就是使用多个专门的接口比使用单个接口好很多。

该原则观点如下:
1)一个类对另外一个类的依赖性应当是建立在最小的接口上
2)客户端程序不应该依赖它不需要的接口方法。

3、开放-封闭原则
open模块的行为必须是开放的、支持扩展的,而不是僵化的。
closed在对模块的功能进行扩展时,不应该影响或大规模影响已有的程序模块。一句话概括:一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。
核心思想就是对抽象编程,而不对具体编程。

4、替换原则
子类型必须能够替换掉他们的父类型、并出现在父类能够出现的任何地方。
主要针对继承的设计原则

1)父类的方法都要在子类中实现或者重写,并且派生类只实现其抽象类中生命的方法,而不应当给出多余的,方法定义或实现。
2)在客户端程序中只应该使用父类对象而不应当直接使用子类对象,这样可以实现运行期间绑定。

5、依赖倒置原则
上层模块不应该依赖于下层模块,他们共同依赖于一个抽象,即:父类不能依赖子类,他们都要依赖抽象类。
抽象不能依赖于具体,具体应该要依赖于抽象。

创建型设计模式

单例模式

为什么要使用单例?
单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。单例模式的一些应用场景,比如,避免资源访问冲突、表示业务概念上的全局唯一类。单例模式主要解决一个全局使用的类频繁的创建和销毁的问题。

单例模式有三个要素:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

实战案例一:处理资源访问冲突
我们将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。
实战案例二:表示全局唯一类
从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。再比如,唯一递增 ID 号码生成器(第 34 讲中我们讲的是唯一 ID 生成器,这里讲的是唯一递增 ID 生成器),如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

三种实现

实现步骤
将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;

在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

懒汉式的实现

懒汉式相对于饿汉式的优势是支持延迟加载,这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈,以时间换空间的方法。具体的代码实现如下所示:

class singleton   //实现单例模式的类  
{  
private:  
    singleton(){}  //私有的构造函数  
    static singleton* Instance;  
public:  
    static singleton* GetInstance()  
    {  
        if (Instance == NULL) //判断是否第一调用  
            Instance = new singleton();  
        return Instance;  
    }  
}; 

多线程下的懒汉模式

懒汉模式:即第一次调用该类实例的时候才产生一个新的该类实例,并在以后仅返回此实例。
需要用锁,来保证其线程安全性:原因:多个线程可能进入判断是否已经存在实例的if语句,从而non thread safety.
使用double-check来保证thread safety.但是如果处理大量数据时,该锁才成为严重的性能瓶颈。

class singleton   //实现单例模式的类  
{  
private:  
    singleton(){}  //私有的构造函数  
    static singleton* Instance;  
public:  
    static singleton* GetInstance()  
    {  
        if (Instance == NULL) //判断是否第一调用  
        	Lock();
        	if (Instance == NULL) {
        		Instance = new singleton(); 
        	}
             UnLock();
    }  
    return Instance;  
}; 

饿汉式的实现

饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。
以空间换时间。

class singleton   //实现单例模式的类  
{  
private:  
    singleton() {}  //私有的构造函数  
      
public:  
    static singleton* GetInstance()  
    {  
        static singleton Instance;  
        return &Instance;  
    }  
}; 

双重检测锁

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。
思路:只有在第一次创建的时候进行加锁,当Instance不为空的时候就不需要进行加锁的操作。(只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。)代码如下:

class singleton   //实现单例模式的类  
{  
private:  
    singleton(){}  //私有的构造函数  
    static singleton* Instance;  
      
public:  
    static singleton* GetInstance()  
    {  
        if (Instance == NULL) //判断是否第一调用  
        {   
            Lock(); //表示上锁的函数  
            if (Instance == NULL)  
            {  
                Instance = new singleton();  
            }  
            UnLock() //解锁函数  
        }             
        return Instance;  
    }  
};  

单例存在哪些问题?
大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。

  • 单例对 OOP 特性的支持不友好。抽象特性中,违背了基于接口而非实现的设计原则,如果一个需求发生改变,需要修改所有用到该单例类的地方,改动比较大。
  • 单例会隐藏类之间的依赖关系。单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。
  • 单例对代码的扩展性不友好。单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。举例数据库连接池,把数据库连接池设计成了单例类,如果只有一个数据库连接池的话,那么慢SQL长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
  • 单例对代码的可测试性不友好。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
  • 单例不支持有参数的构造函数。单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。三种解决方法:(1):创建完实例之后,再调用 init() 函数传递参数。(2)将参数放到 getIntance() 方法中。(3)将参数放到另外一个全局变量中。

单例模式有什么代替解决方案?
为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题,比如不支持延时加载。

如何理解单例模式的唯一性?

单例类中对象的唯一性的作用范围是“进程唯一”的。“进程唯一”指的是进程内唯一,进程间不唯一;“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”就意味着线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。“集群唯一”指的是进程内唯一、进程间也唯一。

如何实现线程唯一的单例?

单例模式只允许创建唯一一个对象,是指进程内只允许创建一个对象。

多线程的单例模式

工厂模式

建造者模式

设计模式总结

创建型设计模式

创建型设计模式包括:单例模式、工厂模式、建造者模式、原型模式。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

单例模式

单例模式用来创建全局唯一的对象。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式、懒汉式、双重检测、静态内部类、枚举。

尽管单例是一个很常用的设计模式,在实际的开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用,主要的理由有以下几点:

  • 单例对 OOP 特性的支持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不支持有参数的构造函数

那有什么替代单例的解决方案呢?如果要完全解决这些问题,我们可能要从根上寻找其他方式来实现全局唯一类。比如,通过工厂模式、IOC 容器来保证全局唯一性。

有人把单例当作反模式,主张杜绝在项目中使用。我个人觉得这有点极端。模式本身没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

除此之外,我们还讲到了进程唯一单例、线程唯一单例、集群唯一单例、多例等扩展知识点,这一部分在实际的开发中并不会被用到,但是可以扩展你的思路、锻炼你的逻辑思维。这里我就不带你回顾了,你可以自己回忆一下。

工厂模式

工厂模式包括简单工厂、工厂方法、抽象工厂这 3 种细分模式。其中,简单工厂和工厂方法比较常用,抽象工厂的应用场景比较特殊,所以很少用到,不是我们学习的重点。

工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。实际上,如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象就可以了,不需要使用工厂模式。当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。

当每个对象的创建逻辑都比较简单的时候,我推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的工厂类,我们推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。

详细点说,工厂模式的作用有下面 4 个,这也是判断要不要使用工厂模式最本质的参考标准。

  • 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
  • 代码复用:创建代码抽离到独立的工厂类之后可以复用。
  • 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
  • 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。

建造者模式

原型模式

设计原则与思想

MVC

MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。它将整个项目分为三层:展示层、逻辑层、数据层。

比如,现在很多 Web 或者 App 项目都是前后端分离的,后端负责暴露接口给前端调用。这种情况下,我们一般就将后端项目分为 Repository 层、Service 层、Controller 层。其中,Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口。当然,这只是其中一种分层和命名方式。

模型

贫血模型和充血模型中的血指的是逻辑,包括业务逻辑和非业务逻辑。

  • 贫血模型,逻辑从模型实体中剥离出去,并被放到了无状态的 Service 层中,于是状态和逻辑就被解耦开了;
  • 充血模型,它既包含数据,也包含逻辑,具备了更高程度的完备性和自恰性

视图

MVC 架构中的视图是指将数据有目的按规则呈现出来的组件。因此,如果返回和呈现给用户的不是图形界面,而是 XML 或 JSON 等特定格式组织呈现的数据,它依然是视图,而用 MVC 来解决的问题,也绝不只是具备图形界面的网站或者 App 上的问题。

页面聚合技术

Model层的解耦是继续分层,或是模块化。View层来说就是拆分页面,分别处理,最终聚合起来。
结构聚合:指的是将一个页面中不同的区域聚合起来,这体现的是分治思想。例如一个页面,具备页眉、导航栏、目录、正文、页脚,这些区域可能是分别生成的,但是最后需要把它们聚合在一起,再呈现给用户。
数据—模板聚合:聚合静态的模板和动态的数据,这体现的是解耦的思想。例如有的新闻网站首页整个页面的 HTML 是静态的,用户每天看到的样子都是差不多的,但每时每刻的新闻列表却是动态的,是不断更新的。

客户端和服务端聚合方式的比较

架构上,客户端聚合达成了客户端 - 服务端分离模板 - 数据聚合这二者的统一,这往往可以简化架构,保持灵活性。

客户端聚合
对于模板和静态资源(如脚本、样式、图片等),可以利用 CDN(Content Delivery Network,内容分发网络)技术,从网络中距离最近的节点获取,以达到快速展示页面的目的;而动态的数据则可以从中心节点异步获取,速度会慢一点,但保证了数据的一致性。

资源上,客户端聚合将服务器端聚合造成的计算压力,分散到了客户端。
客户端聚合也有它天然的弊端。其中最重要的一条,就是客户端聚合要求客户端具备一定的规范性和运算能力。

服务端聚合
使用服务端聚合,就必须在服务端同时准备好模板和数据,聚合形成最终的页面,再返回给浏览器。

常见的聚合技术

  • iFrame 聚合
  • 模板引擎
  • Portlet
  • SSI

控制器

控制器用于接收请求,校验参数,调用 Model 层获取业务数据,构造和绑定上下文,并转给 View 层去渲染。

路径映射和视图指向

入口路由就是路径映射,根据配置的规则,以及请求 URI 的路径,找到具体接收和处理这个请求的控制器逻辑;
出口路由就是视图指向,根据配置的规则,以及控制器处理完毕后返回的信息,找到需要渲染的视图页面。

请求参数绑定

请求被送到了指定的控制器方法,接下去,需要从 HTTP 请求中把参数取出来,绑定到控制器这一层,以便使用。整个控制器的流程中,有两次重要的数据绑定,这是第一次,是为了控制器而绑定请求数据,后面在视图上下文构造这一步中还有一次绑定,那是为了视图而进行的。

参数验证

在参数验证没有通过的情况下,往往会执行异常流程,转到错误页面,返回失败请求。

视图上下文绑定

在控制器中,我们经常需要将数据传入视图层,它可能会携带用户传入的参数,也可能会携带在控制器中查询模型得到的数据,而这个传入方式,就是将数据绑定到视图的上下文中。这就是我刚刚提到过的控制器层两大绑定中的第二个。

整个流程

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值