设计模式的概念

设计模式主要分为三类:创建类的设计模式、结构型设计模式、行为型设计模式。

创建类的设计模式:简单工厂,工厂模式,抽象工厂,建造者,单例,原型

结构型设计模式:代理模式、享元模式

行为型设计模式:观察者模式

一、简单工厂模式

简单工厂模式:由三个角色构成,工厂类,抽象产品类,继承自抽象产品类的产品类。工厂类根据传入的参数决定初始化哪个产品类实例。这个模式的理解关键在于抽象产品类,也就是该工厂生产的产品是有共同的父类或者接口。目前项目中很多实用简单工厂模式存在滥用的情况,初始化的实例没有共同的父类,只是把初始化的代码放到了一个专门的类里。在调用的时候还是需要关注工厂类生产的产品如何使用。而标准的工厂类,生产的产品是有共性的,按照规范使用即可。举个例子:标准工厂模式下,工厂只生产小米手机,苹果手机,华为手机,他们的抽象产品类就是他们都是手机。而滥用情况下,工厂类生产手机,耳机,扫地机器人,吸尘器,冰箱等。

  • 错误示例:
typescript 复制代码
AbstractProductClass (type) {}
FactoryClass  (type) {
 productInstance = new AbstractProductClass {type}
 productInstance.type = type
 return productInstance
}
xmProductInstance = new FactoryClass('xiaomi')
  • 正确示例:
typescript 复制代码
AbstractProductClass () {}
XiaoMiProductClass implement AbstractProductClass  () {}
iPhoneProductClass implement AbstractProductClass  () {}

FactoryClass  (type) {
 if (type == 'xiaomi') {
  return new XiaoMiProductClass()
 } else if (type == 'iPhone') {
  return new iPhoneProductClass()
 }
 ....
}
xmProductInstance = new FactoryClass('xiaomi')

关键点:

产品实例的属性和方法一般是定义在抽象类里的,如果产品自有特性太多,就变成我上面说的滥用了。工厂生产的就不是手机了,生产的更笼统了,变成生产电子设备了

产品一定是有个抽象父类或者更抽象的接口去继承和遵守,这样使用的时候才可以忽略实例细节,按照规范使用。

抽象产品类是固定的,不能根据参数变化

产品一定要有共性才可以用简单工厂

实际工作中,还是很容易滥用的,把一堆没有共性类的初始化放到一个工厂类中,根据不同的参数初始化不同的类。看上去是简单工厂,实际不是。

  • 总结
  1. 滥用的情况下只是把对象的实例过程集中到了一块,和分散的初始化没啥大的区别。简单工厂下初始化出来的实例因为有相同的接口,使用的时候就更方便了,使用方式也是统一的,甚至这些实例都是可以互相替换的。
  2. 弊端:简单工厂模式是不符合设计原则中的开闭原则的。比如你想生产新的一种手机vivo,你需要改工厂类,新增一个else if。这是不符合开闭原则的

二、工厂模式

由4个角色构成,抽象工厂父类具体工厂子类抽象产品父类具体产品子类。接我上面的话,简单工厂是把所有的手机生产都放到了一个工厂,工厂模式是把每个手机都单独建立一个工厂。这样工厂就需要有一个抽象父类了,抽象父类提供了统一的生产手机的方法,子工厂只需要实现这个方法就能生产对应的手机。这个模式下如果新增一种手机产品vivo,需要继承抽象工厂建立自己的vivo工厂,继承手机抽象类生成vivo手机类,然后用vivo工厂去生产vivo手机。符合开闭原则,但是有个问题就是类可能会越变越多,增加一定的复杂度。

  • 当你的简单工厂经常需要新增实例初始化,这样就会经常要改工厂类,就可以考虑用工厂模式优化一下。
  • 映射到现实生活中,没有一家工厂是生产两种手机的,小米手机是小米生产的,苹果是苹果公司生产的,如果想生产vivo,创立一家新公司vivo生产vivo就行了。这个就是工厂模式。

三、抽象工厂模式

在抽象工厂模式下还是有四个角色,抽象工厂父类具体工厂子类抽象产品父类具体产品子类,这个和工厂模式一模一样。唯一不同的是,抽象工厂父类需要再定义一个创建耳机的方法,然后定义耳机父类,耳机子类,然后加到里面就行了。这个在sdk中一些创建数组,列表啥的用。

  • 这个其实很好理解,想一个场景,上面的工厂都只能生产手机,如果你想生产耳机怎么办呢?这就出现了产品族,一个工厂可能生产的是一个产品族而不是单一的产品。

