深入抽象和动态建模

肖鹏老师,ZenUML.com 作者,独立咨询师,服务于澳洲领先的银行、零售企业,前ThoughtWorks中国持续交付Practice Lead,《面向模式的软件架构》卷4、5译者。

以下是肖鹏老师的带你深入理解抽象,及抽象在软件设计中的运用视频分享整理稿。


什么是动态建模

静态模型和动态建模的区别

我们来讲动态建模,与之对应的是静态建模,大家可以通过对比两者在几个概念上差异进行理解。

静态模型关注的概念是静态的:类(Class),属性(Attribute),方法(Method),类关系(Class relationship),类职责(Responsibility),是用类的语言来描述一个静态的类。例如用鸟类理解,静态模型就是关注的是鸟(类),含有哪些属性(眼,嘴巴,翅膀),包含哪些方法(飞,睡觉),包含哪些类关系(继承了动物类),拥有哪些职责(睡眠,鸣叫,飞行)。

而动态模型关注的概念是动态的:对象(Object),状态(State),交互(Interactions),对象关系(Object relationship),业务逻辑(Business Logic),是用对象的语言来描述一个动态的对象。例如用麻雀对象理解,动态模型关注的是麻雀(对象),含有哪些状态(眼睛是否闭合),含有哪些对象间的交互(麻雀和啄木鸟的行为是否用差异化的形式实现),含有哪些对象关系(这个麻雀是另一个麻雀的妈妈),包含哪些业务逻辑(麻雀睡觉的时候是否需要闭上眼睛)。

f7eb90139e0ccb8ae0219aca042f7283.png

为什么需要动态建模

在面向对象这个领域里边,静态建模的书籍和文章都是汗牛充栋。《设计模式》就是最经典的书籍之一,我个人认为策略模式是其中最经典的一个模式。它的概念可以这样理解,有一个策略,对应一个抽象类或者一个接口,然后这个策略有几个具体的子类,当我们在上下文里边注入这个策略之后,便可以在运行时选择具体的类执行。

策略模式看似很好,因为它符合开闭原则,解耦合了具体的类和实现。但如果你没有足够经验的话,其实想把它和具体工作结合起来,会发现是一件很困难的事情,因为当你真的去实现这个模式的时候,还会有很多问题需要回答:

第一个,策略是怎么构建的?它是在运行时new 出来,还是在程序启动的时候创建出来,还是在某个类需要的时候创建出来。

第二个,策略是怎么注入的?是在构造函数里面注入进来,还是用setter 注入,还是用容器来注入。

第三个,策略是怎么执行的?或者说怎么样选中一个策略,是用if else,还是说根据模式匹配等等。

这些话题,其实才是当我们真正落实到写代码的时候,必须思考和解决的问题,也是为什么我们需要研究动态面向对象建模的原因。

动态建模为什么没有流行

我猜测大家在工作中或者在书里,看到关于动态面向对象建模的概念比较少。我觉得可能有两个方面的原因。

第一点,简单的例子无法体现工具或者方法论的价值,比如说设计一个hello world,你没有办法说这个要用什么样的策略,什么设计模式去实现它,以及如何设计它的生命周期等等,这些东西都没有实际应用的价值。

第二点,复杂的例子无法在短时间内交代上下文,或者说需要一定的先验知识。如果用复杂的例子来讲述的话,可能介绍上下文背景的时间就大于讲工具或者方法论的时间了,这一点可能限制了他的传播。

不过即便书籍或者文章比较少,但还是有人研究的,如果你用dynamic object oriented programming去搜的话,还是有一些论文会讲这个。

485fb8fe783d893c16b401df63bdfdb5.png

动态建模的相关研究

我个人认为,有一些研究跟动态建模这个领域是相关的,比如测试驱动设计/开发,重构和specification by example等。

第一个,测试驱动设计/开发。它其实就是从一个大程序里取出来一部分,使我们要测试的类和方法在一个运行时的环境里来验证,实际是从所有代码中,隔离出来一个小的上下文,进行运行时代码逻辑的验证。

