面向对象与设计模式概述

10 篇文章 2 订阅

从宏观理解面向对象的特点:

  1. 提到封装,在身边最常听到的就是为了把功能相似(重复)的代码进行“封装”(伴随着封装往往产生了继承),甚至对类的封装听到的都很少,更不用提抽象层次的封装。封装的目的是使得外部调用只关注使用而非实现过程。但在这里我想强调的是宏观层面的,并不是将功能的实现整理成统一调用的方法和类这么简单,而是通过抽象对类与类或模块与模块之间的关系进行约束。
  2. 对于继承,码农想到的就是子类继承父类,重用父类成员和功能。但是作为架构师,这是远远不够的。继承的目的是为了实现代码的可复用性。但是单纯掌握的父子类之间功能的继承还不足以作为架构层次的能力,很多时候,即便两个类之间有明确的相似或相同的功能实现,我们也不应该使用继承(因为这可能会违背了里氏替换原则)。架构要考虑的是派生或实现对抽象或接口的继承。
  3. 多态性是指实现同一基类或接口的不同对象的同名方法可以具有完全不同的内容。这也是面向接口/抽象编程得以实现的根本。

/*

要提前说明的是,无论设计原则,设计模式,更或是高内聚低耦合这样的设计标准,都应该审时度势的进行分析和使用。实际架构过程中所面对的情况要比任何具体讲解的例子都要复杂的多。高内聚低耦合这样的要求有时候也要为了代码的可读性和开发的便捷性而进行适当的取舍;设计模式的使用往往是多种设计模式协同工作的,单个设计模式,也经常是根据其核心思想和具体的应用环境进行变种的。所以,我们在强调理解和掌握后续内容的同时,更强调灵活的运用,但审时度势的运用必须建立在融会贯通的基础之上。

*/

再谈高内聚、低耦合:

高内聚,低耦合是面向对象设计中判断架构好坏的最重要标准,没有之一。只有符合高内聚低耦合的特点,才能实现代码模块的独立性和可复用性。

高内聚低耦合要求:在横向上减少模块之间的交互复杂度(接口数量,参数个数);在纵向上即层次之间应当实现内容内聚,数据耦合。

有的资料从模块粒度分析高内聚和低耦合,高内聚:类的每个成员方法只完成一件事;低耦合:减少类的内部之间的方法调用。但是,从代码规范来讲,如果方法中存在复杂的逻辑关系如嵌套循环或者一个方法中过多的代码量,都应该对这个方法进行拆分。所以实际架构过程中如何取舍一定是根据具体面对的需求和开发者个人的经验决定的。我个人更提倡在类的内部遵守代码规范的原则,以提高代码的可读性;类与类之间遵守高内聚低耦合的要求,以保证类的可复用性和扩展性。

降低耦合度的方法:

  1. 少使用继承,多使用接口
  2. 模块的功能化分尽可能的单一
  3. 少使用全局变量
  4. 一个定义只在一个地方出现
  5. 避免直接操作或调用其他模块或类的内容,如果确实需要,尽可能使用数据耦合,避免内容耦合。

增强内聚的方法:

  1. 模块只对外暴露最小限度的接口,形成最低的依赖关系。
  2. 只要对外接口不变,模块内部的修改,就不得影响其他模块。

 

但是,我们也需要注意过度解耦的问题。所谓过度解耦就是超出了当前工作需求的解耦,这不仅不能对我们的工作起到积极的帮助,反而会使问题当前的问题复杂化。在敏捷开发中也强调的,不要对未来可能会(就是可能不会)存在的需求考虑过多。在需求出现的时候及时进行的重构和修改,往往更有效。

 

必须理解的几种关系:

依赖(Dependency)是对象之间最弱的一种引用关系,比如公司职员(Personnel.class)使用打印机(Printer.class)的打印方法(Printer.PrintFunction),那么职员和打印机之间就是依赖关系。

