何为面向对象?
在谈及设计模式之前,我们先谈谈面向对象,在面向对象之前先出现的是面向过程的编程,其中具有代表性的是C语言。面向过程更加注重逻辑过程,根据输入进行运算处理并输出。通过阅读程序可以较清晰的看出代码执行的操作和解决问题的过程,在一定程度上可以说面向过程编程只要代码无错就是优。但是随着社会对计算机软件的需求日益增长,经常会出现一个个新的需求,即现有的系统需要增加新的功能,而有些功能中依赖的算法在其它现有功能中也有运用到,这就可能会导致代码冗余,此外一个函数接口可能被赋予了多种职责(作用),在调用和修改过程中容易导致其它模块出现不可预期的问题(这就是所谓的紧耦合现象)。那么如果可以将程序中的一些共有属性操作进行一个提取并整合在一起利用,每个整合的模块只提供简单直接的作用,在进行新功能开发时,能够降低模块之间的依赖,独立编写新的模块而互不干扰,实现紧耦合向松耦合的转变,可能更利于开发。很快面向对象就出现了,面向对象就类似方法论,对解决问题的方法进行总结分析。对象可以理解为要解决的问题中出现的成员或者是用于解决问题的方法依赖的媒介,通过构造对象模型,使这个模型能够更加契合问题域,通过定义对象的成员方法来解决问题。(这里并没有对面向过程有偏见,面向过程与面向对象各有各的优势)
重识面向对象
1. 何为对象?
- 从语言实现层面,对象封装了代码和数据;
- 从规格层面,对象是一系列可被使用的公共接口;
- 从概念层面,对象是某种拥有责任的抽象;
2. 从宏观层面来看,面向对象的构建方式更能适应软件的变化,能够将变化带来的影响减为最小(理解隔离变化);
3. 从微观层面上看,面向对象的方式更强调各个类的“责任”,由于需求变化导致的新增类型不应该影响原有类型的实现(各司其职);
什么是设计模式?
当面向对象出现后,大多数开发者能够较好地实现程序模块的解耦,但是在实际开发过程中还是可能会有一些模块存在较高的耦合度,这会提高软件设计开发的难度。一个好的软件设计体现在模块之间的耦合程度低,代码复用率高。市场需求的增长使得软件越来越复杂多样,这就是软件设计固有的复杂性。其根本原因是变化,这种变化的来源有很多,例如客户需求的变化、技术平台的变化、开发团队的变化、市场环境的变化等等。
那么如何解决软件设计的复杂性呢?主要从两方面着手
- 分解:将大问题分成小问题,整个流程分为子流程,复杂问题分解成多个简单的问题;
- 抽象:由于不能掌握全部的复杂对象,我们选择忽视它的非本质而去处理泛化和理想化的对象模型;
这里的抽象讲的的确挺“抽象”的,这里涉及到两个概念,即底层思维和抽象思维。
底层思维(向下):如何把握机器底层从微观理解对象构造,这也可以说是开发人员在软件开发过程的编码阶段的惯有思维方式,主要体现在语言构造、编译转换、内存模型以及运行时机制;
抽象思维(向上):如何将我们周围世界抽象为程序代码,即将一个实际场景的问题转换成程序进行处理,主要体现在面向对象、组件封装、设计模式以及架构模式;
设计模式就是运用抽象思维来解决软件设计复杂性的一方法套论,抵御软件设计的变化!
克里斯托弗·亚历山大曾提出:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复运动。”
这虽然是针对的建筑设计模型提出的,但也很好地应用到了计算机软件设计领域中。可以看出设计模式从某种角度上看就是为了实现代码的复用性,提高效率,降低开发的成本。
面向对象设计原则
在设计一个类时我们需要将抽象对象的共有属性和方法进行提取,但如果一个类承担的职责过多,这些职责之间就具备了高耦合度,可能一个职责的变化会影响甚至破坏另一个职责的作用,类的设计就遭到了意想不到的破坏,例如类A有两个职责D1、D2,修改了D1后,D2就无法履行应有的职责。这就表示在设计类的过程中要尽量减少一个类应有的功能。
“开放”指的是对扩展开放,“封闭”则是对更改封闭。对于现有的类不能够修改类内部的实现,即在类内修改、增加或是删除成员变量和成员函数,对于已经定义好的成员函数不能进行修改具体实现(有bug的例外);当新的功能需求需要开发时,只能通过新增代码模块来实现,例如新增接口函数,新增派生类等,从而避免由于类之间的耦合性导致代码修改带来的不可预期的错误。
“高层模块”指的是客户操作的功能模块,“底层模块”则是系统实现细节上划分的模块,“抽象”则是实际问题的泛化模型。这个原则实际是就是指要针对接口编程,而不是针对实现编程。例如在实际开发中,为了减少代码的冗余,提高代码的复用率,我们会将常用的代码写成函数库再调用。当在开发另一个项目时,需要相同的函数功能时,可以直接调用原先的函数库进行操作,从而提高代码的复用性。再比如一个系统如果将界面和底层实现写在了一起,当需要修改底层实现时可能也需要修改界面,当需要修改界面时也会修改到底层的实现。
如果一个软件实体使用一个父类的话,那么一定适用于子类,而且它察觉不出父类对象和子类对象的区别,即子类替换掉父类,程序的行为没有发生变化。这是这个原则使得继承复用扩展变为了可能,子类继承了父类的属性方法,并在这基础上进行扩展(增加新的属性方法),使得父类真正得到了复用,而不需修改父类的类模块。
该原则指的是降低类模块之间的耦合程度,实现模块之间的松耦合。其前提是在类的结构设计上,应该限制类外部对类内成员的访问权限(设置为private),一个类的修改不会影响到另一个类的功能,使得每个类尽量独立,低耦合度将有利于代码的复用。
接口一般是被继承的公有属性和方法。客户端若依赖了它不需要的接口,会给客户端带来额外的耦合度,而这种耦合度没有带来任何有用之处。定义的接口应该小并且完备,提供细粒度的操作,这符合之前提到的单一职责原则,提高类的内聚性,降低类之间的耦合性。
之前提到了里氏替换原则,这是针对使用到继承时需要遵守这个原则。而在继承中,子类复用了父类的属性方法并进行扩展,这种复用方式被称作“白箱复用”,父类的实现细节对于子类来说是完全可见的,这破坏了类的封装性。此外父类与子类之间耦合度高,如果父类进行了一定的修改,子类也会受到一定的影响。而组合/聚合则是在类A内定义一个类B的成员对象,对于类A的来说,类B的实现细节是不可见的,因此被称作“黑箱复用”。类A只能通过类B的公用方法来访问类B,两者之间耦合度低,更加灵活。
GOF-23 设计模式
现代软件设计的特征是“需求的频繁变化”,设计模式的要点是“寻找变化点”,然后在变化点处应用设计模式,从而更好地应对需求的变化,在学习设计模式中“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。
设计模式从目的上可以分成三类
- 创建型模式:将对象的部分创建工作延迟到子类或者其它对象,从而应对需求变化为对象创建具体类型的实现带来的影响;
- 结构型模式:通过类继承或者对象组合获得更灵活的结构,从而应对需求变化为对象结构带来的影响;
- 行为型模式:通过类继承或者对象组合来划分类与对象之间的职责,从而应对需求变化为多个交互的对象带来的影响;
上面的设计模式在后续的文章中会挑选一些进行详细讲解,在第一次学完这些设计模式后,可能会出现各个模式之间界限模糊的情况。需要在多实践多巩固才能较好地掌握。
在利用设计模式进行重构时可以参考以下几点关键技法:
- 静态 → 动态
- 早绑定 → 晚绑定
- 继承 → 组合
- 编译时依赖 → 运行时依赖
- 紧耦合 → 松耦合
学习设计模式可以参考以下资料:
1、《大话设计模式》,这本书通俗易懂,大多以实例来讲解
电子书的下载链接在这,https://blog.csdn.net/hemingyang97/article/details/82025608
2、李建忠老师的《C++设计模式》课程
附B站地址:https://www.bilibili.com/video/BV1kW411P7KS?from=search&seid=10792673449812940500