2022-11-16 23:54:36    28    0    0

什么是战术设计

按照上一回合,我们使用事件风暴的方式,把我们一个复杂领域的微服务划分方案解决了。但这个时候,如果要你写代码,那肯定不可能,因为这个东西距离代码实现还有很长的路。那我们如何落地代码实现,这就战术设计需要解决的问题。战术设计负责最后的落地阶段。

用界限上下文将用户故事连起来

我们用划分出来的界限上下文(不好理解的话可以理解成微服务)将用户故事连起来,这一步我们把限界上下文当成黑盒,确认它们之间是如何协作的,但是我们不关心限界上下文内部的情况

这里借用一下尤达老师自动售卖机的例子,围绕自动售卖机为例子梳理出来的限界上下文,自动售卖机系统的界限上下文如下:

然后我们借助架构师,PM,领域专家梳理的用户故事,用限界上下文交互的方式将整个用户故事串联起来。

我们这里只用到一个用户扫码在自助售卖机上扫码支付的例子来串联一下。注意,有的时候我们可以只针对业务的几个关键用户故事进行串联,串联的时候需要把真正的研发人员也召集到一起进行讨论了,因为这里距离研发已经很近了

 

值得一提的是,有的同学可能会问,这里为什么没有用户上下文,注意,这只是用户故事之一,如果列举全的话,比如涉及免密支付的场景,就肯定会出现用户上下文。包括运营上下文,如果涉及商品补货的用户故事,肯定会出现运营上下文。

 

注意:这张图必须是多个限界上下文负责,包括即将分配的研发人员一起讨论,因为这里已经可以看到很多开发关注的东西了

1)图中连线表示是接口操作,箭头表示自己,或者对方必须要提供的接口协议,比如设备上下文的负责人必须提供一个弹出商品的接口

2)据我的经验,一般业务会有5-8个核心流程,这些核心流程都需要串联一遍,方便我们再检验一遍我们划分出来的限界上下文是否合理

 

如果经过充分讨论,没问题的话,这一步基本把限界上下文的职责界定清楚了,其实相当于限定了微服务功能点。比如支付下文的功能描述

1)对外:对业务方提供创建支付订单接口,创建成功返回支付链接

2)对外:提供支付结果回调,支付结果会异步回调业务方

3)内部:调用微信开放平台接口,获取微信支付code

4)内部:接受微信支付回调结果,变更支付订单内部状态

 

所以接下来,再深入到每个限界上下文内部,如何实现这些功能。

限界上下文内部实现

这一步开始充斥大量的细节,好比我们平时用微信开放平台进行支付,微信开放平台暴露给我们的是非常简单的接口。但是其内部的实现,我们做技术的应该不难猜

blue    2022-11-16 00:27:33    6    0    0

领域驱动设计

什么是领域驱动设计

 《架构整洁之道》里面有一句话定义业务逻辑

严格来讲,业务逻辑就是程序中那些真正用于挣钱/省钱的业务逻辑与过程。更严格得讲,不管这些业务逻辑是计算机上实现的还是人工实现的,他们在省钱/赚钱上的作用是一致的

软件开发通常被应用到真实世界中已经存在的自动化流程。从一开始,我们就必须明白软件脱胎于领域,并且跟领域密切相关。在我们开发软件的时候,当然也可以直接坐下来敲代码,但仅限于在开发价值不大的软件时。

为了创建一个复杂的软件,你必须知道这个软件究竟是什么,在你充分了解金融业务是什么之前,你是做不出一个好的银行业软件系统的,你必须理解银行业的领域

谁了解业务

没有丰富的领域知识能做出复杂的银行业业务软件吗,没门!那谁了解银行业业务呢?软件架构师吗?不,他只是用银行来存储自己的金钱,软件开发人员?别为难他了。只有银行业务人员才是这个领域的专家,他们知道所有的细节,所有的困难,以及所有的规章。这些就是我们永远的起始点:领域