关联(Association)是对象之间的一种引用关系,比如运营部(OperationDepartment.class)和研发部(DevelopmentDepartment.class)之间的关系就是关联的。关联的关系可以是单向的也可以是双向的。

组合(Composition)表示contains-a的关系,是一种强烈的包含关系。比如公司(Company.class)和部门(Department.class)的关系,部门不能脱离公司而单独存在。整体控制局部的声明周期。

聚合(Aggregation)表示has-a的关系,是一种不稳定的松散的关系。比如公司(Company.class)和员工(Personnel.class)的关系,员工可以脱离公司而单独存在。

 

面向对象的设计原则:

在学习设计模式之前,必须首先掌握设计原则,因为大部分的设计模式其实不过是设计原则的实现而已。

 

单一职责原则(Single Responsibility Principle):

一个类只有一个职责;如果一个类需要改变,引起改变的理由只有一个。

开放封闭原则(Open Close Principle)

功能实体应该是可扩展但不可修改的。对扩展开放,对修改封闭。或简称为开闭原则。

依赖倒转原则(Dependence Inversion Principle

高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。

抽象不应该依赖于具体,具体应该依赖于抽象。

里氏替换原则(Liskov Substitution Principle)

只有当派生类可以替换掉基类而不影响功能实现时,基类才是真正的被复用了。

最少知识原则(Least Knowledge Principle

也叫:迪米特法则(Law of Demeter

一个对象应该对其他对象有尽可能少的了解,不和陌生人你说话。每个类都应该降低成员的 访问权限。

接口隔离原则(Interface Insolation Principle)

使用多个专门的接口比使用单一的总接口要灵活。

合成/聚合复用原则(Composite/Aggregate Reuse Principle)

尽量适用合成/聚合,尽量不要使用类继承。

 

Gof23种设计模式

设计模式的本质使面向对象设计原则的实际运用,使对类的封装性、继承性、多态性以及类的关联关系和组合关系的充分理解。正确使用设计模式具能够:使程序设计更加标准化,工程化,提高开发效率以缩短开发周期;提高代码的可重用性、可靠性、灵活性、可维护性。(至于提高可读性的前提,是阅读者必须对前面提到的内容有足够扎实的理解)

在具体的软件开发中,必须根据需求来做恰当的选择。对于简单的需求,可能写一个简单的算法要比引入设计模式更符合当前的场景,但对于大型项目或架构设计而言,使用设计模式组织代码显然更有优势。

------------------------------------------------------------------------------------------------------------------------------------------------

对设计模式的分类方式主要有两种,一种是按照作用范围可分为类模式和对象模式两种,另一种是根据设计目的可分为创建型、结构型和行为型三种。在之后的讨论中我们更关注按设计目的分类的方式。分清某个设计模式的具体分类,有利于我们对该模式的理解。

 

根据作用范围分类

类模式:用于处理类与子类之间的关系,这些关系的建立是通过继承来实现的,是静态的,即在编译时就已经确定的。类模式包含工厂方法模式、适配器模式,模板模式、解释器模式。

对象模式:用于处理对象之间的关系,这些关系可以通过组合和聚合实现,是动态的,即在运行过程中是可以变化的。除类模式中的四种模式外,其他设计模式都是对象模式。

 

根据设计目的分类

创建型:用于实现“如何创建对象”和“将对象的创建和使用分离”。

结构型:用于实现将类或对象按照某种方式进行布局以组成更大的结构。

行为型:用于实现类或对象之间的协作,以完成单个对象无法完成的任务,以及进行职责分配。

------------------------------------------------------------------------------------------------------------------------------------------------

 

注意:以下内容不包含设计模式的实现方式及相关内容,只强调各自的优缺点和适用的环境,可以作为初学者的快速查询字典使用。具体实现和注意细节请查阅其他文章的讲解,后续我也会出一些具体环境中的变种和组合的讲解文章。但是肯定不是适合新手学习的正统讲解。

 

单例模式Singleton(创建型)

单例模式是最简单的创建型模式,也是最简单的设计模式之一。类自身负责创建和保存它的唯一实例,并且提供一个访问该实例的方法。

单例类必须满足以下条件:只能有一个实例;必须自己创建自己的唯一实例;其他对象能够访问这个实例

单例类应该注意的一个问题,单例类应当是sealed修饰的,因为派生类可能会生成多个实例。

在Unity中使用单例模式时,继承自MonoBehavior的类不可能从语法上保证其实例唯一,因为这些类可以通过AddComponent方法或者在编辑器面板中直接添加的方式生成实例,所以从严格意义上说,这样的类并不是真正的单例类,要保证他们实例的唯一性,我们只能通过约定的规则实现。

虽然他们不是真正的单例类,但是我们还是习惯把他们称为单例的,因为更多时候我们最重要的的目的是为了全局的更方便的引用。如果我们接收了这点,那么单例在游戏开发中就可以有各种各样的阉割版本。比如最简单的只需要一句代码:

public static Class_A instance;

当然,还是应该强调,这不是真正的单例。

 

原型模式Prototype(创建型)

简单讲就是通过一个实例生成另一个实例。

在复杂的需求环境种,通过new生成对象的方式并不能满足所有需求。当出现一下情况时,可以考虑使用原型模式:

  1. 对象种类繁多,无法整合到一个类中
  2. 难以根据类生成实例
  3. 想解耦框架与生成的实例

Unity中的Instantate方法就是典型的原型模式实现的。

 

工厂模式Factory(创建型)

(简单工厂模式不再GoF23种设计模式内,并且简单工厂在产品种类变化时必须修改工厂类,不符合开闭原则,所以在我这里不予考虑,有兴趣的自行百度。)

工厂模式定义一个用于创建目标对象的接口,并在其子类中决定如何创建所需的对象。当你需要在不同条件下创建不同的实例时,就选择工厂模式。

工厂模式没有特定的应用环境,它只是一种用来创建对象的思想。可以应用在任何系统中。举个例子:剧情的表现方式是非常多的,可以是文字,对话,动画等等形式,但他们都属于剧情的一部分,都受剧情管理。所以在推进剧情时,就需要有生成文字剧情的工厂,生成对话剧情的工厂,生成动画剧情的工厂,因为存在一系列的不同的工厂,所以应该使用工厂模式。

 

抽象工厂模式AbstractFactory(创建型)

抽象工厂提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们的具体实现。——《大话设计模式》

抽象工厂的工作方式是:将抽象零件组装为抽象产品。——《图解设计模式》

上面的两句话能不能理解看自己本事吧,我尝试用更通俗的语言来表达这些内容,但总是不满意,所以标明引用的出处自行查阅吧。

同样是为了生产产品,与工厂模式相比,抽象工厂处理的产品结构更复杂。抽象工厂模式相对于工厂方法模式来说,就是工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂类,而抽象工厂模式是多个产品系列一个工厂类。

 

建造者模式Builder(创建型)

建造者模式将一个复杂对象的构建与它的表现分离,使得相同的构建过程可以创建不同的表现。它主要用于创建一些复杂的对象,这些对象内部构建之间的顺序通常是稳定的,但具体构建的表现却是复杂的。当创建复杂对象的算法应该独立于该对象的组成部分以及他们的装配方式时应该考虑使用建造者模式。

游戏中运用最典型的场景就是根据职业创建角色。创建法师的Builder和创建战士的Builder在流程上是基本一致并且过程稳定的。建造者模式和后面的抽象工厂模式都是用于创建复杂对象,但是从层次而言,抽象工厂关注的是更宏观的层次,即只关心产品的抽象;从处理的需求分析,建造者模式的主要目的则是通过组装不同的零件生成新的产品。

 

代理模式Proxy(结构型)

代理模式为其他对象提供一种代理以控制对这个对象的访问。——《大话设计模式》

这个概念的定义很抽象,然而我也没有自认为的更通俗的语言来表达。很多人说代理就是中介,但我不赞成这样说,因为这容易引起和中介模式的混淆。我认为更恰当的应该是经纪人,因为代理模式起到了增强和保护目标对象的作用。

网页游戏中,当场景里存在大量玩家角色而网络速度不足以在短时间内下载所有角色或为了保证帧率而无法在同一帧内显示所有角色的表现时,就会明显看到那些没有显示完成的角色是一个通用的图标表示。在这背后,就是使用了代理模式的虚拟代理。网页中的多媒体资源的加载也是代理模式的远程代理。控制对目标对象的访问权限时用到的就是安全代理

 

适配器模式Adapter(结构型)

适配器将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本因为接口不兼容而不能一起工作的那些类可以一起工作了。

通常适配器模式应用于对接已有的完善功能,在不改变既有的成熟代码的情况下,使之能够在新的环境中运行。要统一两个或多个在功能上相同或相似,但接口不一致的类的访问方式时,也可以适用适配器模式。

需要强调的是,我们更应该这样来看待适配器:当现有代码在客观条件下不易修改的情况下所采取的临时措施。毕竟多一层包装就多一层理解和维护成本。

 

桥接模式Bridge(结构型)

桥接模式,将抽象部分与它的实现部分分离,使他们都能够独立的变化。——《大话设计模式》

Bridge模式的特征是将“类的功能层次结构”与“类的实现层次结构”分离开了。将类的这两个层次结构分离开有利于独立地对它们进行扩展。——《图解设计模式》

桥接模式是合成/聚合复用原则的实现,将抽象和实现通过合成/聚合的方式联系起来。

如果代码中出现了因为一层一层的继承而导致的过深的功能层次结构,就会导致子类和父类的耦合度变高,在增加新的功能和调整已有功能时难免会设计到基类的修改。这时候,就要考虑使用桥接模式,对代码进行重构了。

 

装饰模式Decorator(结构型)

装饰模式可以动态的给一个对象添加一些额外的职责,也就是为已有的功能添加更多的功能。就增加功能来说,装饰模式比生成子类更加灵活。通过装饰模式,可以让一个类只拥有核心功能,而其他不稳定的装饰性功能就可以分离出去。

游戏中复杂的技能实现可以考虑使用装饰模式,这样可以通过有限的实体类得到N个不同的技能表现。

相较于组合模式,从结构上说,装饰模式的组成是单向链表,组合模式是树。装饰模式是为增加功能时的灵活性。

 

外观模式Facade(结构型)

外观模式在某些资料中也翻译为窗口模式。外观模式通过为复杂的子系统定义统一的访问接口,使得各子系统的调用更加简单。外观模式最直观的结果就是接口变少了。

在以下情况中应该考虑添加Facade类:系统分层之间,比如MVC架构,在各分层之间应该有一个Facade类;需要简化复杂的子系统之间的访问;在处理已经存在的难以修改但又不能舍弃的复杂系统时,也可以通过Facade类简化调用方式。

外观模式和中介者模式在选择时的区别:外观模式是单向的,中介者模式是双向的。

 

享元模式Flyweight(结构型)

Flyweight的本意是指拳击比赛中最轻量级的比赛,顾名思义,该设计模式的目的就是为了让对象变轻。而这里的轻重是指对内存的占用,内存占用越少则对象越轻。实现的思路是:利用共享技术有效地支持大量细粒度的对象。

享元模式不同于其它为了高内聚低耦合而存在的模式,它的核心目标是优化内存开销。

C#中的字符串String就是使用了享元模式。如果一个在功能实现中使用了大量的对象,并且这些对象又有很高的重复性时,就应该考虑使用享元模式来优化内存开销。

享元模式和对象池思想是不同的,对象池中相同的对象可以有多个,但享元模式相同的对象只有一个。所以,共享的实例中只能有绝对相同的属性,比如围棋中的黑白两种棋子,至于棋子所在的位置信息,则需要外部实例进行记录,而对象池中的对象则不需要细分。

 

组合模式Composite(结构型)

能够使容器与内容具有一致性,是一种递归结构的模式。组合模式将对象组合成树形结构以表示部分和整体的层次结构。

组合模式使得用户对单个对象和组合对象的使用具有一致性。最常见的应用就是系统的文件结构,文件夹套文件夹,文件夹里有文件。所有树结构的数据模型,都可以使用组合模式。

 

模板模式Template(行为型)

在父类中定义处理流程的框架,在子类中实现具体处理的模式就是模板模式。

这个模式的结构很简单,只有一个父类和它的派生类,意图也很明显,就是为了规范流程,使得不同的派生类也都要遵循父类的规则。当你的父子类之间不符合模板模式的意图时,你应该基于合成/聚合复用原则,考虑一下这样的父子关系是否有必要存在。

 

策略模式Strategy(行为型)

定义算法家族并分别封装起来,算法之间可以互相替换而不会影响到使用方。

这也是一个结构简单的模式,一个抽象类和它的具体实现。相比于模板模式,策略模式的目的是为了使算法的切换更为灵活,甚至在运行过程中选择具体的算法,抽象接口要做的统一输入输出;而模板模式是为了在父类中制定业务流程。

 

命令模式Command(行为型)

将一个请求封装为一个对象,以实现对请求的排队管理和日志记录。命令模式把请求操作的对象和处理请求的对象分离。

如果游戏中需要撤销或回放,可以考虑使用命令模式。

 

职责链模式Chain of Responsibility(行为型)

将多个对象(Handler)连成一条职责链,使请求得以在这条链上一级一级的进行传递,直到找到能够处理该请求的对象。

职责链使用单向链表的方式进行连接,使得链中的每个对象知道到自己的下一级对象,而不知道整个链的结构。同时也解耦了请求和具体处理请求的对象之间的联系。

职责链在增强灵活性的同时带来了一些其他的负面效果,如处理的延迟性。但游戏中这样有限个数的传递造成的延迟请忽略它,因为任何其他的优化手段所能带来的收益,都要远远高于对这个链表的优化。职责链最值得注意的问题是:一个请求很有可能到了链的末端也没有能够得到处理。这是该设计模式固有的缺陷,但是可以通过定义一个通用处理对象放在链表末端,比如抛异常。

游戏(桌面应用)中玩家操作(触控/输入)的处理就是职责链模式,当玩家点击屏幕后,首先接收到玩家触控的是UI层,如果UI层处理掉了该操作,该操作就不会继续向游戏世界层继续传递。行为树中也有职责链的思想存在,但是行为树的实现方式更为复杂,Handler之间存在多种链接关系,但从宏观上分析,行为树仍然是职责链模式的。

如果在编码过程中存在一个用来处理“接下来该让谁来做什么”的对象时,可以考虑使用职责链。

 

状态模式State(行为型)

通过切换类来改变对象的状态,就是状态模式。用类来表示状态的方式,使得在增加状态时变得更为灵活。

状态模式主要解决的是:当控制一个对象状态转换的条件过于复杂的情况。把状态的判断条件和状态转换的关系转移到各个状态类当中,就可以把复杂的逻辑简单化。

当程序中存在大量的条件分支语句时,就应该考虑引入状态模式。当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,也应该考虑状态模式。

游戏中的状态模式非常普遍,最常见的AI实现方式简单状态机,动画状态机,很多很多。

 

观察者模式Observer/发布-订阅模式Publish-Subscribe(行为型)

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当这个主题对象的状态发生变化时,会通知所有观察者对象,使他们能够自动的更新自己。观察者模式适用于根据对象状态进行相应处理的场景。

游戏中各种各样的消息或事件机制,都属于或包含观察者模式的设计思想。C#中的委托就是基于观察者模式设计的。

当一个对象的改变需要同时改变其他对象时,可以考虑使用观察者模式。

这里只总结观察者模式的设计思想,至于有的人将观察者模式和发布-订阅模式根据细节拆分成两个,在这里取主流说法将他们当作统一个模式。后续详细讲解的文章中可能会进行对比讲解。

 

中介者模式Mediator(行为型)

中介者模式用一个独立封装了一系列组员对象之间交互方式的实体,解耦各个组员对象之间的显式引用。组员对象向中介者报告,中介者向组员对象下达指示。这样在修改交互方式的时候就不会影响到各组员的实现。

将一个复杂的系统拆分后,可以增加各组成部分的复用性,但是形成的多对多的关系,使得各组成部分之间产生相互引用,又降低了各自的复用性。这时候就应该考虑使用中介模式。但是中介者模式中中介类的实现会比每个组员的实现更为复杂,其维护性可能会随着组员的增加而变的非常困难。所以当遇到多对多的引用时,首先应该考虑的是系统设计是否何理。

复杂的系统里都有中介者的影子存在,如回合制战斗系统,必然包含回合控制器,命令选择控制器,战斗流程管理器等等组员对象,这些组员对象又必然相互调用,这时就需要建立一个中介者作为战斗管理器,以协调各功能之间的协作。

中介者模式和观察者模式往往同时存在,中介者处理各组成之间的关系,观察者处理交互的通信方式。

 

迭代器模式Iterator(行为型)

提供按某种顺序访问聚合对象中的各个元素,而又不暴露该对象内部实现的方式。

对于迭代器我们更重要的是掌握其实现的思想,因为高级语言中都已经提供了迭代器的封装供我们使用,我们不再需要自己实现迭代器,比如对集合或字典的遍历。

 

访问者模式Visitor(行为型)

在Visitor模式中,数据结构与处理被分离开来。用于解决稳定的数据结构和易变的操作之间的耦合问题。这也是最复杂的一个设计模式,而且缺点很明显,不支持对数据结构的修改,且没有办法规避。所以除非特别符合的场景,不建议使用。

但是,对访问者模式的研究,更容易体会在之前所说的审时度势的理解。因为访问者模式在设计时,本身就违背了迪米特法则,也违背了依赖倒转原则,但在合适的情况下,访问者模式仍然是一个经典的设计模式。

 

备忘录模式Memento(行为型)

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样就可以将该对象恢复到原先保存的状态。

通过建立一个Memento的集合,就可以实现保存各个时间点的对象状态。单机游戏中常用的存档,就应该使用备忘录模式。如果在系统中使用了命令模,并且需要实现命令的撤销功能时,就可以使用备忘录模式来存储可撤销操作的状态。

 

 

解释器模式Interpreter(行为型)

把自定义的字符串按照你定义的规则,翻译成为其他可执行的命令,就是解释器需要完成的工作。——Hello Mingo

上面的表述直观但是并不非常准确,只作为简单理解的方式。

“给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。”

最常见的解释器应用场景有:编译器,SQL解析,正则表达式等。在工作过程中,真正需要自己实现解释器的情况并不多,我唯一用到的情况是,游戏中的属性和计算公式通过配置实现时,因为代码中不再知道有什么属性,也不知道某个操作的计算方式,这些信息全部来自于配置信息,所以需要将配置信息翻译成可以执行的程序代码。

 

结语:

在前面的内容中,反复强调的审时度势的选择,并不是放弃学习设计模式的理由。我们没有任何理由放弃对设计模式的研究,就程序员自身而言,学习设计模式,可以调高我们的编程能力和设计能力。然而,在我的身边却存在着大量的程序员,对设计模式的了解仅仅局限在单例模式和工厂模式这样的水平上,甚至连自己代码中无处不在的模板模式都不自知。我想,对理论学习的恐惧,就是他们工作五六年以后甚至更久,仍然还是低级程序员的重要原因吧。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值