四、建造者模式

建造者模式:有四个角色,产品类抽象建造者类具体建造者类指挥者类。该模式主要解决的问题是如何创建一个复杂的对象,对象的复杂性主要体现在两个方面:初始化参数多,参数之间有依赖关系或者有校验要求。在不使用设计模式的情况下我们初始化一个复杂对象,就直接通过往构造函数中传递一堆参数来完成。如果觉得参数太多,写起来太长,又可以给这些参数各自定义一个set方法,通过写一堆set方法挨个赋值。但是这样还有一些问题,比如如何保证必填参数被设置了,一堆set方法被暴露出去外部可以修改这个对象,而我希望这个对象一旦创建就不可变,这个时候就引入了建造者模式。这个模式下建造者类需要定义产品类的属性和方法,而指挥者类接受一个建造者对象(Builer),然后调用Builder中的方法生产这个产品。

  • 完整示例
typescript 复制代码
public class Phone{
    private String cpu;
    private String screen;
    private String memory;
    private String mainboard;
    
    private Phone(AbstractBuilder builder){
        this.cpu = builer.cpu;
        this.screen = builer.screen;
        this.memory = builer.memory;
        this.mainboard = builer.mainboard;
    }
}

public class AbstractBuilder{
    private String cpu;
    private String screen;
    private String memory;
    private String mainboard;
    
    public AbstractBuilder cpu{
        return this;
    }
    public AbstractBuilder screen{
        return this;
    }
    public AbstractBuilder memory{
        return this;
    }
    public AbstractBuilder mainboard{
        return this;
    }
    public Phone build(){
        return new Phone(this)
    }
}

public class ConcreteXiaoMiBuilder extend AbstractBuilder{
    public ConcreteXiaoMiBuilder cpu{
        this.cpu = @"小米CPU";
        return this;
    }
    public ConcreteXiaoMiBuilder screen{
        this.screen = @"小米显示屏";
        return this;
    }
    public ConcreteXiaoMiBuilder memory{
        this.memory = @"小米内存条";
        return this;
    }
    public ConcreteXiaoMiBuilder mainboard{
        this.mainboard = @"小米主板";
        return this;
    }
}

public class Director(AbstractBuilder builder){
    Phone phone = builder.cpu().screen().memory().mainboard().build();
}

main(int argc, char * argv[]){
    ConcreteXiaoMiBuilder *builder = new ConcreteXiaoMiBuilder();
    Phone *xiaomiPhone = new Director(builder);
}
  • 简化示例
typescript 复制代码
public class Phone{
    private String cpu;
    private String screen;
    private String memory;
    private String mainboard;
    
    private Phone(Builder builder){
        this.cpu = builer.cpu;
        this.screen = builer.screen;
        this.memory = builer.memory;
        this.mainboard = builer.mainboard;
    }
}

public class Builder{
    private String cpu;
    private String screen;
    private String memory;
    private String mainboard;
    
    public Builder cpu(String val){
        this.cpu = val;
        return this;
    }
    public Builder screen(String val){
        this.screen = val;
        return this;
    }
    public Builder memory(String val){
        this.memory = val;
        return this;
    }
    public Builder mainboard(String val){
        this.mainboard = val;
        return this;
    }
    public Phone build(){
        return new Phone(this)
    }
}

main(int argc, char * argv[]){
    Builder builder = new Builder();
    Phone xiaomiPhone = builder.cpu('小米CPU').screen('小米显示屏').memory('小米内存条').mainboard('小米主板').build();
}

完整示例 - 》简化示例:完整示例中这个是比较规范的写法,实际使用的时候可以简化,即简化示例。比如如果只有一个builder,可以省略抽象builder,或者把抽象builder和具体builder结合一下,通过传递参数的形式返回不同的builder。指挥官的角色可以省略,其实他就相当于调用者。

五、代理模式

