【技术干货】DSL和DDD

简介: DSL与DDD可以优化代码写法,和提高效率。

DSL和DDD

DSL对于很多程序员来说,既陌生又熟悉。熟悉到每天都在和各种形形色色的DSL打交道,陌生到可能都没意识到它们的存在,也就忽视了DSL的魅力。

那,我们就从DSL的魅力说起。

DSL的魅力

18年跟客户一起练习Coding Kata, 其中一道很有意思的题目叫做Poker Hands, 题目要求比较两手扑克牌的大小。如下是比较规则:

image-20191030220207401.pngimage-20191030220207401.png

上图右边部分是用Java代码所描述的比较规则,由一个枚举表示,完整代码如下:

public enum Rank {

    HIGH(shape(1, 1, 1, 1, 1)),
    ONE_PAIR(shape(2, 1, 1, 1)),
    TWO_PAIR(shape(2, 2, 1)),
    THREE_OF_KIND(shape(3, 1, 1)),
    STRAIGHT(consecutive()),
    FLUSH(sameSuit()),
    FULL_HOUSE(shape(3, 2)),
    FOUR_OF_KIND(shape(4, 1)),
    STRAIGHT_FLUSH(compose(consecutive(), sameSuit()));

    private Qualifier qualifier;

    Rank(Qualifier qualifier) {
        this.qualifier = qualifier;
    }

    static Rank rank(Hand hand) {
        return stream(Rank.values())
                .sorted(reverseOrder())
                .filter(rank -> rank.qualifier.qualify(hand))
                .findFirst()
                .orElse(HIGH);
    }

    interface Qualifier {
        boolean qualify(Hand hand);
    }
}

rank方法的职责是,给定一手牌(Hand),返回这手牌的Rank,比较两手牌的大小时,首先会比较它们的Rank,只有当Rank相同时才会进一步(按高牌规则)比较。

我们来看看这门DSL中的一些词汇:

  • shape(1, 1, 1, 1, 1)shape(2, 2, 1), shape(2, 1, 1, 1)shape(3, 1, 1)shape(3, 2)shape(4, 1)

定义一手牌的牌型,(1,1,1,1,1)为5张单牌,(2, 2, 1)为两个一对,加一张单牌。

  • consecutive()

定义一手牌是否连续。

  • sameSuit()

定义一手牌是否是同一种花色

  • compose(consecutive(), sameSuit())

组合多个规则,这里代表一手牌既连续,又同花。

通过这个例子,我们来窥探一下这门DS的"魅力":

首先,它给程序员带来了效率提升。高牌(HIGH),一对(ONE_PAIR),两对(TWO_PAIR),三条(THREE_OF_KIND),葫芦(FULL_HOUSE)和四条(FOUR_OF_KIND),都只用到了shape来描述,其描述了这个特定问题下的大多数场景。另外用于描述顺子(STRAIGHT)的consecutive也是由shape组合而来:

static Qualifier consecutive() {
        return compose(shape(1, 1, 1, 1, 1),
                hand -> hand.max().value() - hand.min().value() == hand.cards().size() - 1);
}

然后,在于它非常直观,也就是可读性好, 好到非程序员也可以理解。

最后,它让写代码变得更有趣味,可以更多的运用想象力。Martin Fowler在其《领域特定语言》一书中提到过图形化的DSL。

一道Coding Kata或许过于简单,面对复杂软件,DSL能够如何参与其中呢?

统一语言

Eric Evans通过《领域驱动设计 - 软件核心复杂性与应对之道》一书给不少企业和组织设计复杂软件提供了思路。统一语言是Eric Evans在该书中提倡的一种实践,强调用户与开发人员应该建立一种通用的,严谨的语言,这种语言是基于软件开发中所使用到的领域模型而建立的。用户,领域专家和开发人员使用这套语言进行沟通交流,由于计算机是精确的,所以要求这门语言必须严谨,没有歧义。同时,Eric Evans还明确指出,这门语言应该与领域模型一起,被演化,被重构,这门语言也可以被领域专家用来测试领域模型。