第二个,重构。它和动态和静态都有关系,它既关心静态的部分,比如说怎么样抽出类来,怎么样抽出接口,也关心动态的部分。比如说方法调用,if-else怎么样把它变成多态,等等。

第三个,specification by example。我记得我在国内的时候,还在一些公司做过相关的培训,不知道现在有没有人还在用这个东西,我个人是非常喜欢这个方法的,但在国外我没有见过用这个的公司。

我们今天会用一个非常简化版的specification by example ,来做一个例子,从而探讨动态面向对象建模的一个过程。

动态建模案例

需求说明

给武夷山的一个茶叶铺设计一个系统,该系统能计算顾客购买茶叶的总价格(图表对应相关费用的规则)

630b44ee08ea448bdf7e21306f81762d.png

对比静态建模

静态建模也有一些方法论的指导,这些是我在上学时的老师讲的一些方法,比如字典法,就是你先看一段需求用例的描述,把这个描述里边的名词提出来,作为类的类名。把描述里面的动词提出来,作为类的方法。把描述里面的宾语提出来,作为方法的参数。这个茶铺系统案例的主谓宾挺全的。

大家也可以自己尝试一下。如果采用静态建模的方法,想象一下你会创建几个类?每个类有什么方法?属性?以下是我尝试用静态建模思路设计的几个类:订单类(Order),地址类(Address),顾客类(Customer),价格类(Price),运费类(ShippingFee),总价计算类(PriceCalculator)。

但是这样的方法设计出来的结果,往往会存在一些问题,例如静态建模设计出来的代码可能不符合SOLID原则,可能没有办法回答如何构建如何注入如何执行的问题。

接下来,让我们尝试用动态建模的方式来设计这个茶铺系统,大家可以在这个设计过程中,对比下其中的差异。

做动态设计的一个好处是什么呢?尤其是当你有工具去辅助设计的话,你可以非常灵活的去调整你的设计,这个我觉得是非常重要的,它有点类似于敏捷和瀑布的一个区别,你不需要一开始就设计一个非常好的,完美的设计,而是不断迭代和重构,在这个过程中,设计变得更好。

第一步:设计最外层接口

在计算购买茶叶总价格这个例子中,我们假设最关键的是设计一个总价计算类(TotalPriceCal)。这个类提供一个方法,可以计算指定订单的总价。外部系统调用这个方法,就会返回一个价格,它就是我们最外层的一个接口。

8dbbae56557e5d6ecd5af289a48dd030.png

第二步:尝试实现

建议大家跟着我一起写一下这个代码,在写这个代码的过程中,你可能会理解为什么我们把它称为动态建模。

根据例子中的条件,我们在这个TotalPriceCal.cal()方法里,需要传入一个地址参数(adress),一个最初的价格(price)。根据图表中的规则,我们用Zen工具来编写伪代码来实现对应的逻辑:

如果address 是江浙沪(JZH),并且price大于等于100,则运费为0,总价不变。

如果address是江浙沪(JZH),并且price小于100,我们就要计算一个运费,运费是价格(price)乘以(multiply)运费标准0.3。这里我们引入了一个类叫做运费类(shippingFee),然后用总价(totalPrice)加上这个值。

如果address的国家是中国(CN),我们就要统一按照3%的来算这个额外的价格,因为我们这儿用了else 了,所以不用考虑这个江浙沪了,

如果address的国家是澳大利亚(AU),就要引入一个税费(gst),总的价格加上税费,然后总的价格再加上运费。

其他国际地区的逻辑,以此类推。最终,我们设计出了三个类,这是我们第一遍的设计。

96763ddea1800335e237d14c94d5c38a.png

9f23e9e0e8fc0504d89ef9256966b487.png

第三步:重构实现

看左侧的伪代码或者右侧的时序图,大家应该都能看出一些坏味道来:有很多重复的if-else,这个时候我们就要利用一些重构的知识来优化了,我们需要把代码逻辑中重复的部分提炼出来,并进行重构。

