前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈面向对象的那些形而上

浅谈面向对象的那些形而上

作者头像
章鱼carl
发布2022-03-31 11:27:04
3760
发布2022-03-31 11:27:04
举报
文章被收录于专栏:章鱼carl的专栏

本文主要聊一聊笔者对软件工程、系统设计、OOA/OOD/OOP、面向对象设计原则、设计模式等概念的简单理解。这些都是高度抽象化,同时又很重要的概念。笔者仅是一个毕业工作五年的研究生,必然存在较大的认知局限。文中同时也借用了许多专家的精彩段落来帮助笔者阐述,引用会贴在段末。

首先,一瞥软件历史的重大变革,前瞻可能成为的下一次变革

半个世纪前,Dijkstra 已经敏锐洞见了机器算力的提升是编程方法发展的直接牵引,每当人类掌握了更强的算力,便按捺不住想去解决一些以前甚至都不敢去设想的新问题,由此引发软件设计模式的重大变革。

1. 结构化编程

背景:

计算机刚诞生的年代,硬件规模还很小,甚至程序员仅凭大脑就足够记住数据在几 KB 内存中的布局情况,理解每条指令在电路中的运行逻辑。此时的软件开发并没有独立的“架构”可言,软件架构与硬件架构是直接物理对齐的。

因素:

人脑的生物局限无法跟上机器算力前进的步伐。这便是历史上第一次软件危机的根源。

变革:

结构化编程扭转了当时直接面向全局数据、满屏 Goto 语句书写面条式代码的编程风气,强调可独立编写、可重复利用的子过程 / 局部块的重要性,让软件的每个局部都能够设计专门的算法和数据结构,允许每一位程序员只关注自己所负责的部分,从而在整体上控制住了软件开发的复杂度。

面条型代码就是所有逻辑堆砌在一起,就像写一篇文章,不怎么分段落。比如古代雕刻文字,在一块木板上雕刻一首诗,如果诗人要把其中的一个修改下,那得重新雕刻这首诗。非常容易发现这种模式的缺点:耦合太严重,牵一发而动全身。

过程式代码在面条型代码基础上有了很大的进步,它遵循“自顶向下,逐步求精”的思想,把一个大问题划分成若干个小问题,分而治之。对应上面雕刻诗的例子,诗是由若干个行组成的,如果每块木板上只雕刻一行诗,万一要改某个字,只用重新雕刻那一行就行,不用重新雕刻整首诗。但如果要修改多个字,而且在不同的行时,这种极端情况下整个首诗又得重新雕刻了。

2. 面向对象编程

背景:

结构化编程将软件从整体划分成若干个局部,人类能够以群体配合来共同开发软件,使得人与计算机又和谐共处了十余年。

因素:

机器的算力膨胀仍然在持续,人类群体的沟通协作能力却终究有极限。如何让各个模块能准确地协同工作成了一场灾难,这就是第二次软件危机的根源。

变革:

渡过第二次软件危机的过程中,面向对象编程逐步取代了面向过程的结构化编程,成为主流的程序设计思想。这次思想转变宣告“追求最符合人类思维的视角来抽象问题”取代了“追求最符合机器运行特征的算法与数据结构”成为软件架构的最高优先级,并一直持续沿用至今。

面向对象代码换了一种思考方式,诗是由行组成的,行又是由一个个字组成的,这也即是活字印刷的思想,这些字还可以复用于其它不同的诗,复用性非常强。

3. 分布式、云原生、微服务?

背景:

如果说历史上的第一、二次软件危机分别是机器算力规模超过了人类个体的生理极限,超过了人类群体的沟通极限的话。那么在今天,在云计算的时代,数据中心所能提供的算力其实已经逼近人类协作的工程极限。

因素:

与此算力相符的程序规模,几乎也到了无论采用何种工程措施去优化过程、无论采用什么管理手段去提升质量,都仍然不可避免会出现意外与异常的程度。如何采用不可靠的部件来构造出一个可靠的系统,是软件架构适配云与分布式算力发展的关键所在。

变革:

软件架构要与硬件算力规模对齐,目前用来适配云计算与分布式的主流架构形式是微服务。从单体到微服务的最根本的推动力,是为了方便某个服务能够顺利地“消亡”与“重生”,局部个体的生死更迭,是关系到整个系统能否可靠续存的关键因素。

软件历史部分主要参考:

https://mp.weixin.qq.com/s/ym9EeRYtHdKHy3tZvj-KEw

接着,我们来聊一聊面向对象的世界观和OOP

在前面我们看到第二次软件历史变革就是从结构化编程到面向对象编程,这次思想转变宣告“追求最符合人类思维的视角来抽象问题”取代了“追求最符合机器运行特征的算法与数据结构”成为软件架构的最高优先级,并一直持续沿用至今。

但是,这里我想重点聊的是,什么是所谓的“人类思维的视角”,这也是困扰系统设计新手的问题之一。

接下来随着笔者的思路一起进入面向对象的世界观。

1. 万物皆对象

凡所见之处我们先将其视作单独的个体,例如,一草、一木,一花、一世界等等皆为单独的对象。但是需要强调的是,对象内部也是若干对象,内部对象通过一定的联系构成了外层的这个对象。

怎么理解呢?例如,水分子H2O,内部是由两个氢原子和一个氧原子通过化学键发生联系组成了水分子。这三个原子本身也是对象,那么,氢原子内部也是由对象组成的,是由原子核和电子通过引力作用发生联系组成一个氢原子。可以继续递归向微观延伸,原子核又是由质子和中子相互作用组成的等等。向宏观延伸,水分子构成溪流,溪流构成江河湖海等等。

所以,对象有一个重要的维度:层次。在每一个层次,对象都有它特定的组成和联系。到这里我们脑海中应该是已经会浮现出一个重要的数据结构了:树,树的元素关系的本质就是分类和分层。在这里可以非常贴切的描绘每个层次的对象,以及下探这个对象的内部构成,即遍历子树节点。

2. 组成+联系=>结构=>功能=>行为

在对象内部,元素之间通过一定的作用发生联系,以此构成了对象。至于元素本身和元素之间的联系,这两点直接影响所构成对象的结构。而结构会直接影响对象的功能,功能又决定了对象会有一些什么样的行为。

怎么理解?还拿微观对象举例,例如金刚石和石墨,都是碳单质,都是由碳原子构成的,但他们却是不同的物质,即同素异形体。他们的化学性质相同,物理性质不同,为什么会出现这种现象呢?本质就是他们内部的碳原子的联系是不同的,导致这两类物质的结构特征不同,最终造成功能的不同,自然人们对他们的用途也就不同。

3. 抽象出类

抽象的一种定义是:抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。抽象的重要性如何强调都不为过。只有具备抽象的思维,我们才能将自己从纷繁复杂的细节中解脱出来,将有限的脑力集中于事物最本质的共性部分。正是因为具备了抽象的能力,人才能掌握更多的本质规律。

怎么理解?例如,平面上的三个点用线段连接,就是三角形,这种结构具有稳定性。这种高度抽象的数学语言,我们不用关心具体这三个点代表什么?是三个钉子,还是三个木桩,亦或者三条边代表什么,是木棒还是铁棒。我们从事物中抽象出来的三角形这种结构,可以泛化的代表一类结构,并且我们得出了这种结构具有稳定性这种特性。其实,我们经常为某一类事物下的定义就是这样的一个过程,是对一类事物抽象出的具有一定内涵外延边界限定的表达而已。

具备了这些基本的面向对象世界观的一致的语义后,将面向对象的世界观向软件设计方向迁移,我们来具体聊聊面向对象的程序设计。

面向对象软件工程的基本思想是尽可能按照人类认识世界的方法和思维来分析和解决问题。所谓面向对象就是基于对象概念,以对象为中心,以类和继承为构造机制,来认识、理解、刻画客观世界和设计、构建相应的软件系统。将软件系统看做一系列离散的解空间对象的集合,并使问题空间的对象与解空间对象尽量一致。而这些正是面向对象相对于传统软件工程的优势:

  • 传统软件开发方法无法实现从问题空间到解空间的直接映射
  • 传统软件开发方法无法实现高效的软件复用
  • 传统软件开发方法难以实现从分析到设计的直接过程

面向对象软件工程的基本特性:

1. 对象

对象由属性和方法组成。属性反映了对象的信息特征,即对象本身的本质,如特点、值、状态等,而方法则用来定义改变属性状态的各种操作。

2. 类

一组具有相同属性和运算的对象的抽象,即一组具有相同数据结构和相同操作的对象的集合,类是对象的模板。

3. 封装

每个对象都存在一定的状态和内部标识,对象将它自身的属性集操作“包装”起来,成为封装。

4. 继承

继承是以现存的定义为基础建立新定义的技术,是父类和子类之间共享数据和方法的机制,这是类之间的一种关系。

5. 聚合

又称组装,其原则是:把一个复杂的事物看成若干比较简单的事物的组装体,从而简化对复杂事物的描述。

6. 多态

多态性是指相同的操作、函数、过程作用于不同的对象上并获得不同的结果。

7. 消息通信

这一原则要求对象之间只能通过消息进行通信,而不允许在对象之外直接地存取对象内部的属性。通过消息进行通信是由于封装原则而引起的。在OOA中要求用消息连接表示出对象之间的动态联系。

8. 粒度控制

一般来讲,人在面对一个复杂的问题域时,不可能在同一时刻既能纵观全局,又能洞察秋毫。因此需要控制自己的视野:考虑全局时,注意其大的组成部分,暂时不详察每一部分的具体的细节;考虑某部分的细节时则暂时撇开其余的部分。这就是粒度控制原则。

除以上,笔者还有一点想特别想强调的是正交。在正交的维度上去抽象系统的核心角色才能最大的对问题域有掌控和覆盖。如何理解这里的正交呢?例如,我们常说的直角坐标系,直角坐标系就是平面内的一个正交的交叉维度,这种正交的横纵坐标轴有能力去表达平面内的任何一个坐标点。这就是正交维度所显现出来的强大张力。

根据笔者的认知给好的系统设计下一个定义:

  • 好的系统设计是从问题本质层面的核心维度做出的抽象。
  • 这些抽象出来的角色构成了系统的骨架,它们正交、稳定、生命周期长。
  • 系统中其他模块围绕核心抽象生长和演进。

我们知道软件开发过程一般定义为:

  1. 定义问题(problem definition)
  2. 需求分析(requirements development)
  3. 规划构建(construction planning)
  4. 软件架构(software architecture)
  5. 详细设计(detailed design)
  6. 编码与调试(coding and debugging)
  7. 单元测试(unit testing)
  8. 集成测试(integration testing)
  9. 集成(integration)
  10. 系统测试(system testing)
  11. 保障维护(corrective maintenance)

而面向对象的软件架构设计中主要分为三个步骤:

1. 面向对象分析OOA

定义:

从确定需求或者业务的角度,按照面向对象的思想来分析业务。

基本任务:

(1) OOA是软件开发过程中的问题定义阶段,目标是完成对所求解问题的分析,确定系统是“做什么”的,并建立系统的需求分析模型。

(2) 运用面向对象的方法,对问题域和系统责任进行分析和理解,找出描述它们的类和对象,定义其属性和操作,以及它们的结构,包括静态联系和动态联系。

(3) 最终获得一个符合用户需求,并能够反映问题域和系统责任的OOA模型。

步骤:

(1) 明确问题域和系统责任

问题域是指被开发系统的应用领域,即拟建立系统进行处理的业务范围。系统责任即所开发系统应该具备的职能。

(2) 需求的不断变化

需求的变化是需求分析过程中遇到的一个严峻问题,应变能力的强弱是衡量一种分析方法优劣的重要标准。

(3) 充分交流问题

(4) 考虑复用要求

2. 面向对象设计OOD

定义:

是面向对象方法的核心阶段。OOA建立的是应用领域面向对象的模型,而OOD建立的则是软件系统的模型。与OOA的模型相比较,OOD模型的抽象层次较低,因为他包含了与具体实现有关的细节,但是建模的原则和方法是相同的。

具体任务:

面向对象的设计是面向对象方法在软件设计阶段应用于扩展的结果,是将OOA所创建的分析模型转换为设计模型,解决系统“如何做”的问题。面向对象设计的主要目标是提高生产率,提高软件质量和可维护性。

3. 面向对象编程OOP

编程实现OOD阶段的设计。

OOA/OOD有若干流派,比较通用的是1989年Coad和Yourdon提出的Coad面向对象开发方法。将OOA/OOD分为5个层次和5个活动

  1. 对象类
  2. 结构:泛化、实现、依赖、关联、组合、聚合
  3. 主题
  4. 属性
  5. 服务
  6. 标识对象类
  7. 标识结构:分类结构(一般与特殊),组装结构(整体与部分)
  8. 定义主题
  9. 定义属性:OOA在定义属性的同时,要识别实例连接。
  10. 定义服务:OOA在定义服务的同时要识别消息连接。

