面向对象的思考
What
面向对象
作为一名程序员,代码编程我们平时伸手就来。日常用到比较多的语言也许是 Java、TypeScript、C++ 等,大家都很清楚,这些都是面向对象的语言。那么问题也随之而来,是我们需要使用面向对象的特性才选择了这些语言开发,还是人云亦云地选择了这些语言开发?
在面向对象的理念中,万物皆对象。面向对象的精髓在于抽象,面向对象的困难也在于抽象。简单来说:面向对象的成功在于成功的抽象,面向对象的失败在于失败的抽象。
对象与对象之间都是孤立的,好比现实生活的你和我之间。只有在特定的场景下,孤立对象之间进行了某些信息交互才表示出一个场景过程,好比基于这边文章,你和我之间才建立起了作者和读者的关系。
面向过程
既然说到面向对象,就还应该了解到另外一个选项:面向过程。面向过程认为我们的世界是由一个个相互关联的消息同组成的,这是一种类似 螺旋式 的结构,每个小系统都有明确的开始和明确的结束,开始和结束之间有着严谨的因果关系。在面向过程的设计方法中强调将问题分解成小的、可重用的模块,每个模块负责执行特定的任务。
Talk is cheap. Show me the code!我们联想下生活中是如何购物的?浏览商品 → 加购 → 结算 。转换成对应的伪代码,应该是这样的:
这很符合我们平时的代码风格,但也确实是一种面向过程的标准写法。感到一丝诧异和矛盾,难道我们平时都在用面向对象语言来写面向过程的代码?事实确实如此。我们平时的方法封装调用很大一部分就是面向过程的设计。
这里并非是说 面向过程 的写法不正确,反而在某些场景下面向过程更加直观。但面向对象的设计为何而来?
哪怕我们平时大部分做的都是 CRUD 的工作,重复性的代码也会构建出一个庞大的系统。构成一个系统的因素太多,要把所有问题的因素都考虑到,再把所有因素的因果关系都分析清楚,接着把整个过程都用代码表述出来未免太过于困难,这不仅对创作者来说是一个灾难,对后继者来说更是:talk is cheap, code is shit !
我们转换下思维,如何利用面向对象的特性设计以上代码?利用面向对象的方法论,万物皆对象。
那么由此可以设计出 商品、购物车 以及 付款 的对象
通过利用这种方式,不论在哪一个层次上,我们都只需要面对有限的复杂度和有限的对象结构,从而可以专心地了解这个层次上的对象是如何工作的。比如在付款的结构上,我们可以只专注付款的扩展,从而可以延伸出 微信付款、支付宝付款、银联付款 等众多方式。
How
作为开发,我们工作的本质就是把一个产品需求转化成一个可以运行的系统,中间可能会涉及产品设计、需求建模、架构设计、实现设计、代码编写等众多步骤。
在调研需求时常常会陷入面向过程的误区,我们会最先弄清楚有多少业务流程,接着画出业务流程图,然后顺藤摸瓜,找出业务流程中每个关键步骤,弄清楚上下文是如何传递的。
事实上,架构设计、实现设计、代码编写都是属于软件开发的阶段。在设计之前,我们如果有一个清晰的目标,那后面的行动无疑会变得顺利很多。我们面对着成百上千的需求,每个需求可能都存在错综复杂的关系,复杂度并非是线性增长的。需求复杂度是否相等于技术复杂度?
image.png面向对象编程意味着编写针对建模对象的代码。这是用于描述复杂系统动作的众多技术之一。它是通过描述交互对象的数据和行为来定义。 那么在编程之间我们就需要进行很关键的一步:建模。
ChatGPT:建模是指对客观事物建立一种抽象的方法用以表征事物并获得对事物本身的理解,同时把这种理解怀念化,并将这些逻辑概念组织起来,构成一种对所观察对象内部结构和工作原理便于理解的表达。
公式:静态的事物(物)+特定的条件(规则)+特定的动作(参与者的驱动)=特定的场景(事件)
当我们尝试为需求背后的场景建模时,首先要决定便是抽象的角度,这一步尤其重要也尤其不易。一旦抽象角度确定,后面的设计就变得顺理成章,而不是杂乱无章。
这一步也是与 面向过程 的主要区别所在:
- 面向过程:希望能够通盘考虑,把所有问题的因素都考虑到,再将这些因果关系理清,这就会将结构变得很复杂。(把事情复杂化)
- 面向对象:希望能够把复杂的事物通过合理的抽象角度分解成小块,每个小块之间单独思考,最后再基于特定的场景将块与块之间串联。(把事情简单化)因此面向对象的关键就在于刚开始面对问题领域的时候不要决定去全盘考虑,而是找出问题领域里包含的抽象角度,只要抽象角度找对找全,那么通盘的问题也就层层解决。当然,在抽象的过程中,每个角度之间可能是互不关联的。
做需求的时候,首要目标不是要弄清楚业务是如何一步一步完成的,而是要弄清楚有多少业务的参与者,以及每个参与者的目标是什么。而这其中参与者的目标便是你需要抽象的角度
抽象角度
抽象的层次越高,具体信息越少,对应的概括能力越强。抽象有两种方式:
- 自顶向下: 自顶向下的方法适用于让人们从头开始认识一个事物。先宏观后微观
- 自底向上:自底向上的方法适用于在实践中改进和提高认识。先微观后宏观
映射到我们的软件开发过程中,我们往往会采用自顶向下的方式,先搭建一个框架,用少量粗粒度的概念来覆盖系统的需求,再逐步细化,降低抽象的层次。同时在细化的过程中,我们也能够通过细节间的相同之处,来改进较高层次的抽象范围。因此这两种方式应当是相辅相成,而不是两者择一的关系。
根据抽象而成的对象理应具备以下特征:
- 对象都具有原子性无论在什么时候,在同一抽象层次上,在分析过程中都应当将对象视为一个不可分割的原子,哪怕这个对象的规模很大。
- 对象都是可抽象的对象所具有的方面,或者说对象所参与的场景越多,对象越有抽象价值,反之则越没有抽象价值
- 对象都有层次性对象是有着抽象层次的。层次越高,其描述越粗略但适应能力越广;层次越低则描述越精确但适应能力越下降
参与者
参与者的角色在建模过程中是处于核心地位的。他们是处于系统范围之外,居于业务范围之内与系统进行交互的。参与者和系统之间有一个明确的边界,参与者只可能存在于边界之外,边界之内(系统之内)的所有人或事都不是参与者。如果参与者涉及到了系统边界之内,那么此时参与者的角色就是可疑的,需要重新定义。因此在业务设计阶段,一定要坚守好这道边界,参与者过早侵入系统就可能会对系统造成损害。
什么是参与者?简单定义如下:
- 谁将使用此功能
- 谁对某个特定功能感兴趣
- 谁负责支持和维护系统参与者一定是直接并且主动地向系统发出动作并获得反馈的,否则就不是参与者
在实际业务分析阶段,我们需要区分 业务范围 和 系统范围
- 业务范围:是指项目所涉及的全部客户业务领域,不论是否涉及开发系统的参与,这些业务都是客观存在的
- 系统范围:是指软件系统将要实现的那一部分功能,这些功能是从业务范围中提取,是业务范围的一个子集
如果在业务分析阶段就预设了计算机系统的存在,那么将会混淆现有业务和将来实际开发的业务,将用户代入到计算机系统中,可能会造成现有业务属性的削弱,而忽略了实际业务背后的含义。
当然,这也是开发人员对接业务需求的一大误区之一。喜欢从计算机系统的角度来思考问题,在向客户收集需求的时候总是在第一时间想到计算机将如何实现它,常常津津乐道于跟客户讨论背后的系统将如何实现客户的需求,并且指望客户能够用这种方式来确认需求。带来的危害:
- 客户不能理解将来的落地实现是什么样子的,但是出于信任开发人员,就会将信将疑地做出肯定
- 开发人员在一开始就加入了自己的主观判断,假设了业务在计算机系统里的实现方式,而没有真正地去理解客户的实际业务
用例
用例是一种描述系统如何与外部用户或其他系统进行交互的技术工具。它描述了系统的功能和行为,并以用户的角度来描述系统的需求和使用场景。简言之:用例是用来捕捉功能性需求
在软件开发阶段,我们会以用例作为最小指导单元进行设计,标准的用例应当具备以下特征:
- 用例是相对独立的
- 用例的执行结果对参与者来说是可观测的和有意义的用例的粒度大小不是从用例包含的步骤的多少来判断的,而是每一个用例能尽量能够说明一件完整的事情
用例的获取并非来源于开发人员,换言之,开发人员是用例的翻译者。用例的定义是参与者驱动的,这也对应了用例的特征之一:用例的执行结果对参与者来说是可观测的和有意义的。因此用例的来源就是参与者对系统的期望。所以发现用例的前提条件就是发现参与者,确定参与者的同时就确定了系统边界。
Code without understanding the business is like playing the rogue结合业务,与参与者聊需求的时候经常感觉需求飘渺不定,每个点都感觉是需求的核心部分。那么是否有认真想过:参与者想做和要做的事情不一定是他真实的目标,也许只是他做事情的一个步骤
- 一个明确有效的目标才是一个用例的来源
- 一个真实的目标应当完备地表达参与者的期望
- 一个有效的目标应当在系统边界内,由参与者发动,并具有明确的后果
在软件开发需要耗时最久是哪个阶段?应该是需求分析和设计阶段。为了缩短开发周期,很容易想到便是缩减需求分析和设计阶段的时间,当你真的这么做了,你可能就会离真正的用例愈偏愈远!要平衡时间和质量,就需要充分理解用户需求,当访谈不顺利的时候应当重新调整策略,调整系统边界的规模或者更换访谈的参与者都是不错的选择。
用例不是功能点在大多数开发者看来 用例是一个功能点。然而实际情况并非如此。功能的生命周期:输入->计算->输出,功能是脱离使用者的愿望而存在的,本身是孤立的。
类似一个判断用户是否有操作权限的方法,这个方法本身是脱离业务的,它的用例场景可能是用户在提交某个模块下的变更记录,才需要校验权限,而校验权限只是其中的一个功能。
在实际情况下,更应当从使用者的观点去描述,一个用例是一个参与者如何使用系统,获得什么结果的一个集合,那么此时用例可以解释为一系列完成一个特定目标的功能的组合,针对不同的应用场景,这些功能可以以不同的组合方式构建出新的用例。
步骤不是目标一个用例是参与者对目标的一个期望。在完成这个目标之前需要经由很多步骤,但每个步骤并不能完整地反映出参与者的目标,因此作为一个用例是有所缺陷的。错误地使用步骤作为用例,将无法准确地描述参与者如何使用系统,整体系统的操作流程以及交互细节会发生偏差,脱离预期。
目标和步骤很容易造成混淆,在弄清楚目标与步骤之前,我们就需要设置一些实际场景来解释,通过场景中涉及到的问题来测试出什么才是参与者真正的目的。
可能在此之前参与者也不清楚自己想要的到底是什么!
至此,回想《领域驱动设计-软件核心复杂性应对之道》自问世以来,带来了一个全新的思想:领域驱动设计。重点传述的也是通过领域设计来分离业务复杂度和技术复杂度。复杂度也许永远不能消除,但我们可以分析复杂度,进而管理复杂度。书中有提到:
每个软件程序是为了执行用户的某项活动,或是满足用户的某种需求。这些用户应用软件的问题区域就是软件的领域。
回过头来看上述所讲到的面向对象,不乏有相同之处。某种程度上,把“领域驱动设计”改为“模型驱动设计”也相当贴切。
回收开头的问题,我认为:需求复杂度 != 技术复杂度,建模理应成为我们管理复杂度的工具!
本篇文章的思维路线:
最后以 ChatGPT 来个结尾 面向对象编程在与业务需求结合时展现出不凡的优势,通过将业务需求映射为对象和类的组织结构,我们能够更好地理解和管理复杂的业务逻辑。
通过面向对象的方法,我们能够将业务需求转化为具体的对象和类,从而更好地理解和模拟真实世界中的业务流程和实体。通过封装数据和行为,我们能够将复杂的业务逻辑划分为独立的对象,每个对象负责特定的功能和责任。这种模块化的设计使得我们能够更好地理解和管理业务需求,同时也为将来的扩展和修改提供了便利。