建模,没必要

简介: Eric在DDD第一章节就介绍了模型,可见模型的作用不言而喻,说DDD是一种模型驱动设计方法,绝对没有问题那是不是我们在拿到业务需求时,就急呼呼的跟业务方来一起构造模型呢?毕竟模型是万事之首嘛在《DDD开篇》[1]提过DDD是一种基于面向对象的设计方法,我们既然已经有了面向对象,而且OOAD也很强大,为什么还需要DDD呢?要想弄清楚这两个问题,首先我们需要拿个示例来仔细比对一下

Eric在DDD第一章节就介绍了模型,可见模型的作用不言而喻,说DDD是一种模型驱动设计方法,绝对没有问题

那是不是我们在拿到业务需求时,就急呼呼的跟业务方来一起构造模型呢?毕竟模型是万事之首嘛

《DDD开篇》[1]提过DDD是一种基于面向对象的设计方法,我们既然已经有了面向对象,而且OOAD也很强大,为什么还需要DDD呢?

要想弄清楚这两个问题,首先我们需要拿个示例来仔细比对一下

OOP小示例

《面向对象是什么》[2]一文中提到的游戏小示例

有个游戏,基本规则就是玩家装备武器去攻击怪物

玩家(Player)可以是战士(Fighter)、法师(Mage)、龙骑(Dragoon)怪物(Monster)可以是兽人(Orc)、精灵(Elf)、龙(Dragon),怪物有血量武器(Weapon)可以是剑(Sword)、法杖(Staff),武器有攻击力玩家可以装备一个武器,武器攻击可以是物理类型(0),火(1),冰(2)等,武器类型决定伤害类型

作为一名受过OO熏陶的程序员,借助OO的继承特性把类结构设计成:

image.gifimage.png

public abstract class Player {
      Weapon weapon
}
public class Fighter extends Player {}
public class Mage extends Player {}
public class Dragoon extends Player {}
public abstract class Weapon {
    int damage;
    int damageType; // 0 - physical, 1 - fire, 2 - ice etc.
}
public Sword extends Weapon {}
public Staff extends Weapon {}

攻击规则如下:

兽人对物理攻击伤害减半精灵对魔法攻击伤害减半龙对物理和魔法攻击免疫,除非玩家是龙骑,则伤害加倍


public class Player {
    public void attack(Monster monster) {
        monster.receiveDamageBy(weapon, this);
    }
}
public class Monster {
    public void receiveDamageBy(Weapon weapon, Player player) {
        this.health -= weapon.getDamage(); // 基础规则
    }
}
public class Orc extends Monster {
    @Override
    public void receiveDamageBy(Weapon weapon, Player player) {
        if (weapon.getDamageType() == 0) {
            this.setHealth(this.getHealth() - weapon.getDamage() / 2); // Orc的物理防御规则
        } else {
            super.receiveDamageBy(weapon, player);
        }
    }
}
public class Dragon extends Monster {
    @Override
    public void receiveDamageBy(Weapon weapon, Player player) {
        if (player instanceof Dragoon) {
            this.setHealth(this.getHealth() - weapon.getDamage() * 2); // 龙骑伤害规则
        }
        // else no damage, 龙免疫力规则
    }
}

如果此时,要增加一个武器类型:狙击枪,能够无视一切防御,此时需要修改

1.Weapon,扩展狙击枪Gun2.Player和所有子类(是否能装备某个武器)3.Monster和所有子类(伤害计算逻辑)

除了伤害逻辑有各种规则,还有装备武器也会有各种规则

比如,战士只能装备剑,法师只能装备法杖,但他们都可以装备匕首

再比如,当我们有不同的对象,但又有相同或类似的行为时,OOP会不可避免的导致代码的重复

在这个例子里,如果我们去增加一个“可移动”的行为,需要在Player和Monster类中都增加类似的逻辑:

public abstract class Player {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}
public abstract class Monster {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}

一个可能的解法是有个通用的父类:

public abstract class Movable {
    int x;
    int y;
    void move(int targetX, int targetY) {
        // logic
    }
}
public abstract class Player extends Movable;
public abstract class Monster extends Movable;

但如果再增加一个跳跃能力Jumpable呢?一个跑步能力Runnable呢?如果Player可以Move和Jump,Monster可以Move和Run,怎么处理继承关系?要知道Java(以及绝大部分语言)是不支持多父类继承的,所以只能通过重复代码来实现

原生OOP力不从心