模型

模型是软件设计中最基础的部分,我们对领域的所有思考过程被汇总到这个模型中,我们需要就这个模型跟领域专家就进行交流,跟资深的设计人员进行交流,跟开发人员进行交流,模型是软件的根本。要表现这种模型,常见的方式是将模型图形化:图,用例等。我们需要用模型交流

软件设计方式

软件设计有不同的方法,其中之一是瀑布设计法。这种方法包含了一些阶段,业务专家提出一堆需求同业务分析人员进行交流,分析人员基于那些需求来创建模型并将结果传递给开发人员,开发人员根据他们收到的内容开始编码。瀑布设计法中知识只有单一的流向。这种传统方法虽然应用很多年,但是还是有一定的缺陷和局限。主要是业务专家得不到分析人员的反馈信息,分析人员也得不到开发人员的反馈信息。

另一个方法是敏捷方法学,这些方法不同于瀑布方法的一堆动作,产生背景是预先很难确定所有的需求,特别是需求经常变化的情况。敏捷开发试图解决另一个问题被称为"分析瘫痪",团队成员会因为害怕做出任何设计决定而无济于事。敏捷开发者反对预先设计,相反,他们应用大量的灵活设计。敏捷开发也有自己的不足,

敏捷开发缺乏真实可见的设计原则,由开发人员执行持续重构会导致代码难以理解或者难以变更。瀑布流会过度工程,过度工程可能会带来另一种担心:害怕做出深度,彻底的设计。

领域驱动设计

本书介绍领域驱动设计原则,这些原则会增进对领域内复杂问题进行

2022-11-15 00:35:22    15    0    0

什么是限界上下文

这里要吐槽一下目前网站上的各种不说人话系列的文章,基本上全是书本上抄过来的,根本不是正常人能理解的,DDD难怪会变成玄学,读者在搜读文章的时候几乎是奔溃的状态。

为了好理解,也可能有局限性,但为了防止被喷,这里声明只是个人理解,不保证正确性。

假设我们面对一个庞大的系统,或者我们要从庞大的系统解放出来,我们首先想到的就是微服务,但是这里有一个难点,如何合理拆分微服务?我们就冲这个问题去解释。我们把限界上下文拆开看,限界表示划分,划分上下文,上下文表示名次。

1)我们拆分出来的一个个微服务就是一个个上下文

    a)支付微服务 = 支付上下文

    b)交易微服务 = 交易上下文

    c)风控微服务 = 风控上下文

2)上下文映射就是微服务直接调用方式,或者说调用协议

    a)假设你们部门很强势,那你的微服务永远不变,让别人适配你的协议,微信支付协议就长这样,你爱用不用

    b)假设你们部门很弱势,你要调用别人的微服务,那你加防腐层(适配器)去调用它的服务,你要去适配微信支付协议

3)关于微服务划分合理性,我们可以用事件风暴法来指导我们拆分合理性


所以当我们需要拆分一个庞大系统的时候,首先用事件风暴把微服务合理划分出来,然后确定微服务调用关系和协议。

事件风暴

事件风暴,首先得定义什么是事件,然后围绕事件进行头脑风暴。

什么是领域事件

领域事件是领域中发生的任何领域专家感兴趣的事情。这些事件我们从用户故事中就可以提取出来,比如我们用电影票务系统来举例,这里就会产生以下事件

1)当售票员标记座位后,产生一个座位已标记事件

2)当用户确认座位后,产生一个座位已确认事件

3)当售票员收到用户的确认之后,产生一个座位已预约事件

4)可能后续还有支付动作,会产生座位已支付事件

值得一提的是:领域事件不是技术概念,而是作为通用语言的一部分和领域专家,运营人员,产品团队达成一致。这一点很重要,很多技术团队内部有各种Event事件,然后这些Event事件和产品团队介绍的时候,需要加各种补充解释,其实完全没必要,领域事件在初期就需要和产品团队达成一致。