代理模式:分为静态代理和动态代理,静态代理有两种实现方式(继承,组合)。组合方式是代理类持有被代理类,代理类处理完一些自己的逻辑之后,再去调用被代理类,代理类和被代理类有相同的接口,调用者像使用被代理对象一样使用代理对象。继承的方式主要用在一些比如三方库这种没法重新定义其接口的情况,这种情况下需要在继承类中重写父类方法的方式插入自己的逻辑。静态代理的缺点是每个被代理的类都需要创建一个代理类,如果这些代理类的逻辑类似,代码会重复,且类的数量增多。这就引入了动态代理,多个被代理类共享一个代理类,通过运行时的一些方法,在运行阶段用代理类去替换被代理类做一些逻辑处理。代理模式常用场景:远程代理(在本地封装远程对象,调用者像使用本地函数一样,调用远程对象,个人认为云函数就是基于代理模式实现的),网络日志,网络缓存(在发起网络请求前后都可以对网络数据做缓存处理,或者日志的上报)等。

六、桥接模式

桥接模式:这个模式比较难理解,用的也比较少,用来解决继承关系的指数爆炸。举例:形状(Shape)包含圆形,方形,椭圆形等,而颜色包含红色,绿色,蓝色等。如果我们用继承的关系去定义这些形状和颜色的组合,会有n*m个类。在桥接模式下Shape作为顶层的抽象类,可以扩展出多个抽象子类(圆形,方形等),同时Shape持有一个颜色的引用。这样当需要一个红色的圆时,只需要把红色对象传递到circle类中,就可以得到一个红色的圆。这个模式也遵循了组合原则,用组合的方式减少继承的层级。工作中当你意识到继承层次过深的时候,多考虑能不能用组合的方式处理,桥接只是处理这种问题的一个模式。另外在看别人的代码时,如果遇到了这种桥接模式,能看懂就行。

七、装饰者模式

装饰者模式:对一个对象能力的增强。装饰器类和源类具有相同的抽象类。装饰器类持有源类,因为装饰器和源类继承自相同的父类,所以装饰器和装饰器可以嵌套调用,实现对源类的多层次装饰。

主要由4个角色构成:component接口;concretecomponent实现类,继承自component;Decorator装饰器抽象类,继承自component,持有对实现了component接口的类的引用;ConcreteDecorator装饰器实现类。

八、适配器模式

适配器模式:这个模式是一种妥协的模式,为解决兼容性提出的模式。如果代码从一开始就设计的比较好,这个模式就不会用到。但实际情况下这个模式是很常见的。最常见的场景是原接口废弃,但是如果直接改原接口会导致代码调用的地方都要跟着变动,所以一般情况下都是重新定义新接口,然后老接口调用新接口,将老接口标记为deprecated。还有一种常见场景是在使用一些三方库的时候,由于同类型众多三方库中对外接口不一致,为了能在后期方便对三方库替换,可以考虑引入适配器,调用的时候通过适配器调用,如果后面需要替换三方库,只需要改动适配器的代码即可。

九、门面模式

门面模式:这个模式很简单,主要是针对接口的封装,方便子系统被高层系统调用。举例:后端基于接口的可复用性,定义了细粒度的接口a,b,c,d。前端在渲染页面数据时,需要用到a,b,d接口,那我们就需要发起3次请求,如果这三次请求还有顺序要求,那对前端来说使用成本就比较高,并且多个接口也影响网络性能。这个时候就可以要求后端基于a,b,c子接口,提供一个门面接口,将这三个接口包起来。门面就相当于对外的一个展示,隐藏了内部的细节。

十、组合模式

组合模式,这个模式比较少见,针对的是很特殊的数据结构用。如果你的数据结构是一种树状的,用组合模式处理起来就方便。常见的比如公司组织架构,文件夹目录。这种可以考虑用组合模式。

十一、享元模式

享元模式:这个模式理解和使用起来也很简单,就是为了节省内存对对象的共享使用。举例:在棋牌类游戏中,假设有1000个游戏虚拟房间,每个房间都有关于棋子的信息,这样棋子的信息也会创建1000个,但他们又很相似,这个时候就可以用享元模式,把棋子信息设计成共享的。这个和单例模式有一些相似,但是设计初衷不一样。单例是为了限制对象的个数,享元模式是为了节省内存。

十二、原型模式

就是对对象的copy,需要注意深浅copy。

十三、观察者模式