如何将不同的逻辑变得通用呢?我们可以认为,对于每一个if分支,它们都有一个计算税费(gstFee)和运费(shippingFee)的逻辑,只不过有的地方税费(gstFee)是0,有的是根据地址来计算,同样的,有的地方运费(shippingFee)是0,有的地方运费需要根据地址和订单来计算。

据此,我们可以引入一个税费计算器(gstFeeCal),和运费计算器(shippingFeeCal),我们可以对每一个if-else的部分,使用这两个计算器补全逻辑:没有计算税费的地方,加入税费计算的逻辑,gst=GstCal.get(add, price),同理,没有计算运费的部分,加入运费计算的逻辑,ShippingFeeCal.get(add,price)。

71482b919016cc2f180c83387acd9b29.png

这么做的目的是什么呢?我们可以看到,重构后的代码,对每一个if分支,处理逻辑是完全一样的,然后,我们可以把这些重复的代码去掉了(此处应有掌声)。

dc5c73e85f104c1791758292b6fd32f6.png

可以看到,我们通过动态面向对象建模的方式,用重构的思想,把重复的逻辑去掉之后,我们的设计立刻变得非常的清晰了。

我们引入两个计算器(calculator),一个税费计算器(gstCalculator),一个运费计算器(shippingCalculator),分别用来计算税费和运费,并且把它加到总价格(totalPrice) 上去。那你可能说了,这个只不过是把逻辑给藏到另一边去了,这样的设计真的是好的吗?

这是个非常好的问题,接下来让我们实现具体的计算运费逻辑,即ShippingCalculator.get(add, price)这段代码,入参是地址(add)和价格(price)参数,实现方法可以参考第一遍的设计过程。

2f4f2353094d34d52119abf64551eaf9.png

让我们分析ShippingCalculator.get(add, price)的实现,如果你要扩展运费的逻辑的话,比如说加上京津冀(JJH),我们只需要改一个地方,新增一个if(address == JJH && price ...) { rate.set(xxx); }就可以了。符合开闭原则。

7c597136242987091519aaa9e15a1f19.png

OK,这一步做完了之后,我们再来回看一下这个伪代码的设计(当然也可以看时序图)。大家有没有觉得,这个图其实还是有重复的地方,也就是红色圈中的这两处的逻辑。他们都是获取get 一个费用,然后再把它加到这个总价格(totalPrice)上。

这意味着我们可以抽象出一个叫做价格计算器(priceCalculator)的东西,这样,我们其实就把gstCalculator和shippingFeeCalculator这两种情况都给概括了,这也就意味着我们又消除了一处重复代码(此处也应该有掌声)。

这段代码最终的实现到底代表什么呢?看上去就是策略模式。在我们这个案例中,同时使用了两种策略来计算价格。

3f8d560cf57017588196a2c1035d97b0.png

以上讨论的就是动态建模的部分了,就是应该怎么样去构建它,怎么样去注入它,以及怎么样去执行它。

第一个,对象的构建。本例,我们是直接new实例的方式构建,没有采用IOC容器。

第二个,对象的注入。本例,我们通过创建gstCal,shippingFeeCal,covidTxCal等实例,并把它们加入到总价计算器(TotalPriceCal)的构造方法,来实现注入。

第三个,对象的执行。本例,我们采用for-each的形式,顺序执行几个计算器。

最后,我们可以用SOLID设计原则来验证我们的设计。

首先是单一职责(single responsibility)。税费(gst)就在税费计算器(gstCalculator)里面做了,税费的调整不会影响运费计算器(shippingFeeCalculator)的逻辑,是满足单一职责的。

然后第二个开闭原则,如果我们要加入新的计算器,例如因为疫情我们要加了一个专门的疫情费用计算器(covidTaxCal)。只要它也实现计算器这个接口,然后加入到计算器集合(cals)即可。这样基本上你可以认为它是符合开闭原则的。对于这个扩展是开放的,对于修改是闭合的(这个方法你都不需要改)。