在实践过程中,我见过很多团队在这方面做出过很多努力,他们在交流过程中不断挑战队友所使用的模棱两可的词汇,在团队内整理词汇表,明确定义不同词汇的含义等等。毫无疑问,这些做法都是有价值的。我同时也见到过有团队将这门语言落地成一套可运行的DSL。个人比较赞同这种做法,其中的一些明显的好处包括:

  • 使用计算机保证这门语言没有二义性。
  • 可以像代码一样进行重构,进行版本管理。
  • 可以运行,也可以放进CI,也就不会有如静态文档会过期之类的问题。
  • 由于采用DSL, 使程序员效率提高,可读性变好,开发体验也就更好。

DSL在自动化测试中的应用

前文提到过,这门语言可以用来测试领域模型,也可以放进CI, 读者应该能想到,DSL的第一个用武之地可以是自动化测试中。我们经常说测试即文档,我们期望领域专家也能理解使用DSL编写的可运行的活文档。

我们来看这样一段文档:

@Test
public void should_book_meeting_success_if_room_available() throws Exception {
  room()
    .from(now())
    .to(now().plusHours(3))
    .available(true)
    .capacity(30)
    .setup();
  
    meeting()
      .from(now())
      .to(now().plusHours(2))
    .requires(capacity(20))
      .book(assertSuccess());  
}

@Test
public void should_book_meeting_failed_if_room_unavilable() throws Exception {
  room()
    .from(now())
    .to(now().plusHours(3))
    .available(false)
    .capacity(30)
    .setup();
  
    meeting()
      .from(now())
      .to(now().plusHours(2))
    .requires(capacity(20))
      .book(assertFailed());
}

@Test
public void should_book_projector_required_meeting_success_if_room_available_with_projector() throws Exception {
  room()
    .from(now())
    .to(now().plusHours(3))
    .equipment(projector(), projector())
    .available(true)
    .capacity(30)
    .setup();
  
  meeting()
      .from(now())
      .to(now().plusHours(2))
    .requires(projector(2), capacity(20))
      .book(assertSuccess());
}

@Test
public void should_book_projector_required_meeting_failed_if_room_available_without_projector() throws Exception {
  room()
    .from(now())
    .to(now().plusHours(3))
    .available(true)
    .capacity(30)
    .setup();
  
  meeting()
      .from(now())
      .to(now().plusHours(2))
    .requires(projector(2), capacity(20))
      .book(assertFailed());
}

这个例子中的二义性从人的角度来看,并没有完全消除,但是从计算机的角度来看,却是没有二义性的,如其中对于capacity的理解不同的人可能理解是不一样的,有人可能会理解成房间的座位数量,有人可能会理解成房间的座位数量,加上一些可以站立的空间的总和。但是对于计算机来说不重要,只要meeting book时capacity <= room.capacity即可。不管这个capacity代表的是什么,即使错了,与实现模型中的规则一起错了,那也是一致的,团队可以通过对模型和DSL持续重构,获得对模型更深刻的理解,如进一步重构可以精确描述房间的座位数等信息:

room()
    .from(now())
    .to(now().plusHours(3))
    .available(true)
    .capacity(30)
    .seats(20)
    .setup();

这段文档同时是一段可运行的自动化测试,所以可以放到CI中持续检验业务规则的变化,同时,程序员TDD的过程使用这种方式编写,也带来了效率的提升。

DSL在业务代码中的应用

上面的例子,只是提到了DSL在自动化测试中的应用,可能觉得并没有深入核心。如果你使用了《领域驱动设计》中提到的The Building Block of a Model-Driven Design(如Entiities, Value Objects, Repositories, Aggregates, Factories等),你其实已经在使用DSL编写你的业务代码了。Eric Evans所总结,或者说抽象出来的这些概念,是根据对象的职责,提炼出来的一些有共性对象的分组,这些概念就是这门DSL的基础词汇。我们每天都在使用,在代码中使用,在思考时使用,跟团队沟通时在使用,只是没有从这个角度去看待而已。

一个项目会由不同的领域来组成,不止是我们DDD中很强调的业务领域,也包括一些技术领域,比如事务,缓存,ORM,日志,安全等等。这些领域都有不同的框架致力于解决对应的问题,每个框架都有自己的DSL,有设计良好的,也有设计不好的,程序员免不了与这些框架打交道,自然而然就在应用这些框架所提供的DSL。Eric Evans在书中提到分离技术考量(Techinical Concern)与业务代码的重要性,分离或者是能够找到和谐相处之道,让我们用DSL编写一些业务规则变得简单。