从OO角度看待,逻辑简单,代码也算过得去,也基本符合充血模型需要的数据与行为结合性要求

但如果业务比较复杂,未来会有大量的业务规则变更时,简单的OOP代码会在后期变成复杂的一团浆糊,逻辑分散在各地,缺少全局视角,各种规则的叠加会触发bug。

在这个小示例中,可以看到新增加一次规则几乎重写很多类,改造成本相当高,这还写得不够OO吗?

总体而言,上面的代码没有处理好这三个问题:

业务规则的归属到底是对象的“行为”还是独立的”规则对象“?业务规则之间的关系如何处理?通用“行为”应该如何复用和维护?


DDD应对

示例和单纯使用面向对象的问题已经很明晰了,DDD如何应对呢?

当然,可以申辩

虽然示例代码已经很OO,但却没有遵守OO原则SOLID[3],至少没有达到OCP目标

尤其开始就掉进OOP的陷阱,使用继承来实现看似是继承关系的逻辑,没有遵循组合优先于继承的原则

尤其没有提取出业务规则,并理清业务规则的归属,不应该与实体对象混合

建模

示例本身很简单,如果我们建模,大概是这样:

image.png

但很怪,模型则偏重于数据角度,描述了在不同业务维度下,数据将会如何改变,以及如何支撑对应的计算与统计,也就是说模型上看,会有实体以及实体间的关系,隐藏了业务维度,可以我们这个模型上却包含了动词,来描述业务行为

当然这个模型可以再充实一下,比如把业务规则标识上去,这也说明了传统模型的缺点,如果你对其他模型感兴趣,请关注我,后期会详情介绍模型系列文章

我们回到有问题的本质原点,为什么要建模呢,为了抽象复杂业务逻辑,降低理解业务的成本,挖掘更多的业务隐藏知识

可上面的示例太清楚了,一览无余。一句话可以概述出整个业务需求:

玩家使用武器攻击怪物,对怪物造成伤害,直至怪物死亡

把规则加进去:

玩家按规则使用武器按规则攻击怪物,对怪物、玩家、武器造成一定规则的影响(怪物受到伤害,玩家可能会有反弹伤害,武器持久属性会下降直到武器消失),直至怪物死亡

这其实是任何一款ARGP游戏的核心业务

软件开发的核心难度在于处理隐藏在业务知识中的复杂度,模型就是对这种复杂度的简化与精练,DDD改进版还使用事件风暴方式挖掘业务知识,而像这种业务知识没有隐藏的简明型业务系统,我们已经把核心问题描述得很清楚,无需再去知识消化,事件风暴,为了DDD而DDD,所以建模价值不高,甚至毫无必要

DDD应对

在上面的申辩中,我们已经发现了并不是OO不行,而是使用OO方式不对,虽说要把OO原则深入骨髓,可有没有一种方法能直接上升一层次,就像我们在使用面向过程语言时,也要有面向对象思维,实践没那么容易,直接使用面向对象语言,会让我们更容易使用面向对象思维,领略OO精髓

DDD正好就是这样一种方法,基于OO的升华,主要看看领域层的规范

实体,充血的实体

这一点与原生OO一样,数据与行为相结合

public class Player {
    private String name;
    private long health;
    private WeaponId weaponId;
    public void equip(Weapon weapon) {
       // ...
    }
}

任何实体的行为只能直接影响到本实体(和其子实体)因为 Weapon 是实体类,但是Weapon能独立存在,Player不是聚合根,所以Player只能保存WeaponId,而不能直接指向Weapon实体需要依赖其他服务时,也不能直接依赖,使用Double Dispatch

public class Player {
    public void equip(Weapon weapon, EquipmentService equipmentService) {
        if (equipmentService.canEquip(this, weapon)) {
            this.weaponId = weapon.getId();
        } else {
            throw new IllegalArgumentException("Cannot Equip: " + weapon);
        }
    }
}

领域服务(Domain Service)

单对象

这种领域对象主要面向的是单个实体对象的变更,但涉及到多个领域对象或外部依赖的一些规则

跨对象领域服务

当一个行为会直接修改多个实体时,不能再通过单一实体的方法作处理,而必须直接使用领域服务的方法来做操作。

在这里,领域服务更多的起到了跨对象事务的作用,确保多个实体的变更之间是有一致性的

不能学习实体的Double Dispatch


public class Player {  
void attack(Monster, CombatService) 
{       
CombatService.performAttack(this, Monster); // ❌,不要这么写,会导致副作用  
}
}