其他的设计原则我们就不一一的去讲了,大家可以去用这些面向对象设计的原则去再去验证一遍。

第四步:对象生命周期

此时,我们就可以通过动态建模后的时序图,清晰的知道对象的生命周期:对象是何时构建和注入以及执行的。

下面这个图的实现,其实和前三步中代码的实现还不完全一样,这个就是一个很有意思的点。前三步的实现里其实是把各种计算器通过构造器注入的,而这个图是在方法里面去创建实例来实现注入的。

在现有的上下文里,其实很难说哪个好哪个坏,说到底,我们只不过是用这种动态建模的方式,用更低的成本,将对象的逻辑展示出来。如果让大家去根据这些图做沟通的话,就比较容易方便,并且不用等到把它的所有逻辑全部实现成代码后,才能做沟通。

25085ebcff15144e7f001dd8bfd18447.png

相关文章和工具

b站视频搜索:带你深入理解抽象,及抽象在软件设计中的运用

相关研究:https://www.tutorialspoint.com/object_oriented_analysis_design/ooad_dynamic_modeling.htm

ZenUml源码:https://github.com/ZenUml

提问环节

1.动态体现在哪里?

提问者:

我最大的问题是不理解如何去“动态”的,我稍微收窄下提问,铺垫下场景,我觉得今天分享的案例,最终的效果就是通过一个parent calculator,然后往里面传了N个child calculator。我认为它唯一可能存在动态的场景,就是去提供这个child calculator,请问是这样理解吗?

回答者:

回到我们刚分享的为什么需要动态建模那一个部分,我觉得至少有三个部分能体现这个动态:如何构建,如何注入,如何执行。

也就是说为什么我觉得动态建模重要,我是觉得静态建模里边真的没有体现这些概念。关于动态建模,我是针对静态建模讲的,我们这个行业里面很多东西,其实你都可以从不同的角度去看,而且他也有相通的地方,例如其他的关于建模的方法论,它也会牵扯到动态建模,因此方法论中也会存在类似的概念。

提问者:

这个动态就意味着修改结构上的一些事情,从而实现不用改动细节,然后去改变它的行为吗?

回答者:

我们看这张图来理解,各种讲面向对象的书,里边基本上都是这些词:类,方法,属性,类和类的关系,类的职责。我们很少看到动态模型的这些概念:对象,状态,交互,对象关系,业务逻辑。

09ee73610ea6d92e3f5c06602d328499.png

我们先找到我们共同的地方,比如说你打开设计模式这本书。他会跟你讲。我要有一个策略类,它有几个子类。然后这个策略类是作为一个字段,被这个上下文这个类使用,策略模式是讲这么一个东西,你同意吗?

提问者:

同意。

回答者:

其实对于动态建模,我也是有出发点的,你看我翻译的这本面向模式的软件架构这本书,它的第五卷的名字叫模式语言(pattern language)。他要解决一个什么问题呢?作者说,我们已经有这么多设计模式了。这些设计模式我们把它称为词语。我们要写的程序是一篇文章,这篇文章的最基本的单位,如果要表达一个意思的话,它至少要有一句话。

我们自然语言里面的一句话有主语、谓语、宾语。这23个设计模式里边,哪些是处于主语的位置,哪些是处于谓语的位置,哪些是处于宾语的位置,并且通过怎么样的组合能讲一个故事呢?

我举一个最简单的面向对象模式语言的例子,我首先要把一个对象创建起来,这个对象,我们可以用最简单的创建型的设计模式——单例模式来创建出来,接着我要用策略模式来装配他的行为,然后用选择器来匹配一个策略(选择器这个模式不在什么不在23个设计模式里面,但是它也算一种设计模式),然后进行执行,这样就构成了一句话。

但是这句话在对象设计模式第五卷(模式语言)出现之前,这个领域里面其实没有把模式语言这个概念抽象出来,动态建模其实跟模式语言是很接近的,面向模式的软件架构第五卷,这本书是我思考这个问题,进入一个比较系统化的时期,我在读和翻译这本书的时候,我就意识到,面向对象分析里边缺少的就是这部分。

我不知道,是否回答了你的问题。

提问者:

那比如说一句话:我吃了一个苹果,请问该如何用动态建模来理解。

回答者:

苹果就像策略模式。你告诉我说我有个苹果,这个苹果你给我描述的再漂亮,我也不会吃它。我也不知道我是要吃它,还是我要把它籽儿拿出来种一下,还是把它切成片儿看一下里边的细胞结构。你需要把它放到一个上下文里面去,说清楚你要把这个苹果怎么样用起来,是把它吃掉还是来种树?这个时候你就要考虑动态模型。如果你不考虑动态模型的话,就无法解答静态模型遗漏的那三个问题。

我十几年前看设计模式这本书的时候,我也是看的如痴如醉。但是回到项目上,我就是不会用,比如说大家耳熟能详的MVC,C是在哪个地方创建的?是C创建M,还是C创建V,M和V是什么关系?通过这个例子你会发现,MVC的这么多的书,都没有明确的跟你讲,谁创建谁这件事情。

这个时候,你就需要一个模式语言去来讲,当然一个故事有N多种讲法,你可能C创建M,也可能M创建C,但是这种创建的过程和选择,就体现了动态这两个字。

提问者:

如果不说苹果之类的太抽象的东西。就是说这个创建这一块,如果说我用这种类似于spring framework(一个完整的IOC container )的框架来创建东西,那创建的这个步骤是不是就已经解决了。

回答者:

这是个很好的问题,这就是你认识到了原来创建这件事情还是发生了,只不过是给你隐藏起来了。作为一个菜鸟或者说新手这样理解这个问题是可以的,因为创建这件事情太难了,正确的选择一个正确的时间去创建一个对象太难了,工具或者框架的出现,就是为了解决这个问题的,框架直接把对象创建好了,程序员直接从里面取出来用就行了。

但是,作为一个有经验的程序员,你必须要理解他是怎么创建的,因为被隐藏虽然有好处,但是你也要知道被隐藏也是有风险的,如果你工作过五年或者十年以上的话,你一定会遇到过一个问题:“我丢,为什么把这个东西隐藏了?我需要办法把它修改了!”。所以,并不是说创建对象的问题不存在了,它是仍然存在的,而且它是非常重要的一件事情,我认为是需要我们来理解这个过程的。

再进一步讲,就是注入的问题,那我是不是全部的对象都需要自动注入,以spring 为例,是不是全部对象都可以用这种方式创建?

如果你真的把所有对象都用这种方式注入,很有可能你的代码就会变成一坨shit。当一个类里面有几十个依赖的情况,其实你就要想了,我真的需要这些东西吗?我真的需要用这种方法去注入它吗?还是说我可以有更好的方法去管理这个注入呢?

如果你去看重构那本书的话,它的作者非常倾向于不要用这样的方法,而是用constructor 去管理注入。尤其是对于小的业务的类,不要去用那种service 的方式去去注入。

说完注入,那接下来就是执行,你怎么样去执行呢?你去看一下我们刚才的案例,执行到底是并行的,还是用一个数组做for each 的去执行呢?这些都是执行的细节。你会发现在静态建模里边,是不会有人讲这个事情的,只有重构这样的书才会讲。

最后,你如果觉得动态没有办法理解的话,可以把它替换成【运行时】。

提问者:

那就对了,我想起来自己使用go时,需要考虑运行时注入对象的一系列问题了,我知道你说的是什么了。

2.案例建模是通过一系列重构讲原有设计(各种if-else)变为的设计模式吗?

提问者:

老师,我进来的比较晚,我进来的时候,你已经在讲那个就是那个案例了,虽然你俩聊了很多,但是我还是没太听懂动态建模这个概念。

你讲案例写的这个过程,给我的感觉就是一个重构的过程。把原有的代码设计,重构成了一个更抽象的一个设计,抽象之后,发现这个更像是一个设计模式(而不是先知道有一个设计模式然后再往上套),这种就是您指的动态吗?