注意,领域事件的几点约束:

1)领域事件是表示已发生的动作,通常是名词+过去式动词,比如座位已标记,座位已预约

2)领域事件一定要和领域专家相关,不是一个技术概念

但是,基于用户故事的方式,可能不能完全覆盖领域事件,所以接下来介绍一种新的挖掘领域事件的

2022-11-10 00:12:20    12    0    0

DDD系列-领域划分

什么是领域划分

领域划分是以分离关注点为原则对问题空间的划分。领域划分必然会划分成很多个子域。

子域是领域中某个方面的问题和解决它所涉及的一切

其实我们面对复杂问题的时候,都会采用这种方式,纵观我们整个社会,有各行各业,有餐饮业,保险业,房地产业,然后每个领域解决自己领域内的问题,最后连接成一个高效运作的社会形态。

同样,我们做互联网如果要达到高效运作的效果,就必须做到各司其职,做细做精。但是这里有一个难题,如果做领域划分,就跟社会上不会平白无故出现一个保险业一样,它肯定有社会普遍的问题,最终人们抽象成了一个保险业领域来解决保险相关的所有问题。

假设没有领域划分

其实很多业务初期的时候,都是没有领域划分的,基本是按照需求来。接下来我们看下,如果这样长期演变下去会有什么问题。笔者是做医疗的,还是用那个医疗的用户故事来讲解。

我们假设梳理出来了下面几个顶层用户故事

1)收集用户的病情

2)用户购买订单

3)指派订单给医生

4)医生接单

5)医生和患者IM对话

6)医生开具处方

7)用户购买处方药

假设在一个项目压力比较大,而研发leader又没有做太多思考的情况下,很容易将这6个顶层用户故事派发给6个研发同学,也就是按照需求来划分指责

这样会导致:

1)问题点和领域知识重叠,比如"用户提交订单"和"用户购买处方药"都会处理支付问题,支付领域的问题他们都需要处理一遍,痛苦加倍

2)基础模型重叠,比如"收集用户病情"和"医生接单"和"用户购买处方药"都会用到就诊人和病例的模型,他们很可能自己定义一套内部的模型,将来想打通简直就是噩梦

3)难以区分核心和非核心,如果你作为一个在线医疗系统,结果一半的人力在支付问题上,那可想而知,医疗真正的核心问题谁来解决?

领域划分解决的问题

很多人可能已经想到了,建立通用底层服务,然后上层业务统一调用底层服务,这样可能有一定的思路了,但是还不够彻底,因为一旦涉及到服务,就会涉及到拆分的依据和原则,这个问题就相当复杂了。所以我们假设带着这个思路去拆分一下上面的用户故事划分的域。

1)交易域,首先能想到的是交易域,这里处理支付相关的问题,所以支付领域的问题汇聚到这

2)订单域,这里包括订单,商品等重要的信息

2)对话域,作为医生和用户的医疗IM通用工具,这里需要处理各种个性化的通信协议,比如医嘱,病例等医疗特性内容

3)医生域,医生具备接单,看病,开处方等能力

所以还是上

2022-11-07 00:36:02    7    0    0

Map数据竞争

直接看一段例子

package main

import (
    "sync"
    "fmt"
)

func main() {
    m := map[int]int{}
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            if _, exists := m[i]; !exists {
                m[i] = i
            }
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println(m)
}

这个例子可能有三种输出情况

可能输出情况

况1

输出结果:正常

map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9]

情况2

报fatal,错误信息显示存在读和写冲突

fatal error: concurrent map read and map write

情况3

报fatal,错误信息显示写冲突

fatal error: concurrent map writes

原因

map在go的内部实现其实是哈希表,哈希表结构体定义。重点关注flags字段。这个字段在处理冲突中起了关键作用。

type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // app
2022-11-04 16:31:33    5    0    0

DDD系列-领域建模

为什么要建模