观察者模式:这个模式也是很常用的,理解也很简单,也叫发布订阅模式。iOS下系统框架实现了这种设计模式,可以直接用,但是不太好用,所以有时候会自己写一个。前端和安卓常见的实现方式叫eventbus,有阻塞和非阻塞两种方式,我们一般都是用的阻塞。

十四、模板方法模式

模板方法模式:这个模式也比较好理解,通过继承的方式抽象方法步骤,形成固定的调用模板,然后子类去重写被调用的这些步骤。这其中还有一个小点就是钩子,可以在这些步骤中间插入一些钩子,实现扩展能力。举例:有个抽象类方法A,其中定义了一个方法a,这个方法会依次调用a1,a2,a3,这样基于A的子类B,只需要重写a1,a2,a3,就可以实现固定的调用顺序。这个可能在实际工作中已经在用了,只不过不知道这就叫模板方法。

十五、责任链模式

责任链模式:用一个线性表按照顺序存储了若干处理对象,然后把被处理对象传递给管理这个线性表的对象(chain),chain按照顺序依次处理该对象。

有两种实现方式:链表,数组。有两种场景:遇到有对象能处理则停止;遇到有对象处理不停止,直到chain中所有对象都处理一遍。这个地方所有的处理对象也有一个共同的抽象父类,抽象父类主要定义了处理方法和调用下一个处理对象。

(chain数组中存了一堆类的实例对象,这些对象有共同的抽象类,在处理一个被处理对象B时,就从数组里取实例对象去处理B就完了,你可以选择遇到有能处理B的就break,也可以选择continue(这样就是另一个变种,数组中每个对象都会处理一下B)。)

十六、状态机模式

状态机:这个模式和前面说的组合模式(处理树形数据模型时:组织架构,文件结构)都不是常用的模式,但是在特定场景下又特别好用。我们项目中播放器状态管理就用的状态机。主要由3个部分组成,状态,事件,动作。当一个状态触发了某个事件后做对应的操作并且切换状态。有三种实现方式:分支逻辑(其实就是ifelse这种,在一些比较简单的状态机下用,这个并不推荐,扩展性差,更像是硬编码);查表法:这个模式适用于动作比较简单的情况,大概意思就是构建一个状态转移的二维表,一个动作二维表,这两个表甚至可以做成远程配置的,当事件触发的时候直接从这两个表查映射关系做对应操作。状态模式:把状态和动作封装成类,每个状态处理自己遇到这些事件的状态转移和动作处理。这种模式会产生很多类,但是能处理动作比较复杂的场景。

十七、迭代器模式

迭代器模式:有两个角色:容器,迭代器。而容器又分容器接口,容器实现,迭代器也分为迭代器接口,和迭代器实现。对于迭代器的接口,主要含有三个方法:hasNext,currentItem,next。容器为啥也需要有个接口呢,这个主要是因为接口可以统一迭代器的创建,比如容器接口定义一个iterator方法,用来创建一个迭代器实例,容器的接口实现类只需要实现这个方法,返回一个自己的迭代器实例即可。迭代器实例通过依赖注入的方式持有容器实例,然后迭代器通过操作这个容器实现对应的那三个方法。

(这个模式在实际的工作中使用时比较少的,因为大部分变成语言下都对自己容器实现了迭代器,只需要直接调用就行了,很少需要自己实现一个迭代器。并且对于像数组,链表这种简单的容器,甚至不需要用迭代器更容易访问。但是对于更复杂的容器,比如树,图他们有更复杂更多样的遍历路径,这种情况就可以实现多个迭代器,根据不同的需求,调用不同的迭代器。)

十八、访问者模式

访问者模式:大概得使用场景就是,当一个对象需要针对这个对象做很多操作的时候,这个类就会变得越来越复杂。访问者模式将对象模型和操作分离开来,访问者可以操作这个对象里的元素。比如,你有一个房子,房子里有各种电器设备和生活用品,在非访问者模式下,每个设备坏了,你就去学习一种维修能力,最后你能修这个房子里的所有电器,对应到编程里面,就是这个类变得非常大。而访问者模式下,电器和维修工是分开的,当你需要维修的时候,有个访问者来敲门,你开们让他进来维修,修完他就走了。在软件设计里就理解为,数据对象模型和操作分离开,防止数据对象里包含了太多业务操作逻辑。以上是一个大概的理解,因为这个比较少见,细节上没有细研究。以后遇到这种场景再看吧

十九、备忘录模式

备忘录模式:主要使用场景:需要回滚操作的场景或者需要恢复数据的场景。主要有三个角色:原始类,备忘录类,负责人类。原始类就是你要备份的对象,这个对象有一些类似set方法可以改变对象本身,所以这个对象不适合用来做备忘录,备忘录类是一个不可变对象,也就是没有set方法,只有get方法,用来保存原始类的状态。而原始类提供了创建备忘录类实例的方法,并且也提供了通过备忘录类,重新赋值原始类的方法。而负责人类就是对备忘录类实例的一个持有和操作,它提供一个容器持有了所有的备忘录类,通过备忘录类就可以用来恢复一个原始类,而通过原始类也可以创建一个备忘录类存储在负责人类中。

(这个模式实现方式是很多种的,核心就是你原来的对象因为一般都是可变的,直接缓存有很多问题,不满足封装原则。所以弄了一个备忘录类,这个类通过初始化方法之后,就不能被改变了,就像是给这个对象拍了一个快照一样。之后通过对快照的操作完成回滚,恢复等操作。)

二十、命令模式

命令模式:这个模式使用的也比较少,主要是后端用的比较多一点。在经典设计模式里,这个解释很复杂,看不懂。王争那个文中对这个做了进一步解释,就是把一个函数封装到对象里,这样每一个命令对应的处理函数就变成了一个对象,对象可以传来传去,还可以异步调用,撤销等操作。大概理解就行吧,实际没遇到过这种使用场景。遇到了再说

二十一、解释器模式

解释器模式:这个模式更少见,遇到能看懂就行,使用场景是对语法规则进行解析,比如编译器里的语法解析器,规则引擎,正则表达式解析。实现方式也比较灵活,没有固定范式。

把一个复杂语句的解析进行拆分,拆成更多个小的解析类,最后把各个解析类的结果合并起来,形成最终的解析结果。举一个简单例子:比如对一个算数表达式(包含数字,加减乘除)进行解析,可以生成5个解析类NumberExpression、AdditionExpression、SubstractionExpression、MultiplicationExpression、DivisionExpression,它们有一个共同的接口,有一个共同的方法interpret(),在遇到一个表达式的时候就分别用这几个类进行细分的解析。

二十二、中介者模式

中介者模式:这个模式也不常用,但是理解起来比较简单,使用场景是如果多个对象之间相互调用,会造成关系比较复杂,耦合性比较强。这个时候引入一个中介者,所有的对象都通过中介者进行交互。这样就把一个多对多的关系,变成了一对多。缺点是这样可能会导致中介者变得特别庞大。现实中例子,就是飞机和塔台的关系,飞机把自己的定位等信息告诉塔台,塔台进行航线的协调工作。编程中的例子是:一个对话框中有一堆UI组件,这些组件之间会相互影响,如果这些逻辑都写在一块,组件也会变得不可复用,交互也会乱。这个时候就可以引入一个中介者。

总结

以上是所有的设计模式,有些比较常用也好理解。有些不常用也不好理解。整体上对设计模式有个概念了,在日常编码过程中,需要注意的是,当遇到一些比较复杂的编码需求时,尤其是代码越写越乱的时候,多思考一些面向对象编程和设计模式,看看代码能不能优化,写出更容易维护,阅读,复用的代码。

相关推荐
Auc242 分钟前
使用scrapy框架爬取微博热搜榜
开发语言·python
QQ同步助手9 分钟前
C++ 指针进阶:动态内存与复杂应用
开发语言·c++
信徒_11 分钟前
常用设计模式
java·单例模式·设计模式
凯子坚持 c15 分钟前
仓颉编程语言深入教程:基础概念和数据类型
开发语言·华为
小爬虫程序猿17 分钟前
利用Java爬虫速卖通按关键字搜索AliExpress商品
java·开发语言·爬虫
程序猿-瑞瑞19 分钟前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
qq_4335545420 分钟前
C++ 面向对象编程:递增重载
开发语言·c++·算法
易码智能28 分钟前
【EtherCATBasics】- KRTS C++示例精讲(2)
开发语言·c++·kithara·windows 实时套件·krts
一只自律的鸡29 分钟前
C语言项目 天天酷跑(上篇)
c语言·开发语言
程序猿000001号32 分钟前
使用Python的Seaborn库进行数据可视化
开发语言·python·信息可视化