这个原则也映射了“任何实体的行为只能直接影响到本实体(和其子实体)”的原则,即Player.attack会直接影响到Monster,但这个调用Monster又没有感知

通用组件型

像Movalbe、Runnable通用能力,提供组件化的行为,但本身又不直接绑死在一种实体类上

策略对象(Domain Policy)

Policy或者Strategy设计模式是一个通用的设计模式,但是在DDD架构中会经常出现,其核心就是封装领域规则。

一个Policy是一个无状态的单例对象,通常需要至少2个方法:canApply 和 一个业务方法。其中,canApply方法用来判断一个Policy是否适用于当前的上下文,如果适用则调用方会去触发业务方法。通常,为了降低一个Policy的可测试性和复杂度,Policy不应该直接操作对象,而是通过返回计算后的值,在Domain Service里对对象进行操作。

总结

DDD是一种模型驱动设计方法,但使用DDD也并不是一定要按固定方式方法一步步执行,建模是为了对复杂问题的简化和精炼,挖掘隐藏的业务知识。如果能通过简明方式就能把业务核心问题描述清楚,比其他一切手段都有用,也都重要。那我们就没必要再去为了DDD而DDD,去进行事件风暴,知识消化慢迭代方式

本文中虽然提取了一些DDD领域层规范直接升华OO,但你有没有注意到一个问题,Player如果拥有很多能力,比如Moveable,Runnable,Jumpable,Fireable,那这个实体如何实现?

首先我们肯定会面向接口编程,提取出interface Moveable,interface Runnable,interface Jumpable,interface Fireable,可Player呢?

public class Player implements Moveable,Jumpable,Fireable {
    void move(int targetX, int targetY) {
        // logic
    }
    void jump() {
        // logic
    }
    void fire() {
        // logic
    }
}

可以想象,随着能力越来越强大,player类会越来越臃肿,发展成超大类,充满坏味道,可我们这次也没有违反什么原则?难道达到了原生面向对象的能力极限?

如果你有好的想法,欢迎留言交流。如果你觉得文章有帮助,多转发,点击右下角『看一看』

References

[1] 《DDD开篇》: http://www.zhuxingsheng.com/blog/ddd-opening.html

[2] 《面向对象是什么》: http://www.zhuxingsheng.com/blog/what-is-objectoriented.html

[3] SOLID: http://www.zhuxingsheng.com/tags/SOLID/


目录
相关文章
|
3月前
|
计算机视觉
利用各类回归模型,对数据集进行建模
【8月更文挑战第8天】利用各类回归模型,对数据集进行建模。
48 4
|
5月前
|
SQL 存储 关系型数据库
技术心得记录:数仓建模方法之范式建模、ER实体建模、维度建模
技术心得记录:数仓建模方法之范式建模、ER实体建模、维度建模
113 0
|
6月前
时间序列分析实战(四):Holt-Winters建模及预测
时间序列分析实战(四):Holt-Winters建模及预测
|
6月前
|
数据可视化 vr&ar Python
时间序列分析技巧(二):ARIMA模型建模步骤总结
时间序列分析技巧(二):ARIMA模型建模步骤总结
|
测试技术
分析建模
分析建模
114 0
|
机器学习/深度学习 并行计算 算法
R-建模 randomForest
本分分享了R语言中 `randomForest` 函数的用法,以供参考
140 0
|
数据可视化 Python
使用PyMC进行时间序列分层建模
在统计建模领域,理解总体趋势的同时解释群体差异的一个强大方法是分层(或多层)建模。这种方法允许参数随组而变化,并捕获组内和组间的变化。在时间序列数据中,这些特定于组的参数可以表示不同组随时间的不同模式。
137 0
|
机器学习/深度学习 算法 搜索推荐
多目标建模算法PLE
本文用于介绍PLE算法
216 0
|
算法 异构计算
时序电路建模基础
⭐本专栏针对FPGA进行入门学习,从数电中常见的逻辑代数讲起,结合Verilog HDL语言学习与仿真,主要对组合逻辑电路与时序逻辑电路进行分析与设计,对状态机FSM进行剖析与建模。
114 0
时序电路建模基础
|
机器学习/深度学习 存储 PyTorch
NeuralProphet:基于神经网络的时间序列建模库
NeuralProphet:基于神经网络的时间序列建模库
654 0
NeuralProphet:基于神经网络的时间序列建模库
下一篇
无影云桌面