回答者:

这个还不是。这个是个结果,我个人认为建模一定是个过程。而且我比较推崇的一个词叫驱动,就是driven,就是你看到很多DD(BDD,DDD,FDD)之类的。

如果你告诉我一个结果的话,这个通常是大师型的人物才能做的。就是看一眼就知道这个应该是什么样子的,但是对于凡人来说,我们要有一个驱动,相当于说是你告诉我1和1,然后我能算出2来就不错了。

驱动是什么?这个驱动的过程就像你说的,就是在我们这个里面,其实就是一个重构,发现重复进行重构。

动态并不是说我建模的这个过程是动态的,而是说我关注的点是什么?你看我在这个过程中关注的点都不是类,属性,方法,类和类的关系,类的职责等。我关注的都是这个对象。这个对象是什么状态的?当然这里面我们也考虑到了一些对象之间的交互,对象之间的关系,对象的逻辑。比如说我是顺序的调用,还是for each,这个对象是什么时候创建出来的,什么时候初始化的,是怎么装配的,是怎么结构的,是怎么销毁的。这个是这个动态的来源。

我刚才讲了,就是说如果你觉得动态比较难理解,你可以把它想成运行时。但是为什么我们这个名字不叫运行时呢?是为了跟这个静态对比,这个是dynamic 翻译过后的意思。翻译的过程中我觉得有一点点损失,回到你的问题上,什么样是动态,就是我们关心的是运行时发生的事情和状态。

如果咬文嚼字一点,不是说建模的过程是动态的,而是说建模的驱动力是动态的,建模的驱动力是来自于这些动态的概念(对象,状态,交互,对象关系,业务逻辑),而不是来自于这些静态的概念(类,方法,属性,类和类的关系,类的职责)。

你如果用这样的驱动力去建模的时候,慢慢的就会发现一些好处,可能得到一个更好的设计。我是希望我觉得对于我们这样平凡的程序员来说,需要一些驱动力,来帮助我们做出更好的设计来。

3.动态建模除了时序图这种方式有没有更好的办法?

提问者:

动态建模的目的需要表达静态模型隐藏的一些逻辑,这种交互逻辑,我可以用其他的模型去展现吗?而不是用这种时序图的形式。因为我认为时序图的形式,这种方法级别的模型粒度太细了。

回答者:

实际我工作了这么多年,坦白说,我没有找到其他更好的方法。没有一个其他的图,能把这个描述的平衡做的这么好,这个平衡是指什么呢?就是既不描述太多的细节而又不缺失细节,并且你又可以随时灵活调整(比如说我不关心这一个条件,那我就把它去掉)。

对于模型粒度的问题,其实你可以在非常高的粒度上做这件建模,比如说我有一个图书馆系统。我有一个第三方的系统,叫Payment,我要检查用户有没有罚款,如果有的话,就不能借给你书,这个Payment 到底对应着一个接口还是一个类都可以。此外,假设还有一个外部系统Splunk,我们可以通过notice调用,那Splunk其实也不对应一个类,而对应的是一个系统,这个系统到底是通过in process 的调用还是http的rest 调用,都是允许的。

所以,这就是我为什么说,它是平衡的最好的一个工具,它相当于横轴铺开,纵轴铺开,是最容易理解的一种展现方式。

16e153afcb704939c9d247466f2bc11a.png

肖老师直播视频地址:

关注B站技术琐话

e1fc1ea27d1e3357d4b72956a11bed26.png

如果喜欢本文
欢迎 在看丨留言丨分享至朋友圈 三连

 热文推荐  
 阿里留不住的P10毕玄,到底有多牛?
仿天猫商城项目,超级漂亮【附源码】,接私活必备
有没有完全自主的国产化数据库技术
毕玄:怎么提升写代码的能力 聊聊如何度过寒冬(公司篇)​蚂蚁P10玉伯的产品思考:技术人如何做产品
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值