7

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

 3 years ago
source link: http://lanlingzi.cn/post/technical/2020/1101_code/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

飞哥讲代码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方法中。

试想在现实生活中:

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

1.1 背后的知识

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

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

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

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

2 代码重构

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

第一步:把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;
    }
}

3 再说类模型

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

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

有些材料又把Service分为:

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

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

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

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

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

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

4 事物认知

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

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK