设计模式
设计模式是一套通用的可复用的解决方案,用来解决在软件设计过程中产生的通用问题。面向对象编程共有23种设计模式,按照其要解决的问题一般被分为3类:
- 创建型(creational):解决如何灵活创建对象或者类的问题,共5个;
- 结构型(structural):用于将类或对象进行组合从而构建灵活而高效的结构,共7个;
- 行为型(behavioral):解决类或者对象直接互相通信的问题,共11个。
创建型:
1.单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
- 预加载:没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。
- 懒加载:不浪费内存,但无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。
Tips: 使用synchronized(同步锁)关键字来保证懒加载的线程安全,但要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了就没有必要再使用synchronized加锁,直接返回对象即可。另外还需要使用关键字volatile保证对象实例化过程的顺序性以保证懒加载的线程安全。
2.原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。不常用,可以理解成深拷贝。用于解决构建复杂对象的资源消耗问题,能在某些场景中提升构建对象的效率;还有一个重要的用途就是保护性拷贝,可以通过返回一个拷贝对象的形式,实现只读的限制。
3.工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
4.抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
- 简单工厂:生产同一等级结构中的任意产品。(不支持拓展增加工厂也不支持增加产品族)
- 工厂方法:生产同一等级结构中的固定产品。(支持拓展增加工厂,但不支持增加产品族)
- 抽象工厂:生产不同产品族的全部产品。(支持拓展增加工厂也支持增加产品族)
简单工厂模式就是建立一个实例化对象的类,在该类中对多个对象实例化。工厂方法模式是定义了一个创建对象的抽象方法,由子类决定要实例化的类。这样做的好处是再有新的类型的对象需要实例化只要增加子类即可。抽象工厂模式定义了一个接口用于创建对象族,而无需明确指定具体类。抽象工厂也是把对象的实例化交给了子类,即支持拓展。同时提供给客户端接口,避免了用户直接操作子类工厂。
Tips: 所以,可以用抽象工厂模式创建生产线,用工厂方法模式创建工厂。也就是用抽象工厂模式定义接口用于创建对象族,再用工厂方法模式定义了创建对象的抽象方法。
5.建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
- 产品(Product):具体生产器要构造的复杂对象;
- 抽象生成器(Bulider):抽象生成器是一个接口,该接口除了为创建一个Product对象的各个组件定义了若干个方法之外,还要定义返回Product对象的方法(定义构造步骤);
- 具体生产器(ConcreteBuilder):实现Builder接口的类,具体生成器将实现Builder接口所定义的方法(生产各个组件);
- 指挥者(Director):指挥者是一个类,该类需要含有Builder接口声明的变量。指挥者的职责是负责向用户提供具体生成器,即指挥者将请求具体生成器类来构造用户所需要的Product对象,如果所请求的具体生成器成功地构造出Product对象,指挥者就可以让该具体生产器返回所构造的Product对象。(按照步骤组装部件,并返回Product)
优点:将一个对象分解为各个组件;将对象组件的构造封装起来;可以控制整个对象的生成过程。
缺点:对不同类型的对象需要实现不同的具体构造器的类大大增加类的数量。
与工厂模式的不同: 建造者模式构建对象的时候,对象通常构建的过程中需要多个步骤,建造者模式的作用就是将这些复杂的构建过程封装起来。工厂模式构建对象的时候通常就只有一个步骤,调用一个工厂方法就可以生成一个对象。
结构型:
6.代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
Tips: 代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
- 静态代理:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。但代理对象与目标对象要实现相同的接口,需要为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
- 动态代理 :相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但它始终无法摆脱仅支持interface代理的桎梏(我们要使用被代理的对象的接口)。
- CGLIB代理: CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但CGLIB创建代理对象时所花费的时间比JDK多得多。对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。
7.适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
- 类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。以类给到,在Adapter里,就是将src当做类,继承;(少用)
- 对象适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。以对象给到,在Adapter里,将src作为一个对象,持有;
- 接口适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。以接口给到,在Adapter里,将src作为一个接口,实现。
8.桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
优点: (1)在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。(2)桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
缺点: 桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
9.装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。
Tips: 装饰者和被装饰者之间必须是一样的类型,也就是要有共同的超类。在这里应用继承并不是实现方法的复制,而是实现类型的匹配。因为装饰者和被装饰者是同一个类型,因此装饰者可以取代被装饰者,这样就使被装饰者拥有了装饰者独有的行为。根据装饰者模式的理念,我们可以在任何时候,实现新的装饰者增加新的行为。如果是用继承,每当需要增加新的行为时,就要修改原程序了
10.外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
11.享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
12.组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
行为型:
13.模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
- 优点:(1)具体细节步骤实现定义在子类中,子类定义详细处理算法是不会改变算法整体结构。(2)代码复用的基本技术,在数据库设计中尤为重要。(3)存在一种反向的控制结构,通过一个父类调用其子类的操作,通过子类对父类进行扩展增加新的行为,符合“开闭原则”。
- 缺点:每个不同的实现都需要定义一个子类,会导致类的个数增加,系统更加庞大。
14.策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
- 主要解决:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。
- 如何解决:将这些算法封装成一个一个的类,任意地替换。
- 优点: 1、算法可以*切换。 2、避免使用多重条件判断。 3、扩展性良好。
- 缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
15.迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。简单来说,不同种类的对象可能需要不同的遍历方式,我们对每一种类型的对象配一个迭代器,最后多个迭代器合成一个。
- 主要解决:不同的方式来遍历整个整合对象。
- 如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。
- 关键代码:定义接口:hasNext, next。
- 优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
- 缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
16.职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
- 主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
- 何时使用:在处理消息的时候以过滤很多道。
- 如何解决:拦截的类都实现统一接口。
- 关键代码:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。
17.状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
- 意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
- 主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
- 何时使用:代码中包含大量与对象状态有关的条件语句。
- 如何解决:将各种具体的状态类抽象出来。
- 关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if…else 等条件选择语句。
- 优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
- 缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
18.观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
- 主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
- 如何解决:使用面向对象技术,可以将这种依赖关系弱化。
- 关键代码:在抽象类里有一个 ArrayList 存放观察者们。
- 优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
- 缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
19.中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
20.命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
21.访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
22.备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
23.解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
创建型
- 单例模式:某个类只有一个实例,且自行实例化并向整个系统提供此实例,确保某一个类只有一个实例存在(真实环境中使用较多)。
需要注意的点:
1.如何保证单例?尤其是多线程环境下如何保证系统中只有一个实例。
2.如何创建单例?
- 懒汉模式:只要不被调用就不创建;
- 饿汉模式:一有机会就创建自己的实例。
3.为什么用单例模式不用全局变量?全局变量可能会有名称空间的干扰,如果变量有重名可能会被覆盖。
常用写法:
1.基于new(最常用)
import threading
class Singleton(object):
_instance_lock = threading.Lock()
def __init__(self):
pass
def __new__(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
# 类加括号就回去执行__new__方法,__new__方法会创建一个类实例:Singleton()
Singleton._instance = object.__new__(cls)
# 继承object类的__new__方法,类去调用方法,说明是函数,要手动传cls
return Singleton._instance #obj1
# 类加括号就会先去执行__new__方法,再执行__init__方法
# —————— 单线程 ——————
obj1 = Singleton()
obj2 = Singleton()
print(obj1,obj2)
# —————— 多线程 ——————
def task(arg):
obj = Singleton()
print(obj)
for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()
2.使用模块(常用)
#singleton.py
class Singleton(object):
def foo(self):
pass
singleton = Singleton()
使用时:
from singleton import Singleton
singleton.foo()
3.基于类
import time
import threading
class Singleton(object):
_instance_lock = threading.Lock()
def __init__(self):
time.sleep(1)
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
# 为了保证线程安全在内部加锁
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
# 未加锁部分并发执行,加锁部分串行执行,保证数据安全
# —————— 单线程 ——————
obj1 = Singleton.instance()
obj2 = Singleton.instance()
print(obj1,obj2)
# 错误示例
# obj1 = Singleton()
# obj2 = Singleton()
# print(obj1,obj2)
# ———— 多线程 ————
def task(arg):
obj = Singleton.instance()
print(obj)
for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()
time.sleep(20)
obj = Singleton.instance()
print(obj)
- 抽象工厂:为创建一组相关或者是相互依赖的对象提供一个接口,而不需要指定他们的具体类。