DDD
至少30年以前,一些软件设计人员就已经意识到领域建模和设计的重要性,并形成一种思潮,Eric Evans将其定义为领域驱动设计(Domain-Driven Design,简称DDD)。在互联网开发“小步快跑,迭代试错”的大环境下,DDD似乎是一种比较“古老而缓慢”的思想。然而,由于互联网公司也逐渐深入实体经济,业务日益复杂,我们在开发中也越来越多地遇到传统行业软件开发中所面临的问题。
敏捷宣言
http://www.extremeprogramming.cn/content/xp/test-first.html
https://www.inflectra.com/Ideas/Topic/User-Stories.aspx
复杂性的挑战
有很多因素会使软件开发复杂化,但最根本的原因是问题领域本身的错综复杂。但我们无法回避这种复杂,只能控制这种复杂。
很多因素可能导致项目偏离轨道,如官僚主义、目标不清、资源不足等。但真正决定软件复杂性的是设计方法。很多应用的复杂性并不在技术上,而是来自领域本身,用户的活动或业务。当这种领域复杂性在设计中没有得到解决时,基础技术的构思再好也无济于事。成功的设计必须系统地考虑这个核心方面。
设计过程与开发过程
本向“敏捷开发过程”这一新的体系。特别地, 我们假定项目必须遵循两个开发实践
- 迭代开发
- 开发人员必须与领域专家具有密切的关系。
极限编程承认设计决策的重要性,但强烈反对预告设计。相反,它将相当大的精力投入到促销沟通和提高快速变更能力的工作中。具有这种反应能力之后,开发人员就可以在项目的任何阶段只利用“最简单而管用的方案”,然后不断进行重构,一步一步做出小的设计改进,最终得到满足客户真正需求的设计。
运用领域模型
领域
模型被用来描绘人们所关注的现实或想法的某个方面。模型是一种简化。它是对现实的解释。
它与解决问题密切相关的方面的抽象相关,而忽略无关的细节。
每个软件程序是为了执行用户的某项活动,或是满足用户的某种需求。这些用户应用软件的问题区域就是软件的“领域”。一些领域涉及物质世界,有些领域则是无形的。
为了创建真正能为用户活动所用的软件,开发团队必须运用一整套与这些活动有关的知识体系。所需的知识的广度可能令人望而生畏,庞大而复杂的信息也可能超乎想象。模型正是解决此类信息超载问题的工具。模型这种知识形式对知识进行了选择性的简化和有意的结构化。适当的模型可以使人理解信息的意义,并专注于问题。
模型在领域设计中的作用
在DDD 中三个基本的用户决定了模型的选择:
- 模型的设计的核心互相影响。模型与实现之间的紧密联系才使模型变得有用:
- 确保我们在模型中所进行的分析能够转化为代码。
- 在后期维护期间可以基于对模型的理解来解释代码。
- 模型是团队所有成员使用的通用语言中枢(共识的结果),技术团队与领域专家无翻译沟通。
- 模型是浓缩的知识,
- 来自软件早期的经验可以作为反馈应用到建模过程中(迭代)
软件的核心
软件的核心是其为用户解决领域相关问题的能力。所有其他特性,不管有多么重要,都要服务于这个基本目的。(大部分开发人员只专注于技术本身不关心业务知识,只见树木,不见森林)
消化知识
有效建模的要素
- 模型和实现的绑定
- 建立了一种模型的语言
- 开发一个蕴含丰富知识的模型
- 提供模型
在模型日益完善的过程中,重要的概念不断被添加到模型中,但同样重要的是,不再使用的或不重要的概念则从模型中被移除。当一个不需要的概念与一个需要的概念有关联时,则把重要的概念提取到一个新模型中,
- 头脑风暴和实验
知识消化
知识消息不是一项孤立的活动 ,它一般是在开发人员的领导下,由开发人员与领域专家组成的团队来完成的。他们共同收集信息,并通过消化而将知识组织为有用的形式。信息的原始资源来自于:
- 领域专家头脑里的知识
- 现有系统的用户
- 技术团队以前在相关遗留系统或同领域的其他项目中积累的经验。
信息的形式也多种多样
传统的瀑布流方式因为没有反馈,所以项目经验失败。知识只是朝一个方向流量,而且不会累积。
模型在不断改时的同时,也成为组织项目信息流(统一能用语言)的工具。模型聚集于需求分析。 它与编程和设计紧密交互。它通过良性循环加深团队成员对领域的理解,使他们更透彻地理解模型,并对其进一步精化。模型永远都不不会是完美的,因为它是一个不断演化完善的过程。模型对理解领域必须是切实可用的。它们必须非常精确,以便使应用程序易于实现和理解。
SDLC (System Development Life Cycle)系统开发生命周期
Waterffull vs agile
领域模型并不是按照“选建模,后实现”这个次序来工作的。象很多人一样,我也反对“先设计、再构建”这种固化的思维模式。Eric的经验告诉我们,真正强大的领域模型是随着时间演进的,即使是最有经验的建模人员也往往发现他们是在系统初始版本完成之后才了最好的想法。
Martin fowler
http://www.agilenutshell.com/agile_vs_waterfall
https://baijiahao.baidu.com/s?id=1720106868823032268&wfr=spider&for=pc
https://cloud.tencent.com/developer/article/1861060
存储库是为聚合而服务的
存储库不是一个对象。它是一个程序边界以及一个明确的约定,在其上命名方法时它需要的工作量与领域模型中的对象所需的工作量一样多。你的存储库约定应该是特定的以及能够揭示意图并对领域专家具有意义。
我一般的思考方式是:domainService是规则引擎,appService才是流程引擎。Repo跟规则无关
业务规则与业务流程怎么区分?
有个很简单的办法区分,业务规则是有if/else的,业务流程没有
https://cloud.tencent.com/developer/article/1803939
DDD 第二弹
架构这个词源于英文里的“Architecture“,源头是土木工程里的“建筑”和“结构”,而架构里的”架“同时又包含了”架子“(scaffolding)的含义,意指能快速搭建起来的固定结构。而今天的应用架构,意指软件系统中固定不变的代码结构、设计模式、规范和组件间的通信方式。在应用开发中架构之所以是最重要的第一步,因为一个好的架构能让系统安全、稳定、快速迭代
。在一个团队内通过规定一个固定的架构设计,可以让团队内能力参差不齐的同学们都能有一个统一的开发规范,降低沟通成本,提升效率和代码质量。
在做架构设计时,一个好的架构应该需要实现以下几个目标:
独立于框架:架构不应该依赖某个外部的库或框架,不应该被框架的结构所束缚。
独立于UI:前台展示的样式可能会随时发生变化(今天可能是网页、明天可能变成console、后天是独立app),但是底层架构不应该随之而变化。
独立于底层数据源:无论今天你用MySQL、Oracle还是MongoDB、CouchDB,甚至使用文件系统,软件架构不应该因为不同的底层数据储存方式而产生巨大改变。
独立于外部依赖:无论外部依赖如何变更、升级,业务的核心逻辑不应该随之而大幅变化。
可测试:无论外部依赖了什么数据库、硬件、UI或者服务,业务的逻辑应该都能够快速被验证正确性。
我们可以发现,通过对外部依赖的抽象和内部逻辑的封装重构,应用整体的依赖关系变了:
最底层不再是数据库,而是Entity、Domain Primitive和Domain Service。这些对象不依赖任何外部服务和框架,而是纯内存中的数据和操作。这些对象我们打包为Domain Layer(领域层)。领域层没有任何外部依赖关系。
再其次的是负责组件编排的Application Service,但是这些服务仅仅依赖了一些抽象出来的ACL类和Repository类,而其具体实现类是通过依赖注入注进来的。Application Service、Repository、ACL等我们统称为Application Layer(应用层)。应用层 依赖 领域层,但不依赖具体实现。
最后是ACL,Repository等的具体实现,这些实现通常依赖外部具体的技术实现和框架,所以统称为Infrastructure Layer(基础设施层)。Web框架里的对象如Controller之类的通常也属于基础设施层。
https://mp.weixin.qq.com/s/MU1rqpQ1aA1p7OtXqVVwxQ
聚合根的设计?
策略模式与充血模型设计?
DDD领域驱动设计总结先说下传统系统设计,大部分从数据库开始--自底向上的设计,这种设计会使系统的设计受到数据库的影响,会有比较大的局限性,比如说:数据库仅有数据,没有行为,而对现实世界的描述则会更加抽象,更加远离业务.开发团队通过与产品或客户的沟通,直接设计表模型,由于会受到沟通的效率的影响,客户不懂技术,开发从技术角度思考,整个沟通过程则会有较大的信息损失,所设计出的表模型则很可能面临后期业务变动的挑战.导致系统的成败主要取决于架构师在那个领域的业务水平而不是技术水平.
而DDD是采用自顶而下的设计,战略设计时业务为王,不再受到技术的局限性(我想可能跟现在技术的发展有关系,极大丰富的底层技术框架,使得技术选型更加自由),设计过程中,需要业务专家(不用懂技术,比如客户和产品等)和开发团队一块仅仅从业务的角度分析需求,不要受到任何技术的局限性,采用事件风暴(类似头脑风暴)的方式,讨论整个系统面临的各种用户场景,整个系统提供的各个功能模块,做业务上的归集分类分级,在讨论过程中,各个团队(客户 产品 开发 测试 运维 交付)逐渐统一语言(同一个限界上下文中),使得业务场景更加清晰的被描述和归纳分类出来,在整个沟通中更加有效率,更加准确,后续维护阶段的持续交付迭代都将由此而获益.
https://zhuanlan.zhihu.com/p/91525839
美团
import com.company.team.bussiness.lottery.domain.valobj.;//领域对象-值对象*
import com.company.team.bussiness.lottery.domain.entity.;//领域对象-实体*
import com.company.team.bussiness.lottery.domain.aggregate.;//领域对象-聚合根*
import com.company.team.bussiness.lottery.service.;//领域服务*
import com.company.team.bussiness.lottery.repo.;//领域资源库*
import com.company.team.bussiness.lottery.facade.;//领域防腐层*
https://martinfowler.com/articles/microservices.html
领域事件:
可能发生在同一个微服务内(跨聚合),一般微服务之间才采用领域事件实现异步通知.
值对象是内聚并且可以具有行为
https://www.cnblogs.com/uoyo/p/11951840.html
https://zhuanlan.zhihu.com/p/80921515
对于一个架构师来说,在软件开发中如何降低系统复杂度是一个永恒的挑战,无论是 94 年 GoF 的 Design Patterns , 99 年的 Martin Fowler 的 Refactoring , 02 年的 P of EAA ,还是 03 年的 Enterprise Integration Patterns ,都是通过一系列的设计模式或范例来降低一些常见的复杂度。但是问题在于,这些书的理念是通过技术手段解决技术问题,但并没有从根本上解决业务的问题。所以 03 年 Eric Evans 的 Domain Driven Design 一书,以及后续 Vaughn Vernon 的 Implementing DDD , Uncle Bob 的 Clean Architecture 等书,真正的从业务的角度出发,为全世界绝大部分做纯业务的开发提供了一整套的架构思路。
作者这里有一个说法是不准确的:“DDD 不是一套框架,而是一种架构思想”。DDD不是一种架构思想。DDD和架构之间是正交的关系,DDD可以和多种架构方式配合使用,包括n层架构,端口和连接器架构,Clean架构,微服务架构等等。架构关乎软件结构,从各个维度定义软件的组成部分以及各部分之间的关系。DDD是一种设计范式,主张以领域模型为中心驱动整个软件的设计。在DDD中,业务分析和领域建模是软件开发的关键活动。它不关心软件的架构是怎样的。随着技术的发展,我们可能在新版本中更换软件的架构,但是只要业务没有变更,领域模型就是稳定的,无需改动。
Robert C. Martin 在《整洁架构》一书中认为,架构和业务功能无关,只聚焦于软件质量,尤其是内部质量。而DDD完全聚焦于业务功能——发现问题域的内部组成、结构、规则和机制。
https://zhuanlan.zhihu.com/p/356518017
尽量避免public setter
一个最容易导致不一致性的原因是实体暴露了public的setter方法,特别是set单一参数会导致状态不一致的情况。比如,一个订单可能包含订单状态(下单、已支付、已发货、已收货)、支付单、物流单等子实体,如果一个调用方能随意去set订单状态,就有可能导致订单状态和子实体匹配不上,导致业务流程走不通的情况。所以在实体里,需要通过行为方法来修改内部状态:
继承虽然能Open for extension,但很难做到Closed for modification。所以今天解决OCP的主要方法是通过Composition-over-inheritance,即通过组合来做到扩展性,而不是通过继承。
Player.attack(monster) 还是 Monster.receiveDamage(Weapon, Player)?
在这个例子里,其实业务规则的逻辑到底应该写在哪里是有异议的:当我们去看一个对象和另一个对象之间的交互时,到底是Player去攻击Monster,还是Monster被Player攻击?目前的代码主要将逻辑写在Monster的类中,主要考虑是Monster会受伤降低Health,但如果是Player拿着一把双刃剑会同时伤害自己呢?是不是发现写在Monster类里也有问题?代码写在哪里的原则是什么?
行为抽离
这个在游戏系统里有个比较明显的优势。如果按照OOP的方式,一个游戏对象里可能会包括移动代码、战斗代码、渲染代码、AI代码等,如果都放在一个类里会很长,且很难去维护。通过将通用逻辑抽离出来为单独的System类,可以明显提升代码的可读性。另一个好处则是抽离了一些和对象代码无关的依赖,比如上文的delta,这个delta如果是放在Entity的update方法,则需要作为入参注入,而放在System里则可以统一管理。在第一章的有个问题,到底是应该Player.attack(monster) 还是 Monster.receiveDamage(Weapon, Player)。在ECS里这个问题就变的很简单,放在CombatSystem里就可以了。
攻击行为
在上文中曾经有提起过,到底应该是Player.attack(Monster)还是Monster.receiveDamage(Weapon, Player)?在DDD里,因为这个行为可能会影响到Player、Monster和Weapon,所以属于跨实体的业务逻辑。在这种情况下需要通过一个第三方的领域服务(Domain Service)来完成。
public interface CombatService {
void performAttack(Player player, Monster monster);
}
public class CombatServiceImpl implements CombatService {
private WeaponRepository weaponRepository;
private DamageManager damageManager;
@Override
public void performAttack(Player player, Monster monster) {
Weapon weapon = weaponRepository.find(player.getWeaponId());
int damage = damageManager.calculateDamage(player, weapon, monster);
if (damage > 0) {
monster.takeDamage(damage); // (Note 1)在领域服务里变更Monster
}
// 省略掉Player和Weapon可能受到的影响
}
}
contextmapper-contextmap-d6cnufuknfn.ws-us30.gitpod.io
- 战略设计阶段:
限界上下文(Bounded Context)和上下文映射(Context Map),简单来讲,对问题域进行划分,此时可暂时简单认为限界上下文就是微服务.包含了一组高内聚的服务.需要根据耦合的强弱程度和业务相关性来划分.
统一语言,同一个上下文中统一概念定义,保持各团队间准确高效的沟通.这个很重要,是头脑风暴(事件风暴)和后续沟通的基础.
聚合根的家——资源库
通俗点讲,资源库(Repository)就是用来持久化聚合根的。从技术上讲,Repository和DAO所扮演的角色相似,不过DAO的设计初衷只是对数据库的一层很薄的封装,而Repository是更偏向于领域模型。另外,在所有的领域对象中,只有聚合根才“配得上”拥有Repository,而DAO没有这种约束。
https://zhuanlan.zhihu.com/p/75931257
DDD中的读操作
软件中的读模型和写模型是很不一样的,我们通常所讲的业务逻辑更多的时候是在写操作过程中需要关注的东西,而读操作更多关注的是如何向客户方返回恰当的数据展现。
在DDD的写操作中,我们需要严格地按照“应用服务 -> 聚合根 -> 资源库”的结构进行编码,而在读操作中,采用与写操作相同的结构有时不但得不到好处,反而使整个过程变得冗繁。这里介绍3种读操作的方式:
基于领域模型的读操作
基于数据模型的读操作
CQRS
首先,无论哪种读操作方式,都需要遵循一个原则:领域模型中的对象不能直接返回给客户端,因为这样领域模型的内部便暴露给了外界,而对领域模型的修改将直接影响到客户端。因此,在DDD中我们通常为读操作专门创建相应的模型用于数据展现。在写操作中,我们通过Command后缀进行请求数据的统一,在读操作中,我们通过Representation后缀进行展现数据的统一,这里的Representation也即REST中的“R”。
基于领域模型的读操作
这种方式将读模型和写模型糅合到一起,先通过资源库获取到领域模型,然后将其转换为Representation对象,这也是当前被大量使用的方式,比如对于“获取Order详情的接口”,OrderApplicationService实现如下:
@Transactional(readOnly = true)
public OrderRepresentation byId(String id) {
Order order = orderRepository.byId(orderId(id));
return orderRepresentationService.toRepresentation(order);
}
我们先通过orderRepository.byId()
获取到Order
聚合根对象,然后调用orderRepresentationService.toRepresentation()
将Order
转换为展现对象OrderRepresentation
,OrderRepresentationService.toRepresentation()
实现如下:
public OrderRepresentation toRepresentation(Order order) {
List<OrderItemRepresentation> itemRepresentations = order.getItems().stream()
.map(orderItem -> new OrderItemRepresentation(orderItem.getProductId().toString(),
orderItem.getCount(),
orderItem.getItemPrice()))
.collect(Collectors.toList());
return new OrderRepresentation(order.getId().toString(),
itemRepresentations,
order.getTotalPrice(),
order.getStatus(),
order.getCreatedAt());
}
这种方式的优点是非常直接明了,也不用创建新的数据读取机制,直接使用Repository读取数据即可。然而缺点也很明显:一是读操作完全束缚于聚合根的边界划分,比如,如果客户端需要同时获取Order
及其所包含的Product
,那么我们需要同时将Order
聚合根和Product
聚合根加载到内存再做转换操作,这种方式既繁琐又低效;二是在读操作中,通常需要基于不同的查询条件返回数据,比如通过Order
的日期进行查询或者通过Product
的名称进行查询等,这样导致的结果是Repository上处理了太多的查询逻辑,变得越来越复杂,也逐渐偏离了Repository本应该承担的职责。
基于数据模型的读操作
这种方式绕开了资源库和聚合,直接从数据库中读取客户端所需要的数据,此时写操作和读操作共享的只是数据库。比如,对于“获取Product列表”接口,通过一个专门的ProductRepresentationService
直接从数据库中读取数据:
@Transactional(readOnly = true)
public PagedResource<ProductSummaryRepresentation> listProducts(int pageIndex, int pageSize) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("limit", pageSize);
parameters.addValue("offset", (pageIndex - 1) * pageSize);
List<ProductSummaryRepresentation> products = jdbcTemplate.query(SELECT_SQL, parameters,
(rs, rowNum) -> new ProductSummaryRepresentation(rs.getString("ID"),
rs.getString("NAME"),
rs.getBigDecimal("PRICE")));
int total = jdbcTemplate.queryForObject(COUNT_SQL, newHashMap(), Integer.class);
return PagedResource.of(total, pageIndex, products);
}
然后在Controller中直接返回:
@GetMapping
public PagedResource<ProductSummaryRepresentation> pagedProducts(@RequestParam(required = false, defaultValue = "1") int pageIndex,
@RequestParam(required = false, defaultValue = "10") int pageSize) {
return productRepresentationService.listProducts(pageIndex, pageSize);
}
可以看到,真个过程并没有使用到ProductRepository
和Product
,而是将SQL获取到的数据直接新建为ProductSummaryRepresentation
对象。
这种方式的优点是读操作的过程不用囿于领域模型,而是基于读操作本身的需求直接获取需要的数据即可,一方面简化了整个流程,另一方面大大提升了性能。但是,由于读操作和写操作共享了数据库,而此时的数据库主要是对应于聚合根的结构创建的,因此读操作依然会受到写操作的数据模型的牵制。不过这种方式是一种很好的折中,微软也提倡过这种方式,更多细节请参考微软官网。
CQRS
CQRS(Command Query Responsibility Segregation),即命令查询职责分离,这里的命令可以理解为写操作,而查询可以理解为读操作。与“基于数据模型的读操作”不同的是,在CQRS中写操作和读操作使用了不同的数据库,数据从写模型数据库同步到读模型数据库,通常通过领域事件的形式同步变更信息。
(CQRS架构)
这样一来,读操作便可以根据自身所需独立设计数据结构,而不用受写模型数据结构的牵制。CQRS本身是一个很大的话题,已经超出了本文的范围,读者可以自行研究。
关于仓储(Repository),你必须知道的几个概念。
1,仓储的两种设计方式:面向集合和面向持久化
面向集合和面向持久化,这两种类型的仓储设计方式,在《实现领域驱动设计》中有很详细的讲解,作者还附带了几个具体的实现,比如 Hibernate 实现、TopLink 实现等等,这个必须赞一个,感兴趣的朋友,可以进行阅读下。这面我简单说明下,这两种设计方式的不同之处,举个最直白的例子。
面向集合方式:
this.UserRepository.Add(user);
面向持久化方式:
this.UserRepository.Save(user);
可能很多朋友看到这,会不以为然,需要明确一点,在领域驱动设计中,不论是变量或是方法的命名规则都非常重要,因为其代码就是代表着一种通用语言,你要让人家可以看懂。在面向集合方式中,新对象的添加使用的是 Add,而在面向持久化方式中,不论是新对象的添加或是修改,都是使用的 Save,如果是基于 Unit Of Work(工作单元),会有 Commit。
//www.greatytc.com/p/acb7c8026c21
DDD 领域驱动设计学习(三)- 领域事件
Using the Specification pattern
https://enterprisecraftsmanship.com/posts/cqrs-vs-specification-pattern/
https://blog.csdn.net/sd7o95o/article/details/118864779
DDD战略设计
https://github.com/ddd-crew/context-mapping
https://www.cnblogs.com/powercto/p/14020435.html
战略设计工具
https://contextmapper.org/docs/online-ide/
DDD中问题空间和解决方案空间是一个伪命题
示例
Context Mapper - Visual Studio Marketplace
Vscode
https://github.com/ContextMapper/context-mapper-examples
DDD as Code:如何用代码诠释领域驱动设计?-阿里云开发者社区
Ouml
https://github.com/damonsk/onlinewardleymaps
https://blog.csdn.net/yunqiinsight/article/details/114691659
如花开始DDD的第一步,也就是Subdomain的划分。当然DDD中包括三种类型的SubDomain,分别为通用(Generic)、支撑(Supporting)和核心(Core)三种类型,这里稍微说明一下这几者的区别:
通用(Generic) Domain: 通用Domain通常被认为已经被行业解决的问题,如架构设计中的可观测性的Logging、Metrics和Tracing,各种云服务(Cloud Service)等,这些都已经有比较好的实现方案,对接就可以。当然业务上也有,如成熟的行业解决方案,如ERP、CRM、成熟硬件系统等,你购买就可以啦。
支撑(Supporting) Domain:和通用Domain类似,但是系统更来自内部或者还需要在通用的基础上进行一些定制开发。如一个电商系统,会员、商品、订单、物流等业务系统,当然还有一些内部开发的技术类型支撑系统。
核心(Core) Domain: 也就是我们常说的业务核心,当然如果是技术产品,就是技术核心,这个也就是你最要关注的。
这三者整体关系如下:Core是最与众不同且花费精力比较多的,在复杂性Y维度,我们要避免高复杂度的通用和支撑Domain,这样会分散你的注意力,同时还要投入非常大的精力,如果确实需要,购买服务的方式可能最佳。
工程架构
Testing Strategies in a Microservice Architecture
Ddd 战略设计
Ddd 战术设计
https://contextmapper.org/docs/tactic-ddd/
http://sculptorgenerator.org/documentation/
http://sculptorgenerator.org/documentation/advanced-tutorial
Ddd 语法
https://contextmapper.org/docs/tactic-ddd/
https://contextmapper.org/docs/plant-uml/
Dot demo
https://www.planttext.com/ plattext 也可以画 graphviz dot
http://bj.91join.com/color.html 在线配色
http://magjac.com/graphviz-visual-editor/ 在线可视化
https://graphviz.org/gallery/ dot 示例
https://www.cnblogs.com/itzxy/p/7860165.html
DSL
https://martinfowler.com/books/dsl.html
http://sculptorgenerator.org/documentation/overview#introduction
http://sculptorgenerator.org/documentation/ddd-sample
BPMN-SKETCH 可视化
https://www.bpmn-sketch-miner.ai/
使用DOT语言和Graphviz绘图(翻译) - zxyblog - 博客园
#http://bj.91join.com/color.html
digraph G1 {
rankdir=TB;
graph [compound=true]
node [color=black,shape=egg fillcolor="#FFFFFF" style="filled" shape=egg fontcolor="#000000"] //All nodes will this shape and colour
edge [color=black] //All the lines look like this
subgraph cluster_basic {
fillcolor="#FFAB00"
fontcolor="white"
style="filled"
label = "Infrastructure Layer";
SpringBoot[shape=egg]
Dubbo[shape=egg]
#_[color="white" fontcolor="#ffffff"]
ElasticSearchRepositories[shape=egg]
RedisRepositories[shape=egg]
};
subgraph cluster_domain {
fillcolor="#01939A"
style="filled"
fontcolor="white"
label = "Domain Layer"
ENTITY[shape=egg]
VALUE_OBJECT[shape=egg]
DOMAIN_SERVICE[shape=egg]
Repositories[shape=egg]
};
subgraph cluster_protocol{
fillcolor="#9FEE00"
fontcolor="white"
style="filled"
label = "Protocol Layer"
Protocol[shape=egg]
}
subgraph cluster_app{
fillcolor="#9FEE00"
fontcolor="white"
style="filled"
label = "Application Layer"
{rank="same";Valid;Shuffler;Merge;ReRank;Rank;Filter;Recall;}
Recall->Filter->Rank->ReRank->Merge->Shuffler->Valid
}
subgraph cluster_commons{
fillcolor="#CD0074"
fontcolor="white"
style="filled"
label = "Commons"
commons[fillcolor="white"
style=filled shape=egg]
}
SpringBoot -> Shuffler[label=" " ltail=cluster_basic lhead=cluster_app]
Shuffler -> DOMAIN_SERVICE [label=" " lhead=cluster_domain ltail=cluster_app];
Shuffler -> Protocol [label=" " ltail=cluster_app];
Repositories->commons[label=" " ltail=cluster_domain];
ElasticSearchRepositories-> Repositories
[label="DI" fontcolor="#9FEE00" style="dotted"];
RedisRepositories-> Repositories
[label="DI" style="dotted" fontcolor="#9FEE00"];
}