开发者社区> ljking7002-> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

【技术干货】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的方式书写代码。在此特殊时期,在酒店自行隔离期间完成本文,也算是给自己晚期拖延综合征打上一针。不足之处,欢迎指正。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
再议DDD分层
之前整理过《DDD分层》[1] 以及《分层架构》[2] 最近看网友讨论,整理一些有亮点的地方 现在分层架构+整洁架构似乎是个万金油组合了 之前DDD的标准分层结构:
0 0
DDD之代码架构
这是一篇迟到的文章。这其实是我写DDD的第四篇文章。去年11月份左右我在个人网站上写了三篇关于DDD的文章,都是比较偏战略部分的。那个时候我还在一个正在使用DDD的项目上,也是我第一次真正开始深入使用DDD。
0 0
DDD专题案例一《初识领域驱动设计DDD落地方案》
DDD(Domain-Driven Design 领域驱动设计)是由Eric Evans最先提出,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。整个过程大概是这样的,开发团队和领域专家一起通过 通用语言(Ubiquitous Language)去理解和消化领域知识,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型,再重复以上步骤,这样周而复始,构建出一套符合当前领域的模型。
0 0
殷浩详解DDD:领域层设计规范
在一个DDD架构设计中,领域层的设计合理性会直接影响整个架构的代码结构以及应用层、基础设施层的设计。但是领域层设计又是有挑战的任务,特别是在一个业务逻辑相对复杂应用中,每一个业务规则是应该放在Entity、ValueObject 还是 DomainService是值得用心思考的,既要避免未来的扩展性差,又要确保不会过度设计导致复杂性。今天我用一个相对轻松易懂的领域做一个案例演示,但在实际业务应用中,无论是交易、营销还是互动,都可以用类似的逻辑来实现。
0 0
DDD as Code:如何用代码诠释领域驱动设计?
相较于常规的MVC架构,DDD更抽象、更难以理解,各个开发者对DDD的解释也不尽相同。那么哪种设计方式才更好?在学习时如何知道哪种DDD更正统,没有被别人带歪?本文尝试使用“DDD as Code”的概念,即用DSL代码方式来描述DDD,统一DDD的设计思想,通过案例详细介绍如何基于ContextMapper来完成一个项目基于DDD DSL的表达,并分享现实中DDD的设计流程和微服务的关系。
0 0
殷浩详解DDD系列 第二讲 - 应用架构
# 第二讲 - 应用架构 架构这个词源于英文里的“Architecture“,源头是土木工程里的“建筑”和“结构”,而架构里的”架“同时又包含了”架子“(scaffolding)的含义,意指能快速搭建起来的固定结构。而今天的应用架构,意指软件系统中**固定不变**的代码结构、设计模式、规范和组件间的通信方式。在应用开发中架构之所以是最重要的第一步,因为一个好的架构能让系统安全、稳定、快速迭代
1334 0
基于事件驱动的DDD领域驱动设计框架分享(附源代码)
原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵。不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! 从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD)。
3214 0
关于领域驱动设计(DDD)中聚合设计的一些思考
原文:关于领域驱动设计(DDD)中聚合设计的一些思考 关于DDD的理论知识总结,可参考这篇文章。 DDD社区官网上一篇关于聚合设计的几个原则的简单讨论: 文章地址:http://dddcommunity.org/library/vernon_2011/,该地址中包含了一篇关于介绍如何有效的设计聚合的一些原则,共3个pdf文件。
1378 0
+关注
文章
问答
文章排行榜
最热
最新
相关电子书
更多
DDD(Domain Driven Design)的精髓
立即下载
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载