前面的文章《我理解的Smart Domain与DDD》中,我们分析了 Smart Domain 的设计,尝试回答了为什么 Smart Domain 可以用于实现 DDD,并对Smart Domain和DDD进行了一些扩展性的讨论。
虽然 Smart Domain 作为一种设计范式,可以辅助我们实现 DDD。但是具体到真实项目中,建模这个过程还得结合实际的领域问题,深入思考,大量尝试,大声建模,才能得到好的模型。有哪些值得参考的案例呢?下面分享几个个人在项目中觉得还不错的建模实践。
继承的应用
作为面向对象编程范式三大关键特性之一的“继承”,如果使用得当,在实践中可以帮助我们更好的建模概念间的关系并有效避免重复代码。
Algorithm模型抽象
我曾经经历过一个机器学习平台的项目,代码中有一个算法的概念。在没有有效建模之前,代码库中只有一个名为Algorithm的类,所有算法相关的信息均保存在这个类的属性上。项目中涉及到了几十个算法,都以数据的形式存储到了数据库。
表面上看,这一设计可以让我们更容易的添加算法(只需要增加数据即可)。但是,这带来的后果是大量的算法类型判断出现在代码库中。这是因为我们常常要对不同的算法进行不同的处理,比如在运行算法时需要为基于Tensorflow的深度学习算法和基于Spark的分布式学习选择不同的计算框架。
这一场景可以通过设计良好的一组有继承关系的类来表达(Algorithm基类,SparkAlgorithm/TensorflowAlgorithm抽象类,各个算法实现类),利用多态特性可以轻松避免这些条件判断代码。这一设计的另一大作用是将不太会变化的属性放入了代码而只将需要变化的属性放入数据库,从而很大程度上简化代码(大部分操作无需查询数据库)。关于此项目的更多内容请参考《机器学习平台架构实践 – 面向对象设计》。
Backend模型抽象
另一个项目是我们近期开源的名为Easy SQL的ETL开发语言项目。
为了同时支持不同的后端计算引擎,我们设计了一个抽象的Backend
类型,针对Spark
和MaxCompute
分别提供了实现。
同时由于需要支持不同类型的常规的关系型数据库作为后端计算引擎,我们实现了一个Rdb
的Backend
,在Rdb
的实现中,为了支持不同的方言,定义了一个名为Dialect
的抽象接口,然后针对此接口提供了PostgreSQL
Clickhouse
BigQuery
的实现。详情请参考这里的代码。
Apply模型抽象
还有一个为分布式服务创建中心化的权限管理应用。
在这个应用中有一个权限申请的概念。申请分为两种类型,一是对团队空间的权限申请,二是对发布数据的权限申请。这两种类型的申请存在相似性,比如审批流程相似,都有审批人等。但同时也存在诸多不同,比如,权限类型不同,团队空间可以有写权限,而发布的数据只有读权限。
遗憾的是,团队在进行模型设计时,只用了一个Apply
类来表达申请的概念,并因此引入了多处对申请的资源的类型的判断。现在回想起来,如果可以用两个子类来表达不同的申请,结果可能会好不少。
工厂模式的应用
工厂模式是继承的好朋友。试想,有了继承树,如何创建对应的类呢?一般而言,还需要一个工厂方法来根据不同类型创建不同对象。在我经历过的很多建模实践中,很多情况下都会将“继承”和“工厂模式”搭配起来使用。
算法工厂
比如,在上面的机器学习平台中,由于有多种不同的算法构成的继承树,在通过用户的选择进行对象构建时,就可以使用工厂模式。不同的算法往往需要不同的参数及配置,这一做法可以有效的将参数选择逻辑集中起来管理。
步骤工厂
除了配合“继承”使用,如果某些对象的构造本身比较复杂,也可以考虑用工厂来进行抽象。比如,在Easy SQL中,一个ETL被抽象为多个主要由SQL组成的步骤,在通过SQL文件来创建一组步骤的时候,就可以考虑用工厂模式实现。具体代码可以参考这里。
有限状态机的应用
在机器学习平台项目中,为了管理复杂的批处理任务的状态及其迁移路径,我们用到了有限状态机模式来进行抽象。
状态机定期获取任务的状态,在状态变化时进行记录,并根据启动时设置的任务状态转换处理器进行处理。
没有有限状态机抽象时,程序很多地方需要判断任务状态,并进行一定的逻辑处理。代码分散,很难理解。有了有限状态机的抽象之后,任务状态及状态迁移的处理器都被集中起来管理,从而变得直观、清晰且可控。
关于这个例子的更多内容可以参考之前的博客《机器学习平台架构实践 – 面向对象设计》。
其他设计模式的应用
设计模式是针对某一类问题的通用解决方案。如果能在建模的时候有效使用设计模式,可以以一种大家都熟悉的方式解决问题并提升设计的质量。
其他很多设计模式都可以在建模阶段灵活选用。我们可以从很多架构设计或常用库的实现里面看到他们的影子。
Clean架构中的Adapter
层,其实是用了适配器模式。
前端开发中,我们会添加很多交互事件处理器,这其实是观察者模式的应用。
后端开发中的Filter
是职责链模式的应用。
Java标准库中的各类包装类型,如Integer
, Long
等在实现时使用了Flyweight享元模式。
在我们自己进行建模时,可以参考选用这些设计模式使用。不过在使用设计模式时,需要注意不要为了用设计模式而用设计模式,否则很容易过度设计。
面向接口编程的应用
面向接口编程是一种拥有强大抽象能力的编程范式。在Smart Domain示例中,关联对象以接口的形式定义在模型中,用于辅助实现依赖倒置。
验证器
另一个例子是验证器的实现。
在Web后端开发中,常常要对传入的参数进行严格的校验。很多校验需要跨属性进行。此时可以用自定义JSR的验证器来实现。
用面向接口编程的思想来实现这样的验证器,可以这样做:
1.定义一个验证器类V
,其验证的注解为VA
,验证目标对象为VO
2.在验证器类中定义内部注解接口VA
3.在验证器类中定义内部目标对象接口VO
4.在要验证的类B
上加上VA
注解,并实现VO
接口
代码结构参考:
1 | public class V implements ConstraintValidator<V.VA, V.VO> { |
一个具体的实例可以参考这里的代码。
这样做的好处是,V
这个类变成了DDD原书中提到的Standalone Class,除了Java标准库依赖之外,没有任何其他依赖。这使得这个验证器非常容易被复用,因为它可被用于验证任何实现了VO
接口的对象。
结合Spring这样的依赖注入框架使用,还可以通过构造器给验证器注入任意的其他组件,以便实现更复杂的验证功能。
从这个例子里,我们可以看到面向接口编程带来的强大抽象能力。
TDD的应用
TDD对于改善设计有很大的帮助。
Eric在书中建议团队“大声”的建模,这实际上就是在强调我们人类的语言天赋。不同背景的人在讨论问题时,会很容易形成一种双方都可以理解的“混杂”语言。这是人类的天赋。通过交流和讨论,很多情况下,我们可以自然的找到一种合适的模型。
这跟TDD实践是一致的。在进行TDD时,我们会站在使用代码的角度进行解决方案的描述。在描述的过程中,可以充分发挥语言能力,让我们自然的得到一个良好的模型。
关于如何使用TDD来改善模型设计,我之前有几篇文章分享。列举如下,给大家参考:
总结
本文分享了一些建模的例子,从这些例子中可以看到,其实每个项目中都可以有很多可挖掘的内容,关键在于我们不能轻易满足提取“名词”,而是要深入思考直至深刻理解问题,大胆创新直至找到最恰当的抽象。
对于长期从事某一个特定领域的开发,如只做前端或只做后端的同学,我们可能需要去尝试练习一下端到端的应用设计和开发,以便于认识软件构建的全貌。这可能对于我们从软件整体去思考和建模有更大的帮助。可以按照软件技术发展脉络来设计自己的练习。一个推荐的路径是:简单命令行程序->客户端应用->前后端分离的Web应用->微服务。