
暂无个人介绍
暂时未有相关通用技术能力~
阿里云技能认证
详细说明DSL和DDDDSL对于很多程序员来说,既陌生又熟悉。熟悉到每天都在和各种形形色色的DSL打交道,陌生到可能都没意识到它们的存在,也就忽视了DSL的魅力。那,我们就从DSL的魅力说起。DSL的魅力18年跟客户一起练习Coding Kata, 其中一道很有意思的题目叫做Poker Hands, 题目要求比较两手扑克牌的大小。如下是比较规则:image-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的方式书写代码。在此特殊时期,在酒店自行隔离期间完成本文,也算是给自己晚期拖延综合征打上一针。不足之处,欢迎指正。
领域驱动设计是Eric Evans出版的《Domain Driven Design》一书中提出的,其在关于治理复杂的软件方面的效果被业内普遍认可。本文尝试理清一些重要概念,为使用领域驱动设计研发可组装的组件提供一种思路。业务建模领域驱动设计是一种业务建模的方法,业务建模的重要性往往容易被忽视,缺少业务建模往往就导致软件代码质量差,难以扩展和演进。业务建模不仅是一个寻找解决方案的过程,首先是一个定义问题的过程,清楚正确的定义问题,可以大大降低实现成本,错误的问题定义,实现的时候会出现各种奇技淫巧,往往软件质量也更低。比如某公司差旅系统需要员工将发票导入系统进行报销,财务部门希望系统能够自动识别发票的种类(如餐饮,住宿,交通),以及对应的金额,从而统计各种类员工差旅的花销。如果不对问题仔细定义,直接着手设计解决方案,一种陷入技术思维的实现方式可能是如下通过图像识别:我们需要能够扫描员工上传的发票图片。通过训练机器学习,只要有足够多的发票样本,我们可以让机器自动识别发票的种类和金额。但如果对问题稍加定义,从业务的视角去思考,可能会得出完全不一样的实现方式:让员工提交差旅的时候选择发票类型和对应的金额。并上传对应的发票,以供后台财务人员审核。以上两种方式,假设团队从无到有进行交付,图像识别+机器学习相对于让用户输入相关信息的方式,实现成本天差地别。业务建模的目标业务建模的目标是定义问题,并让所有参与者都能接受,包括业务方和技术方,然后在特定的架构约束下去落地实现。领域驱动设计中,大家耳熟能详的一些词汇,如统一语言(Ubiquitous Language),大声建模,知识消化(Knowledge Crunch),战术模式等,都围绕这些展开,如统一语言,大声建模是为了达成共识,战术模式,实体,值对象,聚合,仓储等都是为了实现落地的总结。统一语言下次你参加需求评审会,或者设计讨论会的时候,不妨认真听一下,你会听到一些常用的业务术语,这些业务术语会用于描述功能,一些显而易见的业务术语会被用于编码实现,用作具体的对象名称,类名,或者方法名。寻找这些术语的过程,其实就是建模的过程,而寻找出来的这些术语,构成模型本身,模型本身也构成每个团队,产品或项目独有的统一语言(Ubiquitous Language)。业务人员和技术人员建立一个通用的统一语言,用于沟通需求,描述实现成本。统一语言通常是在讨论中逐渐形成的。能够用于统一语言的领域模型一定是具有丰富业务知识的有血有肉的领域模型,领域模型的血和肉,是在充分讨论,并进行知识消化(Knowledge Crunch)的过程中形成的。比如如下领域模型:上述领域模型包含了一个航班可以登记多名旅客的业务知识。对于这样的一个业务知识,我们可能在代码中有对应的实现:@Data class Flight { List<Passenger> passengers; List<Seat> seats; public void register(Passenger passenger) { this.passengers.add(passenger); } } public void booking(Flight flight, Passenger passenger) { flight.register(passenger); }在这样的模型基础上,航空公司一个典型的超卖的业务规则,可以通过修改如下代码来达到:public void booking(Flight flight, Passenger passenger) { long maxBooking = Math.round(flight.getSeats().size() * 1.1); if (flight.getPassengers().size() + 1 > maxBooking) { throw new RuntimeException("reject due to over booking"); } flight.register(passenger); }这样的实现,虽然程序能够工作,但是一条重要的业务规则被隐藏在了卫语句中,随着代码越来越多,人员更迭,这条隐藏的业务规则就无法被明显的表达了,非业务人员很难将这条业务规则在软件内部找到对应的位置。经过讨论,消化知识,我们可以将隐藏的业务只是显化,我们可以得到如下领域模型:对应的代码实现,我们可以显示的定义一个单独的OverbookingPolicy类:class OverbookingPolicy { void checkOverbooking(Flight flight, Passenger passenger) { long maxBooking = Math.round(flight.getSeats().size() * 1.1); if (flight.getPassengers().size() + 1 > maxBooking) { throw new RuntimeException("reject due to over booking"); } } } public void booking(Flight flight, Passenger passenger) { overbookingPolicy.checkOverbooking(flight, passenger); flight.register(passenger); }这个简单的例子看到了模型作为统一语言,如何与代码实现相互影响和绑定,以提高代码的表达能力。综上,统一语言为业务人员和技术人员提供沟通的媒介,并且与编码实现绑定。修改模型即修改代码。当模型变得庞大,关系变得复杂的时候,为了控制软件的复杂度就需要划分边界了。边界的划分受到很多因素制约,其中很重要的一部分是康威定律,软件架构受制于组织结构,所以讨论软件架构如何与组织结构动态协调,甚至改变团队拓扑结构,这部分的研究称之为战略设计。将边界内具体落地的模型细节的处理和研究,称之为战术设计。战术设计Eric Evans在战术设计中提出的一系列模式,已经逐渐被社区广泛接受,在一些技术框架中也被广泛应用。在面向对象编程语言中,一个运行的程序由成千上万的对象组成,不同的对象有不同的职责,对于一些明显的对象特征,如实体,值对象,这些概念以被业内普遍接受。实体/值对象实体(Entity)很多对象不是通过他们的属性定义的,而是通过连续性和标识定义的。当一个对象由其标识(而不是属性)区分时,在模型中应该主要通过标识来确定该对象的定义。从技术上实体是一个跨越时间的对象,对象本身是可变的额,只要ID未发生改变就仍然是同一个对象。这里的ID不同与Java语言本身的对象ID,而是从业务角度切实存在的一个标识。值对象(Value Object)很多对象没有概念上的标识,它们描述了一个事物的某种特征。比较两个值对象是否相同的依据是看其属性是否相同,值对象通常是不可变的,如果值发生变化,则是另外一个对象。实体还是值对象?一个对象是实体还是值对象,并没有硬性规定,取决于业务场景,比如如果我们对电影票建模,不同的问题定义(或业务场景),根据所关心的不同业务特征,同样是电影票,可能是实体,也可能是值对象。如果一张电影票,上面通过座位号座位ID,那么电影票就是一个实体,ID变了,实体本身就变了。但如果我们构建系统所解决的电影院售票问题,是像中国早年间录像厅一样,只是一个入场券,而不跟具体座位绑定,即不要求特定的ID,那么电影票就是一个值对象。聚合(Aggregate)丰富的业务知识,不止是存在与领域对象的属性里面,属性只是业务数据,而对于复杂程度更高的业务规则,往往发生在对象与对象产生关系的时候。一些对象放在一起,紧密协作,完成业务动作,适合被放在一起讨论或交流。领域驱动设计使用聚合(Aggregate)的概念来表示这种富含业务知识的模型关系。同一个聚合内的对象,具有同样的生命周期。比如我们用一种更简单的方式来表示聚合:一个订单(Order)中包含多条订单行(OrderItem)。一辆汽车(Vehicle)包含4个轮胎(Wheel)。上图表示了两个聚合。每一个聚合都有一个聚合根(Aggregate Root),聚合根作为聚合内一个重要的特殊实体,维护着整个聚合的业务一致性。在实现层面,聚合操作需要程序员遵循一些特定的原则:聚合内需要保证业务一致性。业务操作从聚合根入口。聚合内对象的生命周期一致,聚合根从数据库中删除,则聚合内其他对象也应该删除。聚合根需要有全局ID,聚合内其他对象只要求局部ID即可。一个聚合的典型例子是:class OrderItem { String name; } class Order { String id; List<OrderItem> items; }对于添加订单行这样的业务动作的实现,如果不从聚合根入口:List<OrderItem> orderItems = order.getItems(); orderItems.add(new OrderItem("牛奶"));这样的问题是,业务一致性容易被打破,比如对于订单一个常见的需求是限制一个订单内的某种商品数量,比如每个订单限购两台苹果手机。如果没有一个专门的地方来固话这条业务规则的话,这个业务规则可能就会被打破,系统中就可能出现脏数据。采用聚合的方式,由聚合根入口的方式如下:class Order { String id; List<OrderItem> items; void addOrderItem(OrderItem orderItem) { //校验订单限制 this.items.add(orderItem); } } order.addOrderItem(new OrderItem());外部只需根据ID获得order, 然后调用order的方法进行操作。对于订单限制在统一的地方去做,只要不直接改数据库数据,对于Order的操作在程序内部都由聚合跟入口,因此业务一致性得以保证。这是一个简单的例子,对于并发修改等复杂场景,聚合根也能发挥优势。当然聚合中并不要求只能是集合代表关系,一些集合操作可以放到集合对象上。集合对象用于解决如分页,大聚和等问题等提供方法,如:class Order { String id; OrderItems items; } class OrderItems { Paged<OrderItem> items; //无需将对象全加载到内存中。 Integer size() { return total; //从数据库中查总数。 } }领域服务(Domain Service)领域模型表达了静态概念,聚合表达了概念与概念之间的关系,业务流程在其中并未表达。领域服务用于建模一个具体的需要多个领域概念交互的流程性的稳定的业务知识。非贫血模型中,动作是实体或值对象上的方法,但有时候一个方法的职责很难归到某一个对象上。这时候就引出了领域服务。领域服务操作的聚合根。抽象领域服务之前,先想想能不能抽象业务该概念,如无法抽象,则使用领域服务。应用服务(Application Service)稳定的业务逻辑放在领域服务中,非业务逻辑,如鉴权,校验,或者不稳定易变的业务逻辑放在应用服务中。应用服务与领域服务的区别:领域服务关注稳定的业务逻辑。应用服务关注非稳定业务逻辑的编排。应用服务和领域服务的区别是变化频率,无论是应用服务还是领域服务,都应该尽量薄,因为很容易将业务知识隐藏在过程式代码中。仓库(Repository)对象在系统中存在两种状态,被加载到内存当中参与计算的内存态,以及持久化在数据库中的持久态,对象在这两种状态之间来回转换。仓库负责按聚合的方式来做这两个状态之间的转换。对于非聚合根以外的对象使用Repository实际上是坏味道,因为通过这样的Repository可以获得一个聚合的一部分,这样就破坏了聚合的原则,导致一致性的维护出现问题。工厂(Factory)复杂对象的构造器,其实就是设计模式中的工厂模式,其职责是创建聚合。不是所有的领域模型都需要对应的工厂。有时候构造函数就够了。发现领域模型发现领域模型的过程,就是建模的过程,建模的过程可以多种多样,事件风暴工作坊作为一种注重讨论过程的建模方法,提供了建模的特定流程,一定程度上约束了无边际的讨论。事件风暴通过工作坊的形式,聚集领域专家和技术团队,在一面墙旁边进行领域建模,过程中会大声的讨论,产生分歧,达成共识。事件风暴工作坊通过四个高阶步骤来完成建模。事件风暴命令风暴寻找领域模型划分边界寻找领域模型会罗列所有的业务概念和聚合,划分边界会过渡到战略设计部分。示例通过一个简单的例子,来看看采用事件风暴的形式,如何来发现领域模型(战术设计),并最终划分边界(战略设计),战略设计我们在后面战略设计章节讨论。示例有如下简化的业务规则(本故事纯属虚构):旅客可以选择航班,并决定是否预订,支付预订订单。航空公司对于每个航班可以有一定比例的超卖。旅客到达急产过后,机场可以根据当前登机状态,广播通知未登记的旅客。不经过详细讨论和思考,直觉上我们可以会划分三个子域,(后台管理,预订,机场登机通知),根据经验直觉来的设计,不一定是合理的。第一步、事件风暴按照时间顺序,头脑风暴出所有领域事件。领域事件是领域专家关心的,在业务上真实发生的,有业务价值的,在系统中产生痕迹的事件。用橙色的卡片(贴纸)代表领域事件。所有领域事件都使用“名词已动词”的过去完成时结构,如“订单已创建”。对于我们示例的事件流,可放大下图查看。时间允许的话,我们可以先按照时间梳理场景化的用户旅程地图,然后根据用户旅程地图,来寻找领域事件。哪些东西算是领域事件呢?我们问如下的一些问题:是否在系统内产生了某种数据?是否触发了某种流程?是否导致了系统的状态变化?是否往外发出了消息?结合一些例子,比如查询是否是一个领域事件?查询取决于是否在系统中留痕,比如一个用户行为分析系统,查询也是用户的一种行为统计类型,那么查询就是领域事件。所以这取决于领域专家是否关心。再比如数据已存储是一个领域事件吗?这或许是一个技术事件,但是领域专家并不关心是否已存储,领域事件应该面向业务,而不是面向技术。第二步、命令风暴按照上一步风暴出来的事件,为所有事件寻找触发事件的命令,以及对应的触发源。用蓝色的卡片(贴纸)代表命令,所有命令都采用“动词”的形式。可能的触发源:用户界面外部系统调用定时任务其他事件一些注意事项:命令名字会变成模型的行为名称。最终会通过方法名体现。命令不只是简单的把事件反过来。需要思考其业务语言。对于用户界面的命令需要设计用户界面。第三步、寻找领域模型通过寻找名词的方法,把时间线上的相同名词放在一起,用大的黄色卡片(贴纸)标识,把命令放在其左边,对应的事件放在右边。这一步寻找出来的领域概念,已经有其价值。因为他定义了业务的关键问题,同时也能指导代码实现,比如业务概念就是一个类,命令可以是方法名,事件是状态的变化,如果是基于事件的架构风格,则向外广播事件。并且是在工作坊中共创的结论,默认是所有相关的人达成了共识。第四步、识别聚合仔细思考,讨论,哪些东西是否应该在一个聚合里,哪些应该是不同的聚合。参考聚合的原则。注意事项:不同的群体建模,可能形成不同的模型重要的是模型能解决业务问题,并且能被大家理解,达成共识。模型没有对错,设计会影响实现而已。我们在战略设计部分会来看事件风暴工作坊接下来关于战略设计的考虑。战略设计随着系统的增长,它会变得越来越复杂,当我们无法通过分析对象来理解系统的时候,就需要掌握一些操纵和理解大型模型的技术了。为了解决多个模型的问题,我们需要明确定义模型的范围——模型的范围是软件系统中一个有界的部分,这部分只应用一个模型,并尽可能保持统一。根据团队组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式)等来设置模型的边界,在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。统一语言在同一个上下文内要求无歧义。同一个概念在不同的上下文名字可能一样,但关注点不一样。在事件风暴工作坊中,找出领域模型后,对于复杂项目,可能会有很多领域模型,此时就需要进行战略设计,边界划分。第五步、划分边界(限界上下文)-- 接前文事件风暴划分边界有很多种角度,不同角度在不同的项目中所占比重可能不一样,第一种:按照业务耦合度划分第二种:按照弹性边界划分弹性是云所带来的好处,由于弹性可以减少计算和运营成本,所以在云原生架构下,弹性边界用于划分边界,有时候可能比业务边界还重要。那么弹性边界是否就与业务边界一模一样呢?答案是不一定,比如随着业务的发展,航班信息检索,和真正下单预订的容量可能会不一样。可能在不同的弹性边界。第三种、按照技术异构划分假设有一个PDF导出的需求,我们需要把用户在网页端看到的机票,导出为PDF。因为使用了chrome无头浏览器,浏览器天生对Javascript友好,所以这个上下文采用Javascript实现。由于系统其余部分使用Java实现,所以单独拆分一个上下文。不同的功能有自己所合适的技术,更适合放到不同的边界。第四种、按照业务变化评率划分三个月引入一个新需求的模块,与2年才引入一个新需求的模块,如果拆分到两个模块,可以有效的隔离变化的传播。第五种、按团队组织结构划分康威定律对于软件架构的作用实际比我们想象的更大:康威定律(康威法则,Conway’s Law)“设计系统的架构受制于产生这些设计的组织和沟通结构。” —M. Conway按照人员组织来划分边界,并按照团队维护不同的边界,往往更稳定。战略设计同样可以用于指导组织结构,这就是逆康威定律,用软件架构设计中划分的边界来设计组织结构。划分微服务有了边界(限界上下文)后,微服务的划分就可以参考这些边界,理想情况下一个微服务就可以对应一个限界上下文。但是考虑到微服务的管理成本,需要结合自身的基础设施管理能力来权衡。编码实现任何建模方法,如果无法落地实现,无法指导程序员如何写代码,都不能算上是成功的建模方法。实际上软件的复杂度除了复杂的业务以外,实际存在与代码当中。模型落地到代码有什么挑战呢?模型落地到代码的挑战用代码把领域模型表达出来并不简单,需要很强的抽象能力。加上代码中存在的各种技术顾虑。如下代码示例,模型代码只占一小部分,长期下去,要从这段代码中找出哪些是表达业务的代码变得几乎不可能。写出计算机认识的代码很简单,写出人能读的代码才是挑战。为了让表达业务的模型代码更显示的表达出来,架构师们在架构上想了很多办法,无论哪种架构风格,几乎都会有专门的一层,或一个地方,来存放模型相关的代码,比如经典的分层架构:经典分层架构的问题经典分层架构中模型层的目的是存放模型代码。上层依赖下层,下层不能依赖上层,目的是为了隔离变化传播,让不稳定的层依赖稳定的层。但是从实际情况来看,基础设施层的变化速率并不低,即基础设施层并不是最稳定的层,原因是我们对于基础设施的变革也没有停止过,从物理机,到虚拟机,到云计算,到微服务,以及FaaS,从Oracle为代表的大型关系型数据库,到轻量的MySQL关系型数据库,到NoSQL的探索,不同的消息中间件等等,这些基础设施或中间件产品,本身也在不断更新换代,所以模型层依赖于基础设施层并不能有效的隔离变化。六边形架构的问题六边形架构把模型放在一个绝对独立的相对核心位置,不再依赖于基础设施,而是让基础设施变成一种端口/适配器。理想情况下,六边形架构可以让模型非常纯粹的表达业务,几乎完全的隔离了技术顾虑。但落地的时候也会碰到一些现实的实际问题,一个最大的实际问题就是,模型始终需要被持久化,由于没有永远不宕机,无限大内存的计算机,所以持久化无法避免。使用六边形架构的实现,对于模型的操作通常会有如下步骤:通过Repository找到对应的聚合。完成业务操作。持久化整个聚合的变化到数据库。用代码描述:public void flightStart(Request request) { Optional<Flight> flight = flightRepository.findById(request.flightId); flight.ifPresent(flight::start); flightRepository.save(flight); }这里的问题是,flight的变化会导致多余字段的数据库更新,在一些特殊情况下会带来性能下降,所以六边形架构依赖于一个精巧的ORM框架来解决这些问题。美好的执念绝对独立的领域层是美好的执念,接受领域层的不独立,往往更合适。“Technology can help you implement a Model or can fight you all the way. When it fights you or doesn’t provide good mechanisms you just workaround with it.” —Someone says Eric Evans said this.技术能够帮助你实现模型,也会阻碍你实现,如果阻碍了你实现,那就想办法学会与其相处。首先应该被打破的是对基础数据库的依赖,因为持久化必然存在,所以我们应该大方承认模型依赖于基础库的持久化,而其他基础设施则不一定,不是所有模型变化都必须发一条消息出来。所以我们认为对基础库的依赖是合理的依赖,是合理的类聚。这对我们的代码有什么影响呢?我们可以写这样的代码:public void flightStart(Request request) { Optional<Flight> flight = flightRepository.findById(request.flightId); flight.ifPresent(flight::start); //运行后持久化就完成了 } class Flight { @Autowired FlightMapper flightMapper; void start() { this.status = FlightStatus.STARTED; this.flightMapper.updateStatus(this); //对数据库进行更新 } }在六边形架构的基础上,我们可以得到如下一张架构示意图:这样的架构也需要一些解决一些细节问题,比如上述代码示例中的FlightMapper,一定会引起一部分程序员的不舒适,如果说不舒适是可以通过改变认知,放弃执念来解决,那么现实是采用Spring这段代码都不工作,因为Flight从数据库中通过Repository重新构造出来的,对于熟悉Mybatis框架的程序员来说,因为Flight由ORM实例化,Spring(依赖注入框架)并未参与,所以@Autowired会抛出无法找到Bean的异常。不过这个问题是可以通过参与框架的生命周期来解决的,比如我们可以参与Mybatis的构建对象的过程,让Spring可以去注入依赖:public class InjectableObjectFactory extends DefaultObjectFactory { private AutowireCapableBeanFactory beanFactory; InjectableObjectFactory(AutowireCapableBeanFactory beanFactory) { this.beanFactory = beanFactory; } @Override public <T> T create(Class<T> type) { T t = super.create(type); autowire(t); return t; } @Override public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { T t = super.create(type, constructorArgTypes, constructorArgs); autowire(t); return t; } private <T> void autowire(T t) { if (t != null) { beanFactory.autowireBean(t); } } } @Configuration public class MybatisConfiguration { @Autowired AutowireCapableBeanFactory beanFactory; @Bean ConfigurationCustomizer configurationCustomizer() { return configuration -> configuration.setObjectFactory(new InjectableObjectFactory(beanFactory)); } }组件设计组件设计中如何划分组件,边界是什么,与限界上下文的思路如出一辙,我们可以将限界上下文根据可组装的属性,做出映射,则得出一个系统如何可以由若干组件组成。总结本文从业务建模出发,描述了领域驱动设计战术战略设计的基础知识,并通过事件风暴工作坊的介绍给出了实际的例子,在编码实现中给出了一些落地的挑战和解决方法,同时为组件设计提供了一种思路。
2022年10月