不管你有没有想过这问题,领域模型一直都在,与是否发现它无关。假设我们去专卖店买手机,买完手机之后留下了自己的名字和电话号码,并且店员用一张单子做记录。

 

什么人需要查看这个单子呢?店员A需要查看,店员B也需要查看,老板也需要查看单子计算今天的销售额。而且大家的关注点不一样,店员要求名字和电话号码对上就行,可是老板却需要售卖金额。这个时候大家都要看的情况下,必须保持格式统一,否则就乱套,要不是少记电话号码,就是少记金额。这个时候老板和店员其实默默的形成了一个心智模型。订单=名字+电话号码+商品+金额

下面是店家和老板达成约定的订单:

名字电话号码商品金额
张三13249383944iPhone13¥9000
李四18393843432小米9¥2100

 

假设手机卖出去之后,客户需要维修手机,于是客户来到店里询问店员,店员查核查了订单记录,然后引导用户到维修部。维修部发现手机过了保修期,所以他写了一个维修单让顾客去缴费。这个时候是维修员和收银员达成的心智模型维修单=名字+电话号码+商品+维修费用

维修人员和收营员约定的维修单:

名字电话号码商品维修费用
张三13249383944
iPhone13
¥500
王五13637673434ViVo C9¥600


这里对应一下有的公司的现状:这一点并不陌生,如果我们将时间拉回到10几年前,我们眼中的领域模型其实说白了就是数据库设计。这种开发方式是基于数据模型的,我还记得很经典的一句话:只要数据库定了,对应的业务逻辑就定下来了。这个时期面对需求的时候,表现就是开发人员一开始就围绕数据库进行设计

 

但是,这个时候他们一定要接受抱怨并且开始动脑筋,抱怨就是:

1)维修人员抱怨为什么用户下单的时候有的信息,在维修的时候还需要再抄一遍

2)收营员开始抱怨,这个用户维修单上的信息和订单上的信息不一致,总是搞错,每次都需要重新核对

这时候你会发现用户信息到处都有,需要做一个客户模型,客户信息独立于订单和维修单,并且你可能也发现,用户拿身份证核对就好了,所有部门共享客户模型。

这时候你又在思考,好像维修单里面的大部分信息在订单里面都有,能不是直接复用订单数据呢,于是你可能做一个订单模型。用订单ID去和其他服务关联。

 

所以,及时你没有使用任何的建模概念和工具,有的时候其实你已经在做这一类的事情了。但是你的目标和建模的目标是一致的,建模的作用

1)

2022-10-24 21:52:26    17    0    0

安装prometheus

mac安装方式

1.下载安装包

https://prometheus.io/download/

 

2.解压运行

解压后,运行./prometheus --config.file=prometheus.yml

这里mac系统可能会碰到无法打开的问题,按照如下操作之后,再次运行命令

a.点击屏幕左上角的苹果图标,选择菜单:系统偏好设置...。
b.打开系统偏好设置界面,点击"安全性与隐私"->"通用"。
c.在窗口底部会看到:已阻止使用“XXX”,因为来自身份不明的开发者。点击后面的"仍要打开"按钮

 

3.访问

浏览器地址输入:http://localhost:9090/,即可访问

使用prometheus监控go应用

默认监控项

在go项目中引入prometheus之后,默认已经有监控项了,我们可以在prometheus的监控面板中看到,比如下面的例子

1)编写一个最简go应用

package main

import (
	"net/http"

	"github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
	http.Handle("/metrics", promhttp.Handler()) //暴露 metrics 指标
	http.ListenAndServe(":8091", nil) //启动Server
}

 

2)查看默认指标

2.1. 运行上面的go程序

go run main.go

2.2 浏览器访问http://localhost:8091/metrics 然后查看默认指标输出

这里我们关注promhttp_metric_handler_requests_total指标,表示http访问量,每次刷新这个指标会+1。


3)将go应用节点添加到prometheus的prometheus.yml配置

