深入抽象和动态建模(1)

简介: 深入抽象和动态建模

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

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


什么是动态建模

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

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

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

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

 

为什么需要动态建模

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

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

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

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

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

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


动态建模为什么没有流行

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

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

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

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

 

动态建模的相关研究

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

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

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

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

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


动态建模案例

需求说明

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

 

对比静态建模

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

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

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

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

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

第一步:设计最外层接口

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


 

第二步:尝试实现

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

根据例子中的条件,我们在这个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),总的价格加上税费,然后总的价格再加上运费。

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

 

 

第三步:重构实现

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

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

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

 

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

 

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

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

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

 

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

 

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

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

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

 

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

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

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

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

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

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

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

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

第四步:对象生命周期

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

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

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

 


相关文章
|
3月前
|
uml
建模底层逻辑问题之在建模时,对现实进行抽象该如何操作
建模底层逻辑问题之在建模时,对现实进行抽象该如何操作
|
3月前
|
设计模式
建模底层逻辑问题之以命令设计模式为例,要用定义法建模,如何实现
建模底层逻辑问题之以命令设计模式为例,要用定义法建模,如何实现
|
3月前
|
安全 Java
建模底层逻辑问题之在建模过程中,知识层和操作层如何区分
建模底层逻辑问题之在建模过程中,知识层和操作层如何区分
|
6月前
|
消息中间件 安全 搜索推荐
概述软件架构的定义与分类
【5月更文挑战第8天】软件架构是指导大型软件系统设计的抽象模式集合,旨在简化复杂工程,通过模块化实现系统各方面的分工。
|
6月前
|
设计模式 算法
|
设计模式 自然语言处理 前端开发
深入抽象和动态建模(2)
深入抽象和动态建模
深入抽象和动态建模(2)
【C++综合设计题】多层继承和抽象基类的综合应用
【C++综合设计题】多层继承和抽象基类的综合应用
|
Java 关系型数据库 程序员
深度思考:到底什么是抽象?
深度思考:到底什么是抽象?
|
测试技术 uml 数据安全/隐私保护
【UML 建模】UML建模语言入门-视图,事物,关系,通用机制(二)
【UML 建模】UML建模语言入门-视图,事物,关系,通用机制(二)
289 0
【UML 建模】UML建模语言入门-视图,事物,关系,通用机制(二)
|
运维 测试技术 uml
【UML 建模】UML建模语言入门-视图,事物,关系,通用机制(一)
【UML 建模】UML建模语言入门-视图,事物,关系,通用机制(一)
405 0
【UML 建模】UML建模语言入门-视图,事物,关系,通用机制(一)