最后我们来聊聊设计模式

如果“追求最符合人类思维的视角来抽象问题”取代了“追求最符合机器运行特征的算法与数据结构”,那么,他所解决的最主要的矛盾是算力发展带来的单机软件复杂度的膨胀,而设计模式就是封装这种复杂度膨胀的具体办法

面向对象是从对象的角度去看问题,解决问题是由各个对象协作完成,设计模式的基石就是面向对象,脱离了面向对象去谈设计模式那是耍流氓。

设计模式的本质:

有一本经典书籍:《设计模式:可复用面向对象软件的基础》,在书中作者提到了一句话:“找到变化,封装变化”,这才是设计模式的底层逻辑。

细细品味这句话,再去看23种设计模式,每种设计模式都在应对变化的事,比如策略模式,具体的策略在变化;工厂模式,创建的对象在变化;模板模式,具体模板算法实现在变化……

这一步就是为了应对前面讲到的在OOA阶段,需求囊括的问题域内的分析阶段要重点识别出的程序可能的扩展方向,即我们常说的可扩展性

所以我对设计模式的认知就是:我们应该清楚每种设计模式重点应对的程序结构变化的类型,然后在OOA\OOD分析出可能的需求增长点,在OOP阶段用相应的设计模式来封装这种可能的需求变化所带来的的程序结构的变化。

那么,我们接下来要回答的两个重要的问题就是:什么在变化,如何封装变化。

1 什么在变化

这里以对象生命周期的视角去看待对象的变化,对象是由创建而产生,然后被使用,最后是消亡。对象有三个不同维度的变化:对象结构的变化、对象规格的变化、对象行为的变化。以对象结构变化为例,对象的关系划分成两类:线性关系和非线性关系(树和图),在线性关系中,如何解决一个对象的变化不会影响到关联的对象?在树型结构中,如何解决不断新增加对象的问题?在图型结构中,如何解决用户方便使用复杂系统的问题?

2 如何封装变化

从封装的类型上看,有数据的封装、方法的封装、类型的封装等。就具体的封装方法而言,常见的有配置项、接口、抽象方法、类、注解、插件等具体的手段,再往上看主要使用了继承、组合的方法,再往上看封装的原则,常见的原则有单一职责、开闭原则、依赖倒置、隔离原则……

用底层逻辑推导结构型设计模式

1. 寻找对象结构变化

从UML看,对象之间的关系有依赖、泛化、组合、聚合,但就结构关系上看只有两种,线性关系和非线性关系。线性关系比较简单,就是一对一的关联关系,非线性关系分成两种:树型关系和图型关系。

关系结构有变化,意味着依赖发生了变化,比如线性关系中的变化,A依赖的B发生了变化,此时B变化了就会影响A,怎么做到B的变化不影响A就是要考虑的问题。

2. 应对线性变化

如上面所讲,如果B发生了变化,由于A依赖B,则对象A也要改变化,如何减少对A的影响呢?这里有两种方法:一种是通过增加适配来解决,另一种是通过代理来解决。这两种方法的要点都是一个对象不与变化的对象直接关联,不管是适配还是代理,都是引入了第三方来与B关联,第三方负责与B进行交互,B对A是没有感知的。有的人马上发现了一个问题,这不是把问题转移到第三方上了吗?乍一看,还真是这么回事,如果我们再发散思考,如果除了A要与B关系,还有E、F……,如果B一改就关联的所有对象就要变化,这种代价就比较高,如果只与第三方关系,只用改一个地方,成本要少得多。

3. 应对非线性变化

非线性关系比线性关系要复杂,常见也有两种方法:一种是通过注册机制,另一种通过抽象层屏蔽复杂性。当一个对象包含多个对象时,如果直接去管理,需要承担的职责太多,通过注册机制就比较好解决,增加一个对象,是通过注册机制主动告知对象。另外一种方法就是通过抽象层屏蔽复杂性,比如门面模式,在门面内把所有的复杂度都规避,对外提供简洁的接口。

设计模式部分主要参考:

https://mp.weixin.qq.com/s/qRjn_4xZdmuUPQFoWMBQ4Q

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 章鱼沉思录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2. 应对线性变化
  • 3. 应对非线性变化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档