追加配置到prometheus.yml中

  # 这里新增一个监控Go应用节点
  - job_name: "gomonitor"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ["localhost:8091
2022-10-21 22:16:57    15    0    0

DDD系列-建模设计全流程

和以往的开发模式不一样的是,DDD不是从用户需求开始的,DDD的起点是用户故事,值得一提的是,为了避免瀑布流模式的缺点,从用户故事到战术设计不是一条不可逆的路,相反,最好的模式应该是斡旋前进。

挖掘用户故事

挖掘用户故事必须包含产品团队,领域专家,研发团队共同参与,这里值得一提的是,领域专家这个角色的定义,这里是相关领域有资深经验的专家,比如

1)针对医疗行业,那么医生就是领域专家

2)针对教育行业,那么老师就是领域专家

但是,普遍存在的一个现象是,公司里并没有这些领域专家,往往PM承担了这个角色,这样在一定程度上也是可以的。但是还是建议每间隔一段时间邀请真正的领域专家来介绍一些这个领域的故事。

 

定义用户故事

用户故事属于问题空间的描述,用户故事描述公式为

公式: 作为什么人,我希望做什么,达到怎么样的一个目的。

例子:作为用户,我希望能够通过手机在线上和医生交流,从而更加便捷的清楚病情

 

复杂版

公式:合适的人(who),通过合适的渠道(channel),将合适的内容(what),在合适的时段(when),发送给合适的客户(whom),产生一定的效果和反馈(effect)

例子:复杂的一句话用户故事,甚至可以把模型建立出来。

电商运营人员 希望 对7天内没有回购的用户 在 周六晚上8点 的时候 通过 短信提醒 的方式 发送一张优惠券 并且希望用户回来再次购物并 评价购物体验

例子

这里列举一个电影票务系统的例子,假设我们并不知道票务系统长什么样子,那么我们(架构师)就去聆听另一方(领域专家或者产品经理)陈述我们的电影票务系统

这里的话术是用简单的公式: 作为什么人,我希望做什么,达到怎么样的一个目的。注意,中间一定要问答和讨论,最后形成一个图形表述的用户故事。

例如,这是一个电影票务系统的用户故事描述,这种图会作为架构师,产品经理,领域专家讨论之后的用户故事图形表达。

建立通用语言

先来看一个笑话:

一飞机在四川上空遇到故障,四川空管台问:“出了啥子问题?”

机上发出了求救信号:“MAYDAY!MAYDAY!”

空管回复:“没得问题就好。”

(四川话“没得”和“MAYDAY”发音差不多,“没得”就是没有,“MAYDAY”是国际通用的无线电求救信号)

这个笑话在平时的工作中非常常见,基本产品团队一套术语,研发团队一套术语,经常出现两个平行时空的对话。有的研发或者产品能发现这个问题,当讨

2022-10-20 00:24:12    18    0    0

DDD系列-如何判断要不要使用DDD

如何判断要不要使用DDD

当我们介绍业务背景的时候,一定要足够复杂,才可能继续讨论DDD如何解决这种复杂度的。如果是一个掷骰子游戏,那肯定不会用DDD去实现,这种例子只帮你理解一些概念。

这里参考尤达老师的零售项目,整个背景可以用下面一张图表示

1)运营人员通过电脑操作投放售卖机

2)用户通过手机支付的方式购买商品

3)安装人员按照安装单到指定地点安装售卖机

4)商务会去各商场洽谈合作售卖机事宜

5)配送人员必须及时按照配送单补充售卖机商品

 

一个智能零售主要包含以上功能,注意,这里不包含细节,但是所有的角色都在里面,需要讨论每个细节的时候,会再次深入到内部。比如你需要考虑购买问题的时候,用户是使用微信,支付宝,或者硬币支付,或者小程序会员的方式进行购买,再或者,这里搞打折活动......

 

 以及同理,下面是一个简化的在线医院系统,目的是让用户足不出户即可达到看病的目的

