这篇博文的主题是什么?
介绍了和面向对象相关的基础概念。本文的定位是只解释概念,因此看起来有点务虚。但依笔者拙见,基础概念是极其重要的,如同一座楼房的地基,若对基础概念的理解有偏差,对高层建筑的理解也不会好到哪里去,如同浮沙之上建高楼。
在现实世界中,科学哲学(以波普尔为代表)指导科学,作为科学的物理学指导机械学,机械学指导制造出锤子,然后出现了锤子使用手册。类比之,在程序世界中,某种抽象的理念指导着计算机科学,计算机科学指导着软件工程,软件过程指导制造出spring框架,然后出现了spring框架的使用文档。我始终认为,一名开发者要想对专业知识有着深入的理解,则必须要沿着这条链往上走。
注:本文是第一版,汇聚了我对这一块的系统理解,会随着我的知识、经验增长不断更新。
1. 什么是面向对象
OOP属于一种编程范式,所以它本身是一种设计思想。拥有OO特性则为面向对象编程语言,语法上一般会提供类、封装、继承等语法。所以,OOP其实涉及到两个概念,一个是OOP设计思想,它是程序员理解程序、设计程序的一种思维方式;另外一种是OOP语言,它提供语法支持来帮助程序员方便的用程序代码去表达、实现OOP的这种设计思想。总结一下OOP的几个重要部分:
类:将数据和操作数据的方法打包在一起作为一个整体,叫做类。类中的数据叫成员变量、属性,类中的操作叫成员函数、方法。类只是定义一种结构,我们创建的类的实例才是真正被分配有内存空间、存储数据的东西。不同的实例包含不同的数据,所以调用相同的方法,这些方法对不同的类内数据操作,也许会得到不同的结果。
封装:将一部分成员变量、函数作为内部实现隐藏起来(private、protected),而另外一部分成员变量、函数则对外公开(public)。
继承:从一个现有的类派生出新的类,派生类自动拥有基类的部分成员(public、protected),还能够添加自己的成员甚至是修改基类的方法。
多态:派生类能够修改(Override)基类的方法,当基类指针或引用指向派生类时,实际调用的是派生类的方法。若基类指针指向不同的派生类对象,则会表现出不同的行为,这就是多态。
其实程序设计领域出现过很多编程范式,最著名的三种就是面向过程编程、面向对象编程、函数式编程。这引起了我的两个思考。
第一,为什么会出现这么多编程范式?个人拙见,之所以出现这么多编程范式,是因为任何一个单一范式是无法解决所有问题的。一种范式的发明有着特定的问题背景,也适用于一定的适用范围。比如面向对象编程一开始就是为了解决GUI编程的问题被发明的,所以面向对象是最适合编写图形界面的。
在我初学面向对象时以为任何问题都能够用它优雅的抽象,后来在接触了其它的编程范式后,才发觉自己以前的思路太狭窄。技术没有银弹。
第二,为什么现在最流行面向对象编程?个人拙见,面向对象一来是容易从业务实体的角度进行抽象,这种抽象易于理解;二来面向对象中的接口给团队合作带来一种规约,封装界定了团队合作中每个人的职责范围,所以面向对象用于大型项目中很容易多人协作。
2. 从面向过程的视角看待面向对象
2.1. 面向过程是什么?
从初学程序设计的第一天起,老师、教材就会告诉我们一句话:
算法 + 数据结构 = 程序
这是Pascal之父Niklaus Wirth提出了,这句话让他获得了图灵奖。(注:wiki上没看到图灵奖的资讯,但中文世界的各博客上都这么说)将这句话衍生一下,任何一个程序,都可以拆分成一个【运算部分】和一个【存储部分】。
如果从硬件的角度来看待,运算部分就是在CPU中执行的指令,存储部分即是在内存中保存的数据。在IT飞速发展的今天,互联网公司大量运用的分布式技术中也有这一理念的影子。当从物理部署的角度上看待分布式系统时,我们会将分布式系统分为无状态的运算集群(如java编写程序启动的进程),和专门存储数据的存储集群(如Redis
集群,MySQL
集群)。即使是这几年炒得比较火热的区块链技术,也能看到这一理念的影子。在可编程的区块链ETH
上,某种角度上也可以分为两个部分,第一个部分是作为运算部分的智能合约,第二个部分是作为存储部分的链上数据。
这么多年过去,Pascal之父的这句话对现代的各种程序仍然适用,这证明这句话的确揭示了有关程序的普世特点。
首先出现的面向过程编程,其程序观主要有两点:
- 第一点就是Pascal提到的,将程序看成数据+代码。如面向过程语言C语言中,代码部分就是一个个的函数完成,数据部分有的定义在
static
上,有的分配在堆中等。 - 将实际问题的步骤按照“自顶向下,逐步求精”的理念拆分,将一个大的问题分解成几个更小的问题,然后对更小的问题进行求解,反而治之,逐个击破。
很多时候有一种误解,认为过程式语言没有封装。虽然封装是面向对象中的特点,但不代表面向过程中没有封装。比如说作为过程式语言的C语言,有对代码的封装机制,就是C语言中的函数,也有对也有对数据的封装机制,就是struct
结构体,它能够将一组数据打包成一个整体以方便使用。
2.2. 面向对象比面向过程多了什么?
上面说的面向过程程序观的两个特点,稍微一琢磨,就发现其实在面向对象中也存在。因此,个人拙见,面向过程和面向对象不是互斥的东西,面向对象是在面向过程的基础上做了些衍生。体现在以下方面:
一、面向过程程序中,逻辑上程序分为数据部分和运算部分,实际的源文件中数据部分和代码部分却是分离的、割裂的。我们知道,有很多代码是对数据的紧密关联的操作 、变换,将这紧密关联的两个东西如果在源文件里的距离比较远,势必会影响程序的内聚性。
面向对象编程就做了改进,设计出叫做“类”、“对象”的东西,把面向过程的程序里的数据与和其紧密关联的函数打包到一个实体中去,并将它命名为类。按照这个思路,对于C语言里的struct
结构体,如果把和结构体里面数据密切相关的函数以函数指针的方式一起打包到这个struct
中去,就有点OOP中类的内味儿了。
但在一个类的内部,我们还是要区分数据部分和运算部分,前者叫属性,后者叫方法。
二、面向对象中一块逻辑的具体实现,也是需要遵循面向过程中“自顶向下,逐步求精”的理念。否则,就会看到这样的代码,在类的层次划分得很优雅,但打开类里面的函数实现一看,所有的代码堆积在一个函数中,臃肿而不堪。因此,对于开发者来说,掌握面向过程编程是掌握面向对象编程的基础。
三、虽然面向过程中也有抽象和封装,但仅局限于函数层次,而面向对象能够在类的层次进行抽象。首先,对象给类型、数据访问添加了一层间接抽象,一种数据的内部表示形式可以有多种,一种类型也可以有多种不同的实现,类层次的抽象就能将实际使用的那种隐藏掉。
不过,在《Code Complete》中提到一种称为ADT抽象数据类型的概念,拥有面向对象中类封装特性的一部分。面向过程语言也能表达出ADT的概念。
四、面向对象编程中增加了继承和多态机制,这是面向对象所特有的。恰当利用这两种机制,能够对实际问题写出更简短、更清晰的代码。
3. 面向对象的三大特性
3.1. 封装
3.1.1. 什么是封装?
看过一些资料,很多人将封装理解为只暴露出提供给外部使用的接口,将其它信息隐藏以来。个人拙见,这个理解只解释了封装概念的一方面。
在书籍《Code Complete》中,解释“封装”的时候,还提到了相关的其它两个概念,抽象和信息隐藏。因此,抽象、封装、信息隐藏是三个密切相关的概念,互相交织,不应该割裂开来看待,也不应该将这三者混淆。这里总结下《Code Complete》中对这几个概念的解释:
抽象:从更高的层次、以一种更简化的方式来看待复杂的事物。例如房子,远看是一栋建筑物,近看是门、窗口、家具等组件的集合体,再拿放大镜看发现都是铁、木头、油漆等各种材料,最后拿显微镜看发现都是分子。类似的,同样一片内存的组织体,远看它是一个List<T>
,能存顺序元素的容器,近看发现它是指针、小块内存、数据指针等元素的集合体。
从List<T>
的角度将这片指针、小块内存等的复杂聚合体看成一个简单的能存数据的容器,能大大降低心智负担。这就是抽象。
封装:标识出允许外部使用的成员函数、数据,区分出内部和外部,调用者只能使用外部的东西,不能看见内部的东西。为什么要区分内外?因为封装是为抽象服务的。
信息隐藏:信息隐藏大都有具体的语言机制(如java的private关键字)或规范约定(如python约定下划线开头的方法是非public的)支撑。信息隐藏将被标识为内部的成员函数、数据隐藏起来。所以信息隐藏是为封装服务的。
3.1.2. 抽象、封装、信息隐藏有什么好处呢?
有以下好处:
确定各单位的边界。每个类、模块被封装后其职责明确,禁止它们越俎代庖插手不应该干涉的事情。
隐藏变化源。将易变化的区域和不易变化的区域隔离开来。类的实现者可以修改被封装的东西,而不会影响到外部调用者。如我们有一个方法先是使用冒泡排序实现的,后续优化性能将函数内部用快速排序重写,但调用者的代码无需变动。再如面向对象设计中的依赖倒置原则,要求高层依赖于抽象接口,低层可接入任意满足抽象接口的实现。更换低层实现对高层的代码没有影响。
隐藏复杂度。将和接口无关的东西隐藏起来,调用者只需要关心接口即可,无需关心内部实现。人脑的注意力资源、工作记忆极其有限,如果同一时间需要关注的东西太多以至于超出人类生理局限许多,那这个系统就会因为极难维护而濒临失控。
团队协作中,提供一个良好的合作基础。外部的调用者只和公开的接口耦合,而不会和内部的成员函数、数据相耦合。这样,两个开发者、甚至是两个团队,他们之间的边界是清晰分明的。只有一方对接口(边界)做了变动才会影响到另一方;而若一方只对自己模块内部做变动,另外一方根本就不需要知道这一信息,也不需要做任何同步修改,这有利于提升团队的合作。
3.1.3. 怎么样做出良好的封装?
想要知道什么是良好的封装,就要先知道什么不是良好的封装。由于抽象、封装、信息隐藏这三个概念密切联系、互相交织,所以一个很常见的误解是将信息隐藏等同于封装。按照这种思路,在设计一个类的时候,将需要给别人调用的设计为public
,而想让别人知道、可能修改的函数、数据设计为private
。
本科时我刚入门面向对象设计后一段时间就是持有这种粗浅的观念编写程序,但在实际编写程序后总感觉到处处不顺手。其实,这种思路最大的问题是没有结合真正的需求去思考,到底哪些是该隐藏的,哪些是不该隐藏的?信息隐藏只是一个战术操作,而结合需求进行深入分析的抽象才是核心的战略问题。借用《Code Complete》中的一句话:
不懂ADT的程序员开发出来的类只是名义上的类而已—-实际上这种”类“只不过是把稍微有点儿关系的数据和子程序堆在一起而已。
假设没把需求考虑全面透彻就开始着手写一顿噼里啪啦敲打private void xxxx()
,到时候发现写出来的类满足不了功能、或者发生了之前没有意料到的需求变更,你的调用者就发现用你的public
接口根本没法实现需求啊,怎么办啊,他就只能一边吐槽着类的编写者一边用一些奇葩怪异的hack方式来访问被隐藏起来的细节。这些hack代码,难以理解维护,稍不注意就容易出现诡异的bug与灾难性的后果。
到头来,你还是要把这个private
改成public
。想要真正杜绝外部调用者对内部细节的访问,就必须要做到功能完备。
良好的封装,有如下特性:
- 对需求进行深入而全面的思考,做出了良好的抽象。一定是结合需求、实际问题的思考,脱离了实际问题来谈抽象就是空中楼阁。
- 外部调用者想要的任何功能,都能够通过你的公开方法做到,这是功能完备,如果更进一步,做到“完备且最小”,则是更完美的设计;外部调用者使用你的类时完全意识不到内部细节的存在。这叫对外透明。
紧扣实际问题建模与功能完备性,是良好封装的两个必要条件。至于“最小”的原则,则是奥卡姆剃刀“如无必要,勿增实体”理念的体现。
思考:在实际的开发体验中,功能完备是较容易做到的,但“最小”原则就比较难了,在本科学习C++的时候很多经典类的实现都能体现”完备且最小“原则带来的美感,但后来发现在java中很多类似乎并不关注该原则,提供的方法也有大量冗余。
也许我们可以换个角度,将实际的类设计看作一个“完备且最小”的核心子集加上一组拓展函数的衍生。拓展函数的存在是为了更方便开发者调用。
”最小“原则虽然容易被忽视,但它的重要性却非同寻常。我们用逆向思维来看待这个问题,以java的流API为例,代表流源头的输入流有文件流(FileInputStream
)、网络流(SocketInputStream
)、存储于内存的流(ByteArrayInputStream
)这3种,还有一个流装饰器BufferedInputStream
。
如果让一个没有学习过程序设计知识的人来设计,它可能会设计出3种不带Buffered特性的类,然后再设计出带3种Buffered特性的类,一共6个类。但是如果遵循”最小“原则进行深入思考,会发现这个Buffered
特性在与其它特性相正交的,将它单独抽取出来,设计复杂度就立刻从乘法(3 x 2)变成加法(3 + 1)。
疑问:正交性从直觉上似乎应该是程序设计中有力的一大思维工具,但我涉猎较浅,目前没找到哪里有资料详细分析它。
3.2. 继承与多态
3.2.1. 什么是继承、多态?
继承是指从一个现有的类型(基类型)派生出新的类型,新的类型自动拥有基类型的所有成员,并且也能够添加新的成员。在所有使用基类型的地方,都可以用派生类型去代替。
这是标准的解释。但如果细细深究起来,这个代替怎么理解呢?
思考:本科时初学面向对象时,第一次读到“代替”这个词,以为是把用到基类型的代码,Ctrl-c Ctrl-v替换成派生类型。往下读下去看到进一步解释时联想起自己之前的猜测不禁笑了。
我们试着看一段代码:
|
|
getSize
函数的参数是List
,在程序员编写这段程序的时候,对该参数了解到的信息只知它是一个List
,这是它的静态类型。当这段程序运行起来后,这个list
参数被传参一个ArrayList
对象,ArrayList
是它的实际类型。静态类型是代码里编写的类型,在编译期间就能确定,而实际类型是经过程序运行到该代码处才能确定的,一般要到运行期间才能确定。
所以,“代替”的内涵指的是,所有静态类型为基类型的代码处,在运行的时都可以指向实际上是派生类型的对象。
至于多态,它指的是,如果新类型想修改掉基类型中方法的实现(Override
),看似长得一样的代码(如上面的getSize
函数),运行起来后,先后被执行两次且传递了不同派生类型的对象,那这两次实际上会执行两个不同版本的代码。
3.2.2. 继承、多态有什么好处呢?
在《Effective C++》中将继承分为三种,这三种继承看似相似,但其内涵却大相径庭。如下:
- 不继承实现,只继承方法接口:纯虚函数。在java中,就是实现接口,java中一个类可以继承多个接口。本文称为接口继承。
- 继承方法接口,以及默认的实现:虚函数。在java中,就是
Override
父类的方法,java中只能继承一个类的实现。本文称为实现继承。 - 继承方法接口,以及强制的实现:普通函数。在java中,就是不修改任何父类的实现(父类方法被
final
关键字修饰或者纯粹程序员自觉遵守规范),仅仅是自己添加新的方法。这种方式由于没有Override
,因此不涉及到多态。本文称为扩展继承。
下面分别讨论。
1)接口继承的好处。
主流的面向对象语言,都允许类继承多个接口并鼓励这种做法。我们知道,在生活中,一个事物,根据实际所处的背景,会有不多的属性划分和标签定义。举个例子,一个17岁的少年,在学校的身份是学生。对老师而言,他面对一个班级的包括少年在内的形形色色的人,他无需关心这些人在其它方面的差异,只需要关注他们作为学生身份的这一个方面即可;去医院,这个少年的身份是病人,对医生而言,他面向病房里的一堆人,根本无需关心他们的民族、宗教信仰、文化背景这些方面,只需要统一的关心他们作为病人身份的这一个方面即可。
以java中的LinkedList
为例,它实现List
接口,说明它的一个身份是一个可以顺序容纳元素的容器;它还实现了Queue
接口,说明它的身份是一个有头有尾的队列。人在生活中有多个身份,LinkedList
在程序世界里也有。
下面是Collections
中的一个函数,交换List
中的两个元素。程序中有很多种完全不同的对象,有的是ArrayList
有的是LinkedList
甚至还有第三方库里定义的对象,但没有关系,只要他们实现了List
接口,swap
函数的开发者就不需要关注他们的差异性,而只需要关注他们作为List
这一投影面的共性。
|
|
LinkedList
还实现了Queue
接口,所以,这个LinkedList
还可以和其它的各种各样的Queue
对象放在一起,在程序的某个地方,被一段代码不加区分的进行处理。
因此,接口继承的优点是,能够一视同仁的处理实现了特定接口的所有对象,开发者只需关注实际对象的一个方面而不需要关注所有方面,就能够大大降低程序的复杂度与开发者的心智负担。
2)实现继承的好处。
java语言中只支持继承一个基类型的实现,C++支持多重继承但该特性经常被批评。即使是单继承,也经常在有些资料中听到“慎重使用继承,尽量用组合来替代”。
这是因为,很多人将实现继承作为代码复用的手段,但这会导致子类与基类出现强耦合。如果父类的逻辑被修改,那么所有的子类都会被影响。之前提到,封装的一个好处就是解耦、隔离变化,这样一来,继承若是有得不恰当则会破坏封装。
如果仅是复用代码,低耦合的代码复用的方式有很多,何必用继承这种危险的方式呢?在实际的编程中,我常用的方法有两种:
- 如果要复用的是一段代码,不涉及成员变量的状态复用,那其实很简单,我宁可在包下加一个专用的
Utils
类,将其作为静态的工具方法加进去。有时候恰好这个工具方法足够通用,我就考虑将它移动到公用的Utils
工具类中。 - 如果涉及到成员变量的状态复用,那考虑将这些状态和相关的方法封装成一个辅助对象,再让大伙儿去组合这个辅助对象。这其实就是“组合”的思路。
思考:在我初学面向对象编程时,我以为java中的
class
继承特性是很重要的特性,而interface
似乎没什么卵用。当真正在阅读了优秀资料和书籍,经过自己的编程经验淬炼后,我现在的观点完全反过来了。如果把java中
class
的继承特性去掉,我还是能够写代码只不过要绕一点;但是如果把java中的interface
特性去掉,嗯,那这代码就完全没法写了。
可以想象一下,如果将java中class
的继承特性去掉,class
之间不能相互继承,考虑下jdk
的集合模块会怎么写?
个人拙见,实现继承的最佳方法,是将其当成可以写方法默认实现的【接口继承】使用,该继承的维度可标识该类的主分类纬度(因为只能继承一个父类)。
子类若比较懒,可直接拿父类的默认实现来用,子类若觉得默认实现不能满足要求,就将其Override
但是不用super
关键字调用父类版本。这样父类和子类之间的耦合就不太大。
思考:java中的
List
接口,会搭配一个AbstractList
抽象类来使用。这个抽象类的设计初衷其实就是提供部分方法的默认实现,来方便使用者。
3)扩展继承的好处。
扩展继承不改变父类的行为,仅仅是额外添加一些自己的数据和方法。我们也分两种情况分析:
一、仅额外添加自己的方法。如果这些方法需要访问protected
的成员,那这个方法和父类的耦合就比较紧密;如果这些方法仅需访问public
成员,那其实可以有替代方案,将这个方法放入Utils
中作为静态的辅助函数。作为辅助函数会更通用一些,如果你要对一个现成的父类对象执行该操作,这时使用扩展继承是没用的,但辅助函数却能派上用场。
二、额外添加自己的数据(成员变量)。由于要额外保存数据,所以这种需求用扩展继承实现最方便。
思考:可以试想,假如不用扩展继承,这种需求的实现,只能将这些额外数据和操作封装为一个辅助对象,并设计一个
HashMap
以该对象的某个独特id为key,以这个对应的辅助对象为value,且要处理好生命周期的问题,如果主对象生命周期结束辅助对象也要跟着回收。这在C++里可用析构函数做到,但在java里基本上没有一个靠谱的方案。
因此,扩展继承总得来说,最有价值的还是上面的第二种需求。
4. OOP设计的5大原则(SOLID)
这里对这5大原则做个总结。这五大原则分别是:
首字母 | 指代 | 概念 |
---|---|---|
S | 单一功能原则 | 对象应该仅具有一种单一功能 |
O | 开闭原则 | 程序对扩展开放,对修改封闭 |
L | 里氏替换原则 | 程序中子类对象可替代基类对象而不改变程序正确性 |
I | 接口隔离原则 | 多个功能单一而小巧的接口,好于一个泛而大的接口 |
D | 依赖反转原则 | 依赖于抽象而不是一个实例 |
单一功能原则就是指的每种对象只做一件事情。我们知道另外一个更基本的原则是“高内聚、低耦合”,单一功能原则是基于“高内聚”的理念的。
开闭原则的几个常见场景:一、设想你编写了一个第三方的库,并有扩展功能。库的调用者如果要扩展库的功能,他又无法直接去修改库的源码。所以库的扩展特性必须要遵循开闭原则。
二、一个大的项目由好几个人分工合作编写。你编写的模块供你的同事调用。如果你的同事想使用你的模块却感到碍手碍脚,不能通过public的接口完成,还必须去修改应该由你负责的代码,这是不合适的。
里氏替换原则是对子类型的特别定义,派生类(子类)对象可以在程序中代替其基类(超类)对象,不改变程序正确性。这个原则指导继承的正确运用。
思考:这说明面向对象中的继承,子类是(is)父类,这个“是”和日常生活中的含义根本不一样,有些继承设计虽然满足生活中的概念定义(子类内涵大于父类内涵,子类外延属于父类外延),但不一定满足里氏替换原则。典型的就如那个著名的例子(正方形应该继承自长方形吗?)。
接口隔离原则指明客户(client)应该不依赖于它不使用的方法。根据该原则,一个大接口,拆分成更小、更具体的接口更好。
在实际的开发中,对这一点也有着很多体会。经常我需要实现spring中的某个扩展接口来满足一些需求时,会发现这些扩展接口包含了多个方法,我只需要使用其中的一个,但却不得不被迫实现其它的几个我根本用不到的方法。根据接口隔离原则,spring如果将这些方法拆分到不同的更小接口中,是更好的方法。
思考:但在实际开发中,将接口拆分得太细会引来过多的麻烦和复杂度。在实际编程中常常会见到另外一种解决以上问题的方法,对于包含多个方法、且客户一般不会使用所有方法的接口,会提供一个抽象基类,基类对每个方法提供了默认实现。用户使用时只需要继承抽象基类并Override自己需要的方法即可。甚至在jdk8中,抽象基类都不需要了,定义接口方法的默认实现即可。当然,必须承认,这种方案本质上还是没有消除客户对不使用方法的依赖,只不过让客户使用时更便利了。
在分层模块中,朴素的做法中高层次的模块会直接调用低层次的模块,这样高层就依赖于低层的实现细节。
依赖反转原则是让高层模块先设计出能抽象底层模块的接口,高层模块的其它功能只依赖于该接口,然后低层模块依赖于该接口。这样依赖关系就被颠倒(反转)。从高层依赖低层,变成低层依赖高层。
举个例子:程序对数据库使用的业务模块是高层,各种数据库如MySQL、SQL Server具体驱动实现是低层。我们的业务模块不依赖于具体的数据库驱动,只依赖于抽象的jdbc接口。各个具体驱动实现抽象的jdbc接口。
5. 标准化设计:设计模式
设计模式是什么?
设计模式是前人在软件开发道路中摸索、总结、提炼而出的通用、有价值的设计方法、复用技巧。《Code Complete》中指出:
一个系统所外来的、古怪的东西越多,别人在第一次想要理解它的时候就越是头疼。要尽量用标准化的、常用的方法,让整个系统给人一种熟悉的感觉。
一种特别有价值的标准化就是使用设计模式。
说的很清楚,设计模式是一种标准化的设计,也是一种习惯用法。就像建筑工程中在设计图纸时会参考前人的优秀、通用案例,设计模式就是前辈总结的、程序设计中的优秀、通用案例。
遵循设计模式,好处有以下几点:
一来在设计程序时直接使用前者总结好的现成设计,不需要自己绞尽脑汁去思索,从这个角度说设计模式是一种思维工具,利用好了能大大提高工作效率。
二来设计模式凝聚了前人的智慧和经验,恰当的遵循设计模式能够避免很多自己无法意料到的坑。
二来能够降低和同事的沟通成本,设计模式将某些有价值的设计标准化取一个名字,那么在和别人沟通、讨论的时候,我们只需要使用这个名字,大家都能领会到该名字所代表的设计思路与智慧。
而如果是自己发明一套全新的设计,还得先花费很大的沟通成本和同事去解释。
5.0.1. 设计模式不是什么?
设计模式不是银弹,不是万能药。
技术是为需求服务的,是为解决某个问题而针对性设计的。编程的过程,也是对具体问题抽象建模,将需求转为程序模型的过程。
所以,需求是什么样的,程序设计也要是是什么样的,程序设计要契合需求。如果对需求理解不透彻、不深入,那必然做不出恰当精妙的程序设计。
如果把设计模式当银弹,以为只需要在程序里使用了XX设计模式,就自动能解决掉XX问题,这种想法是对设计模式的严重误解。设计模式仅仅是开发者思维工具箱中的一种工具而已,但实际施工时是选择用螺丝刀、钉锤还是电钻,则要根据当下的工作恰当选择。
5.0.2. 怎么样才能理解设计模式?
如前所说,设计模式是前人在软件开发的实践中总结出的标准化设计,倘若脱离了该设计模式出现的代码背景,那无论怎么去理解不通透的。
摘录一位大佬精彩的论述:
追根溯源之后,你会发现这知识最初的创造者经过了成百上千的错误。这就像爱迪生发明灯泡,经过了几千次失败的实验。知识的创造者把最后的成功记录在文献里发表,然后你去读它。你以为得到了最宝贵的财富,然而最宝贵的财富却是看不见的。作者从那成百上千的失败中得到的经验教训,才是最宝贵的。而从来没有人把失败写下来发表。
没有这些失败的经验,你就少了所谓“思路”,那你是不大可能从一个知识发展出新的知识的。
设计模式既然是从经验中总结的,那么它除了蕴含着对”正确实践“的指导外,其实还隐含了大量被排除的”错误实践“。没有被”错误实践”坑得抓耳挠腮的体验,也就难以领会为什么“正确实践”如此精妙。
因此,想要理解设计模式,就必须要亲自写一个大程序,因为没有使用设计模式被搞得焦头烂额,代码被搞得混乱、臃肿一团糟。接着使用设计模式重构后发现代码顿时变得清晰优雅了起来,这时才能深刻领会到设计模式的精髓之处。
6. 后记
多时未更新博客,平时也只是在个人笔记里即兴写一些心得,或者是收藏一些优秀大佬的精品文章到onenote中去。将这些零碎知识输出成博客,实在太耗费时间和精力,出了学校越久越深感到时间精力的珍贵,处理完生活杂事、陪陪女朋友,似乎就到了睡觉的时间点了。
感谢最近一位大佬的鼓励,让我决定重拾这个良好的习惯。为解决时间、精力的资源紧缺问题,我思考出如下方法:
- 采用非线性写作方式,有灵感了就打开markdown开一个章节写,小主题采用问答形式,我原始的思路就是这种思维,省去转换、组织语言的麻烦。等觉得写得差不多了就聚合发表,不多占用工作时间和生活时间。
- 至少保持一周一篇的频率。如果业余时间多,就写写系统理解类的;若业余时间不多,就整理下cookbook类型的用例简单分享下。
(记于2020-01-10)。
7. 资料
一、书籍《Code Complete》
二、封装和归一化的概念
https://www.zhihu.com/question/20275578/answer/26577791
三、面向对象设计的五大原则SOLID