@qidiandasheng
2021-01-14T10:26:00.000000Z
字数 9386
阅读 1326
架构
在软件工程中,(引自维基百科)设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领域引入到计算机科学的。
设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
简单的一句话表示就是:
设计模式为问题提供方案。
由来:在软件开发中,即使很多人在用不同的语言去开发不同的业务,但是很多时候这些人遇到的问题抽象出来都是相似的。一些卓越的开发者将一些常出现的问题和对应的解决方案汇总起来,总结出了这些设计模式。
本质&目的:设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性、可读性、可靠性。
绝大部分设计模式的原理和实现,都非常简单,难的是掌握应用场景,搞清楚能解决什么问题。
很多设计模式会使代码更加容易拓展,使庞大的代码拆分为职责更大单一,粒度更细的类。但同时也会引入更多的类,使代码的维护成本变高。
所以更多时候我们要根据业务场景来进行一个权衡,而不是生搬硬套来使用设计模式。
每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可以解决哪类问题;第二部分是解决方案,即这个模式的设计思路和具体的代码实现。不过,代码实现并不是模式必须包含的。如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉。
实际上,设计模式之间的主要区别还是在于设计意图,也就是应用场景。单纯地看设计思路或者代码实现,有些模式确实很相似,比如策略模式和工厂模式。
在软件工程中,创建型模式是处理对象创建的设计模式,试图根据实际情况使用合适的方式创建对象。基本的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
创建型模式由两个主导思想构成。一是将系统使用的具体类封装起来,二是隐藏这些具体类的实例创建和结合的方式。
主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。该类需要跟踪单一的实例,并确保没有其它实例被创建。单例类适合于需要通过单个对象访问全局资源的场合。
有几个Cocoa框架类采用单例模式,包括NSFileManager
、NSWorkspace
、和NSApplication
类。这些类在一个进程中只能有一个实例。当客户代码向该类请求一个实例时,得到的是一个共享的实例,该实例在首次请求的时候被创建。
政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。实际上,如果创建对象的逻辑并不复杂,那我们直接通过new来创建对象就可以了,不需要使用工厂模式。
当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。
当每个对象的创建逻辑都比较简单的时候,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
工厂模式的作用有下面 4 个,这也是判断要不要使用工厂模式最本质的参考标准:
提供一个接口,用于创建与某些对象相关或依赖于某些对象的类家族,而又不需要指定它们的具体类。通过这种模式可以去除客户代码和来自工厂的具体对象细节之间的耦合关系。
类簇是一种把一个公共的抽象超类下的一些私有的具体子类组合在一起的架构。抽象超类负责声明创建私有子类实例的方法,会根据被调用方法的不同分配恰当的具体子类,每个返回的对象都可能属于不同的私有子类。
Cocoa将类簇限制在数据存储可能因环境而变的对象生成上。Foundation框架为NSString
、NSData
、NSDictionary
、NSSet
、和NSArray
对象定义了类簇。公共超类包括上述的不可变类和与其相互补充的可变类NSMutableString
、NSMutableData
、NSMutableDictionary
、NSMutableSet
、和NSMutableArray
。
建造者模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。建造者模式的原理和实现比较简单,重点是掌握应用场景,避免过度使用。
以下场景可以使用建造者模式:
我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过set()方法设置,那校验这些必填属性是否已经填写的逻 辑就无处安放了。
如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合set()方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型模式。
在Objective-C
中使用原型模式,首先要遵循NSCoping
协议(OC中一些内置类遵循该协议, 例如NSArray
, NSMutableArray
等)。
在软件工程中结构型模式是设计模式,借由一以贯之的方式来了解元件间的关系,以简化设计。侧重于接口的设计和系统的结构。主要用于处理类或对象的组合。
设计模式中的结构型模式:
为其他对象提供一种代理以控制对这个对象的访问。
这种模式为某些对象定义接口,使其充当其它对象的代理或占位对象,目的是进行访问控制。这种模式可以用于为一个可能是远程的、创建起来开销很大的、或者需要保证安全的对象创建代表对象,并在代表对象中为其提供访问控制的场合。
它在结构上和装饰模式类似,但服务于不同的目的;装饰对象的目的是为另一个对象添加行为(增强原功能),而代理对象则是进行访问控制(跟原功能无关)。
iOS中大量的使用了代理模式,UITableView
,UIScrollView
,AppDelegate
等。
桥接模式有两种理解方式。第一种理解方式是“将抽象和实现解耦,让它们能独立开发”。这种理解方式比较特别,应用场景也不多。另一种理解方式更加简单,等同于“组合优于继承”设计原则,这种理解方式更加通用,应用场景比较多。不管是哪种理解方式,它们的代码结构都是相同的,都是一种类之间的组合关系。
可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能在开发时分别使用。
假如你有一个几何 形状Shape类,从它能扩展出两个子类:圆形Circle
和方形Square
。你希望对这样的类层次结构进行扩展以使其包含颜色,所以你打算创建名为红色Red
和蓝色Blue
的形状子类。但是由于你已有两个子类, 所以总共需要创建四个类才能覆盖所有组合, 例如 蓝色圆形BlueCircle
和 红色方形RedSquare
。
在层次结构中新增形状和颜色将导致代码复杂程度指数增长。 例如添加三角形状, 你需要新增两个子类, 也就是每种颜色一个; 此后新增一种新颜色需要新增三个子类,即每种形状一个。如此以往情况会越来越糟糕。
问题的根本原因是我们试图在两个独立的维度——形状与颜色——上扩展形状类。这在处理类继承时是很常见的问题。
桥接模式通过将继承改为组合的方式来解决这个问题。具体来说就是抽取其中一个维度并使之成为独立的类层次, 这样就可以在初始类中引用这个新层次的对象, 从而使得一个类不必拥有所有的状态和行为。
根据该方法, 我们可以将颜色相关的代码抽取到拥有红色和蓝色两个子类的颜色类中, 然后在 形状类中添加一个指向某一颜色对象的引用成员变量。现在形状类可以将所有与颜色相关的工作委派给连入的颜色对象。 这样的引用就成为了形状和颜色之间的桥梁。此后新增颜色将不再需要修改形状的类层次, 反之亦然。
适配器模式是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
适配器模式的五种使用场景:
封装有缺陷的接口设计
统一多个类的接口设计
替换依赖的外部系统
兼容老版本接口
适配不同格式的数据
装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。
这种模式动态地将额外的责任附加到一个对象上。在进行功能扩展时,装饰是子类化之外的一种灵活的备选方法。和子类化一样,采纳装饰模式可以加入新的行为,而又不必修改已有的代码。装饰将需要扩展的类的对象进行包装,实现与该对象相同的接口,并在将任务传递给被包装对象之前或之后加入自己的行为。装饰模式表达了这样的设计原则:类应该接纳扩展,但避免修改。
装饰是用于对象组合的模式,代码中应该鼓励使用对象的组合。Cocoa自己提供了一些基于这种模式的类和机制。在这些实现中,扩展对象并不完全复制它所包装的对象的接口,虽然具体实现中可以使用不同的技术来进行接口共享。
Cocoa在实现某些类时用到了装饰模式,包括
NSAttributedString
、NSScrollView
、和NSTableView
。后面两个类是复合视图的例子,它们将其它一些视图类的对象组合在一起,然后协调它们之间的交互。通过类别实现装饰模式,如果你是一名iOS开发者,你可能立即会想到用类别来实现。没错,用类别实现可以达到同样的效果,而且会更简单。类别是
Objective-C
的特性,它可以添加类的行为,而不用进行子类化,通过类别添加的方法不会影响类原来的方法,类别也成为类的一部分,并可由其子类继承。虽然通过类别可以实现装饰模式,但是这并不是一种严格的实现,由类别添加的方法是编译时绑定的,而装饰模式是动态绑定的,另外类别也没有封装被扩展类的实例。
外观模式通过封装细粒度的接口,提供组合各个细粒度接口的高层次接口,来提高接口的易用性,或者解决性能、分布式事务等问题。这种模式为子系统中的一组接口提供统一的接口,通过减少复杂度和隐藏子系统之间的通讯和依赖性,使子系统更加易于使用。
NSImage类为装载和使用基于位图(比如JPEG、PNG、或者TIFF格式)或向量(EPS或PDF格式)的图像提供统一的接口。NSImage可以为同一个图像保持多个表示,不同的表示对应于不同类型的NSImageRep对象。NSImage可以自动选择适合于特定数据类型和显示设备的表示。同时,它隐藏了图像操作和选择的细节,使客户代码可以交替使用很多不同的表示。
在生活中很多地方也用到外观模式,比如购买基金,我们从基金机构那里购买基金,然后他们帮我们管理我们的基金,去操作和运行,我们只管购买和卖出就行了,而不用去管他们内部的操作,下面是UML图:
组合模式跟我们之前讲的面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。这里讲的“组合模式”,主要是用来处理树形结构数据。正因为其应用场景的特殊性,数据必须能表示成树形结构,这也导致了这种模式在实际的项目开发中并不那么常用。但是,一旦数据满足树形结构,应用这种模式就能发挥很大的作用,能让代码变得非常简洁。
组合模式,将一组对象组织成树形结构,将单个对象和组合对象都看作树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。
享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
具体来讲,当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。
适用性
一个应用程序使用了大量的对象。
完全由于使用大量的对象,造成很大的存储开销。
对象的大多数状态都可变为外部状态。
如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
应用程序不依赖于对象标识。由于Flyweight对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。
在软件工程中行为型模式为设计模式的一种类型,用来识别对象之间的常用交流模式并加以实现。如此可在进行这些交流活动时增强弹性。
用于描述对类或对象怎样交互和怎样分配职责。
这种模式定义一种对象间一对多的依赖关系,使得当一个对象的状态发生变化时,其它具有依赖关系的对象可以自动地被通知和更新。观察者模式本质上是个发布-定阅模型,主体和观察者具有宽松的耦合关系。观察和被观察对象之间可以进行通讯,而不需要太多地了解对方。
在iOS常用的观察者模式的模块有通知、KVO等。
这种模式为某个操作中的算法定义框架,并将算法中的某些步骤推迟到子类实现。模板方法模式使子类可以重定义一个算法中的特定步骤,而不需要改变算法的结构。
模板方法模式是Cocoa的基本设计,事实上也是一般的面向对象框架的基本设计。Cocoa中的模式使一个程序的定制组件可以将自己挂到算法上,但何时和如何使用这些定制组件,由框架组件来决定。Cocoa类的编程接口通常包括一些需要被子类重载的方法。在运行环境中,框架会在自己所执行的任务过程中的某些点调用这些所谓的一般方法。一般方法为定制代码提供一个结构,目的是为当前正在执行且由框架类负责协调的任务加入具体程序的的行为和数据。
策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式就是由这三个部分组成的。
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。策略的创建由工厂类来完成,封装策略创建的细节。
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
Application Kit框架中包含一个称为响应者链的架构。该链由一系列响应者对象(就是从NSResponder继承下来的对象)组成,事件(比如鼠标点击)或者动作消息沿着链进行传递并(通常情况下)最终被处理。如果给定的响应者对象不处理特定的消息,就将消息传递给链中的下一个响应者。响应者在链中的顺序通常由视图的层次结构来决定,从层次较低的响应者对象向层次较高的对象传递,顶点是管理视图层次结构的窗口对象,窗口对象的委托对象,或者全局的应用程序对象。事件和动作消息在响应者链中的确切传递路径是不尽相同的。一个应用程序拥有的响应者链可能和它拥有的窗口(甚至是局部层次结构中的视图对象)一样多,但每次只能有一个响应者链是活动的—也就是与当前活动窗口相关联的那个响应链。
这种模式提供一种顺序访问聚合对象(也就是一个集合)中的元素,而又不必暴露潜在表示的方法。迭代器模式将访问和遍历集合元素的责任从集合对象转移到迭代器对象。迭代器定义一个访问集合元素的接口,并对当前元素进行跟踪。不同的迭代器可以执行不同的遍历策略。
Foundation
框架中的NSEnumerator
类实现了迭代器模式。NSEnumerator抽象类的私有具体子类返回的枚举器对象可以顺序遍历不同类型的集合—数组、集合、字典(值和键)—并将集合中的对象返回给客户代码。NSDirectoryEnumerator是一个不紧密相关的类,它的实例可以递归地枚举文件系统中目录的内容。
像NSArray、NSSet、和NSDictionary这样的集合类都包含相应的方法,可以返回与集合的类型相适用的枚举器。所有的枚举器的工作方式都一样。您可以在循环中向枚举器发送nextObject消息,如果该消息返回nil,而不是集合中的下一个对象,则退出循环。
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。状态机又叫有限状态机,它由 3 个部分组成:状态、事件、动作。其中事件也称为转移条件。事件触发状态的转移及动作的执行。不过动作不是必须的,也可能只转移状态,不执行任何动作。
状态机的三种实现方式:
第一种实现方式叫分支逻辑法。利用 if-else 或者 switch-case 分支逻辑,参照状态转移 图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简 单、最直接,是首选。
第二种实现方式叫查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
第三种实现方式叫状态模式。对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。请求对象将一或多个动作绑定在特定的接收者上。命令模式将发出请求的对象和接收及执行请求的对象区分开来。
NSInvocation
类的实例用于封装Objective-C消息。一个调用对象中含有一个目标对象、一个方法选择器、以及方法参数。您可以动态地改变调用对象中消息的目标及其参数,一旦消息被执行,您就可以从该对象得到返回值。通过一个调用对象可以多次调用目标或参数不同的消息。
创建NSInvocation
对象需要使用NSMethodSignature对象,该对象负责封装与方法参数和返回值有关系的信息。NSMethodSignature
对象的创建又需要用到一个方法选择器。NSInvocation
的实现还用到Objective-C
运行环境的一些函数。
NSInvocation
对象是分布式、撤消管理、消息传递、和定时器对象编程接口的一部分。在需要去除消息发送对象和接收对象之间的耦合关系的类似场合下,您也可以使用。
目标-动作机制使控件对象—也就是象按键或文本输入框这样的对象—可以将消息发送给另一个可以对消息进行解释并将它处理为具体应用程序指令的对象。接收对象,或者说是目标,通常是一个定制的控制器对象。消息—也被称为动作消息—由一个选择器来确定,选择器是一个方法的唯一运行时标识。典型情况下,控件拥有的单元对象会对目标和动作进行封装,以便在用户点击或激活控件时发送消息(菜单项也封装了目标和动作,以便在用户选择时发送动作消息)。目标-动作机制之所以能够基于选择器(而不是方法签名),是因为Cocoa规定动作方法的签名和选择器名称总是一样的。
当您用Interface Builder
构建程序的用户界面时,可以对控件的动作和目标进行设置。您因此可以让控件具有定制的行为,而又不必为控件本身书写任何的代码。动作选择器和目标连接被归档在nib文件中,并在nib文件被解档时复活。您也可以通过向控件或它的单元对象发送setTarget:
和setAction:
消息来动态地改变目标和动作。
目标-动作机制经常用于通知定制控制器对象将数据从用户界面传递给模型对象,或者将模型对象的数据显示出来。Cocoa绑定技术则可以避免这种用法,有关这种技术的更多信息请参见Cocoa绑定编程主题文档。
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
中介者模式很好的处理了业务中组件化方案的强耦合的问题,我们iOS当中组件化的实现都是基于中介者的模式的。其中的Mediator
起到至关重要的作用,Mediator
就是我们封装的组件化的框架。
这种模式在不破坏封装的情况下,捕捉和外部化对象的内部状态,使对象在之后可以回复到该状态。备忘录模式使关键对象的重要状态外部化,同时保持对象的内聚性。
在iOS常用的实现备忘录模式的模块有归档、序列化、CoreData等。
访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。它能将算法与其所作用的对象隔离开来。
假如有这样一位非常希望赢得新客户的资深保险代理人。 他可以拜访街区中的每栋楼, 尝试向每个路人推销保险。 所以, 根据大楼内组织类型的不同, 他可以提供专门的保单:
- 如果建筑是居民楼, 他会推销医疗保险。
- 如果建筑是银行, 他会推销失窃保险。
- 如果建筑是咖啡厅, 他会推销火灾和洪水保险。
iOS最实用的13种设计模式(全部有github代码)
iOS设计模式详解
面向对象设计的设计模式(一):创建型模式(附 Demo & UML类图)
设计模式