这个系统也很复杂,模拟了一套线下看病的流程

1)用户在手机上输入足迹的病情,类似去医院对着窗口说自己感冒嗓子疼

2)助手,类似那个窗口给你挂号的,会告你去什么科室看病,这里直接是指派给一名医生了,简化了一下

3)医生,医生进行接诊操作,类似现实中你坐到了医生的对面,然后就行一些谈话

4)医生给你开局药品清淡,然后你进行线上支付,来购买药品

5)药品配送人员会将这些药品送到你的家门口

 

整个系统是极其复杂的,任何一个环节拿出来都可以构成一个小系统,比如开药的环节,就会引入处方药和非处方药等领域的概念,另外他们也需要处理监管的问题

 

总结,以上两种情况都非常适合使用DDD,理由是他们足够复杂,并且相应的现实世界已经有一套成熟的模型,软件开发人员必须亲自搞懂现实中的这些系统是如何运作的,它们的关系是怎样的,然后将这一套模型映射到我们的软件系统中。

我们通常说代码尽量不要复用,软件模型也是同样的道理,现实生活中有的模型尽量参考,改进,避免盲目创建出一个毫不相干的新模型。



 

 

2022-10-18 23:54:17    13    0    0

DDD系列-聊聊敏捷开发模式的缺点

敏捷开发模式

在我以前的软件开发经验中,很多软件都是基于分层思想+贫血模式设计的(如下图),也许从入行开始就一直是这样,也并没有发现什么问题。

一句话总结以前的开发思路:"表建好了,代码也就写好了"

敏捷研发流程

一般从需求开始(其实是局部的需求,RD往往没有全局信息),产品同学将需求分析完和开发同学进行需求评审,评审完毕后开发同学开始基于需求进行技术方案设计。这时候一般会落到数据库设计,将库表设计完毕。设计完表之后,我们常说的"Dao层"就有了,接下来就进行逐层向上开发,最后封装出一个api接口。

 

这时候你会发现真的是"表建好了,代码也就写好了",数据驱动开发或者说是一种基于数据模型的开发。这种开发模型所有的业务逻辑都堆砌在Model层,其实可能开发者本身都没理解这一层的含义,因为Controller层不能直接穿透到Dao层,所以不得不在Model层继续傻瓜式封装一下dao层

敏捷研发最有价值的产出

由于敏捷开发基本是基于数据驱动开发的,所以它的设计基本就是敏捷需求开发的最有价值的输出了,例如客户下单的需求为例子

敏捷开发的舒适期

这种开发方式在创新项目的前期,非常适合用这种方式,因为首要任务是项目能够活下来,不要被淘汰掉。这种模式就非常适合低成本

快速迭代起来,并且新人这个时候是友好的,从api入口开代码能够一口气看下来,逻辑基本上就明了了。

敏捷开发的阵痛期

如果跨过了舒适期,来到阵痛期,恭喜你,至少项目活下来了。这个时间开发和迭代变得非常困难了,更有甚者,一张表的扩展字段可能达到恐怖的100多个了(笔者曾经所在的公司)。这个时候没有一个新人来了不骂娘的,因为根本不可能是正常人能理解的东西了。整个大项目来到一个大单体时代

1)大单体-拆表:治标不治本,只是解决容量问题,对业务的复杂度是没有贡献的

2)大单体-拆微服务(伪服务):为什么说是伪微服务,把dao层暴露api接口作为一个一个服务,然后让大单体接入,后面介绍DDD会知道,这只是改变了调用方式,根本对减少业务复杂度没有任何贡献,反而因为网络请求增加了复杂度

3)大单体-拆微服务(局部拆分):比如电商中的收件人作为微服务,然后包含了各种冲突,融合,关系等完善的服务。然后用这个新微服务潜入到大单体中。这也是死路一条,后面介绍DDD会知道,它改变了模型(虽然作者可能都不知道隐含的模型),模型的改变必然导致

1/4