对于业务规则比较丰富,复杂的系统,非常适合使用DSL来编写,DSL可作为模型的增强,让模型更具表达能力。针对如下模型:

interface Booking {
  Optional<Meeting> book(List<Requirement> requirements);
  Duration duration();
}

interface Requirement {
  boolean meet(Room room);
}

class CapacityRequirement implements Requirement {
  int required;
  
    public boolean meet(Room room) {
        return this.required <= room.capacity();
    }
}

class TimeRequirement implements Requirement {
  DateTime from;
  DateTime to;
    public boolean meet(Room room) {
        return room.available(from, to);
    }
}

如果找到多个房间,根据Booking类型,有不同的挑选策略,如返回第一个,或者返回容量最大的等等,这个策略类型是根据不同的BookingType所不同的,运用DSL我们可以编写如下规则:

public enum BookingType {
  SHORT(lessThan(hour(4)), firstAvailable()),
  LONG(compose(moreThan(hour(4), lessThan(day(1)))), maxCapacity()),
  CROSS_DAYS(moreThan(day(1)), maxCapacity()),
}

interface Qualifier {
  boolean qualify(Booking booking);  
}

interface PickupRule {
  Room pickup(List<Room> rooms);
}

这里举的是个小例子,但是发挥想象力,DSL可以被应用到业务代码的各个方面。Martin Fowler总结了很多DSL的模式。关于DSL本身的学习,推荐阅读Martin Fowler的《领域特定语言》。本文中举例的都是Internal DSL, External DSL的例子也比比皆是。

总结

最后,希望开发人员们能够给予DSL足够的重视,在你能想到的情况下,尽量去使用DSL的方式书写代码。在此特殊时期,在酒店自行隔离期间完成本文,也算是给自己晚期拖延综合征打上一针。不足之处,欢迎指正。

目录
相关文章
|
前端开发 测试技术 API
DDD领域驱动设计实战-分层架构及代码目录结构(上)
DDD领域驱动设计实战-分层架构及代码目录结构
1183 0
DDD领域驱动设计实战-分层架构及代码目录结构(上)
|
存储 设计模式 缓存
DDD领域驱动设计实战-分层架构及代码目录结构(下)
DDD领域驱动设计实战-分层架构及代码目录结构
1442 0
DDD领域驱动设计实战-分层架构及代码目录结构(下)
|
5月前
|
存储 设计模式 前端开发
MVC架构和DDD架构的区别?
最近在学习一个开源社区项目,第一次听说了DDD项目架构,于是通过搜索之后来分享给大家
|
3月前
|
消息中间件 测试技术 领域建模
DDD - 一文读懂DDD领域驱动设计
DDD - 一文读懂DDD领域驱动设计
675 0
|
10月前
|
消息中间件 架构师 搜索推荐
DDD领域驱动设计的概念解析
DDD领域驱动设计的概念解析
163 0
|
12月前
|
消息中间件 JavaScript 小程序
领域驱动设计(DDD)的几种典型架构介绍
领域驱动设计(DDD)的几种典型架构介绍
|
Web App开发 机器学习/深度学习 数据可视化
OneCode 领域驱动设计(DDD)技术实践(一)
OneCode-DSM(以下简称DSM)工具集是建立是以OneCode低代码引擎为基础专注于低代码建模应用的高阶建模工具。 在OneCode引擎中,出了为普通用户提供无代码的拖动设计器,低代码的业务逻辑编排器,之外还提供了供专业业务领域专家的使用的DSM建模工具。
|
IDE Java 程序员
DDD as Code:如何用代码诠释领域驱动设计?(2)
DDD as Code:如何用代码诠释领域驱动设计?
197 0
|
Kubernetes 前端开发 架构师
DDD as Code:如何用代码诠释领域驱动设计?(1)
DDD as Code:如何用代码诠释领域驱动设计?
144 0
|
前端开发 API 网络架构
DDD as Code:如何用代码诠释领域驱动设计?(3)
DDD as Code:如何用代码诠释领域驱动设计?
129 0