蘭陵N梓記

一指流沙,程序年华


  • 首页

  • 归档

  • 关于

  • 搜索
close

飞哥讲代码15:写代码从事物认识开始

时间: 2020-11-01   |   分类: 技术     |   阅读: 2417 字 ~5分钟

案例

上周参加张逸老师解构领域驱动设计培训。课上老师提到传统的设计是贫血模型类+事务脚本(逻辑过程),并给出一个贫血类设计的案例代码。凭记忆记录如下,有三个类:

  • Customer: 顾客
  • Wallet: 顾客的钱包
  • Paperboy: 收银员

实现的主体逻辑是,收银员向顾客收钱。

代码如下:

public class Wallet {
    private float value;

    // 省略构造方法

    public float getTotalMoney() { return value; }
    
    public void addMoney(float deposit) {
        value += deposit;
    }

    public void subtractMoney(float debit) {
        value -= debit;
    }
}

public class Customer {
    private String firstName;
    private String lastName;
    private Wallet myWallet;

    // 省略构造方法,与Getter
}

public class Paperboy {
    public void charge(Customer myCustomer, float payment) {
        Wallet theWallet = myCustomer.getWallet();
        if (theWallet.getTotalMoney() > payment) {
            theWallet.subtractMoney(payment);
        } else {
            throw new NotEnoughMoneyException();
        }
    }
}

显然,案例中的Wallet与Customer类从直观上在太单薄了,而Paperboy是直接操作了顾客的钱包,把整个支付流程都写到了charge方法中。

试想在现实生活中:

  • 假如你作为顾客,你是否愿意也把你的钱包直接给收银员,说:“这是我的钱包,你直接从钱包中拿钱吧”。
  • 假如你作为收银员,你是否愿意直接去数顾客钱包中的钱,还要判断里面的钱是否足够。

背后的知识

我在之前的博文 类的职责单一 一文中提到了类的模型,主要有两种:

  • 贫血模型:是指对象只有属性(getter/setter),或者包含少量的CRUD方法,而业务逻辑都不包含在其中。
  • 充血模型:是指对象里即有数据和状态,也有行为,行为负责维持本身的数据和状态,具有内聚性,最符合面向对象的设计,满足单一职责原则。

Martin Fowler主张这种模型,他是从领域驱动开发(DDD)中领域模型对象来分析的,领域模型(Domain Model)是一个商业建模范畴。从一个模型的封装性来说,即有状态又有行为是合理的。

注:有些资料进一步细分四种:贫血模型、失血模型、富血模型、胀血模型。

代码重构

重构的方法:采用『移』。

第一步:把Paperboy.change的逻辑移到Customer中,收银员看不到顾客的钱包,以及其中钱的数目。

public class Paperboy {
    public void charge(Customer myCustomer, float payment) {
        myCustomer.pay(payment);
    }
}

public class Customer {
    public void pay(float payment) {
        Wallet theWallet = getWallet();
        if (theWallet.getTotalMoney() > payment) {
            theWallet.subtractMoney(payment);
        } else {
            throw new NotEnoughMoneyException();
        }
    }
}

第二步:对外屏蔽Wallet,去掉getWallet(),因为使用钱包只是一种支付方式,后续可能扩展为其它支付,如刷信用卡。

public class Customer {
    public void pay(float payment) {
        if (myWallet.getTotalMoney() > payment) {
            myWallet.subtractMoney(payment);
        } else {
            throw new NotEnoughMoneyException();
        }
    }
}

第三步:假定钱包也是个鲜活个体,也有自己的隐私。去掉getTotalMoney方法,不是暴露钱的数目,而提供判断是否足够的isEnough方法。

public class Customer {
    public void pay(float payment) {
        if (myWallet.isEnough(payment)) {
            myWallet.subtractMoney(payment);
        } else {
            throw new NotEnoughMoneyException();
        }
    }
}

public class Wallet {
    public boolean isEnough(float payment) {
         return this.value > payment;
    }
}

再说类模型

在DDD中,一般将领域模型通过如下三种概念表示:

  • Entity:用来代表一个事物,有唯一标识,它有着自己的生命周期。
  • Value Object:用来描述事物的某一方面的特征,所以它是一个无状态的,且是一个没有标识符的对象,这是和Entity的本质区别。
  • Service:用来组合多个实体(实体间没有聚合关系)和基础设施能力,提供领域内的组合服务能力。

有些材料又把Service分为:

  • Domain Service:即上面的领域模型中的Service,如果某种行为无法归类给任何实体/值对象,则就为这些行为建立相应的领域服务。如在账户管理领域中,转账服务(TransferService)需要操作借方/贷方两个账户实体,而借方/贷方又不能聚合到成一个新实体,并提供行为方法,所以转账行为可以由领域层的Service提供。
  • Application Service:组合领域层的领域对象行为、领域服务和基础设施层能力提供更为场景化的能力。可以根据业务场景需要包装出多变的服务,以适应外部变化并能保持领域层模型稳定。

把上面的三类领域模型都映射为类设计,则需要避免类贫血,应该是充血的,简单说类应该有数据、状态及行为。

贫血模型偏重个性化,面向过程式,逻辑与数据分离了。充血偏共性化,面向对象,类拥有其属性及对应的行为,数据与行为内聚在一个事物内,具有封装性。如果对象的某些行为在任何场景都是通用的,那么就放在领域中去,将其绑定,这是尊重“共性”的约束;如果对象的某些能力依赖于具体的场景,那么则在具体的场景中注入相应的行为,赋予对象相应的角色,这是尊重“个性”的自由。

对象的行为该不该放入“领域模型”,我们要先分析一下这些行为是对象所固有的,还是依赖于场景的。如果是固有的,即是共性的,就放入领域模型(Entity、VO,Domain Service),如果不是则延迟在具体的场景(Appliction Service)中,赋予其角色的个性。

结合DDD的思想,从案例中的代码,我们能体会类设计最好是:

  • 面向对象设计的本质,一个对象是拥有自己数据、状态和行为,具有完备性。
  • 对象行为方法要内聚到各自的实体或值对象上,减少类之间数据依赖,具有独立演进性。
  • 从面向业务流程(面向过程设计)转变为领域建模设计(面向对象设计)

事物认知

再结合DDD的概念,我们再来谈如何认识事物。

描述事物的基本方法:要素、属性和行为

  • 要素:就是事物的构成部分,如车由发动机,轮胎等要素组成。可以理解为DDD的Enitty,具体相关的Entity在一起形成聚合(Aggregation),聚合体即事物(车)。
  • 属性:就是描述要素特征的维度,如轮胎的型号,大小等则是描述轮胎的特征。可以理解为DDD的Value Object。
  • 行为:就是基于要素和属性的行为,要素和属性决定了行力能力,发动机提供动力,轮胎基于动力向前滚动。

要素、属性和方法的模型框架是数据化描述事物时使用的一种有效的方法,DDD建模则是对事物数据化的一种描述方法论。当模型概念映射为计算机编程语言时,而采用面向对象设计方式,事物分解成要素、属性即映射为『类』,类构建了计算机世界描述现实世界中事物的基本元素。

结语

由于我们的需求通常是交付某个功能,在需求分析过程中思考的是如何去达成某个条件,需要哪些步骤来实现功能,这是面向过程的解题思维方式。但现实世界又是由一切事物组成,需求可以映射到事物提供的业务能力,则我们需要思考事物是什么,事物能干什么,事物之间的关系是什么。这是面向对象的解题思维方式。

#软件开发# #java#
飞哥讲代码16:函数式让数据处理更简洁
飞哥讲代码14:好代码源自相互改进
微信扫一扫交流

标题:飞哥讲代码15:写代码从事物认识开始
作者:兰陵子
关注:lanlingthink(览聆时刻)
声明:自由转载-非商用-非衍生-保持署名(创作共享3.0许可证)

  • 文章目录
  • 站点概览
兰陵子

兰陵子

Programmer & Architect

164 日志
4 分类
57 标签
GitHub 知乎
  • 案例
    • 背后的知识
  • 代码重构
  • 再说类模型
  • 事物认知
  • 结语
© 2009 - 2022 蘭陵N梓記
Powered by - Hugo v0.101.0
Theme by - NexT
0%