初识DDD 之CQRS

最近在学习实践DDD,接触到一个新的概念CQRS。以前以为就是读写分离,是基础设施层数据库层面的事情。学习以后才发现,并不只是这样,更应该是App应用层面的事。

缘起从github上找了一个DDD的symfony的例子,想参考一下,用于重构目前的项目。看了介绍和代码以后,发现了他的作者的西班牙语的油管视频。结合自动字幕翻译,觉得还不错,然后看了一下他们写的书。原版要60欧,于是在github上下载了一本电子版 Domian Driven Desgin in PHP。以下是阅读的笔记。

MVC模式的缺点

MVC用了那么久了,最大问题是controller过于庞大。违反了分离原则(Separation of Concerns)。补救的办法是使用依赖倒置。由于controller包含了过多的领域内容。所以首先拿领域模型下手。很多领域模型依赖底层的基础架构,比如数据库。所以按照Bob大叔(Robert C. Martin)的依赖倒置原则,高级模块(领域)不该依赖低级模块(基础设施),而且应该让它依赖抽象,即提出Repository。然后六角形架构被提出了。所有才有后来的 CQRS Event Sourcing

初识DDD 之CQRS

写模 型 Write model.

也叫Command model, 1

被改造为基于事件的写模型。本来直接写入数据库动作,改为发布一个写入事件。

record -> apply -> publish

而且写入事件直接找领域模型里定义。

读模型 Read model

被简化为从ElasticSerach这样的缓存读取。

同步

即然读写分开了,那么两个模型如何实现同步呢。引入了新概念Projection (投射器?)。基本上就是监听事件。当符合的事件被监听到了,就会调用投射器的投射动作。

投射的过程很可能是异步的,就是说会有延迟。为了解决延迟,在Repo里的save方法内,创建一个transaction(过程),把存储,事件放在一起。然后用投射器投射保存的事件。

最终解决了读模型和写模型之间的一致性(consistencies)问题。

Evnet Sourcing 事件溯源

就是可以通过记录完整的event可以还原一个状态。有点像数据库的bin log。

Code Review 收获

今天请Ben对项目的重构Branch进行了Review。收获是很多的。

传入参数和返回值,尽量使用Interface

当使用match分支处理情况的时候,也要和switch类似,加上处理默认default的情况

避免使用动态生成的方法和类

动态的方法和类就是指名称包含变量的。比如

setFirstName(), setLastName()

这样的方法,为了偷懒,我使用set加上一个变量的名字的方法,比如 set$var()

这样的方法或者类不利于静态检查,应该避免使用。

Domain领域应该和外界尽可能的隔绝。

Domain内不该知道什么是REST,DB,gRPC等具体实现的技术概念

应该只使用传入的Interface的公开方法

简化复杂的DI的声明

使用静态分析工具 PhpStan

使用docker volumn共享卷以及同步

启动的时候,报错,说一个卷没有。需要手动创建。然后在docker compose里面看到,在多个容器中都使用了一个卷名。命令行中有提示,如何手动创建一个卷。

尝试创建后发现是空的,再看它还有一个docker-sync配置。然后安装docker-sync工具。制定后启动,然后它就留在后台运行了。

再启动容积,这次卷就被挂上了。

docker volume 命令

docker-sync 工具

在消息队列中使用Symfony的序列化

PHP中,如果需要把内存中的变量进行保存,以便后续继续使用,那么就要把数据序列化。

PHP提供了 serialize()  和 unserialize()  这两个原生方法来完成。但是这样有个限制,就是要求写入和读取的时候使用同样的方法。就是说只能使用PHP原生的反序列化函数来读取PHP原生序列化的数据。 有时候我们写入是PHP,读取的时候可能是别语言,所以需要一种更加通用的格式。

比如,在项目的写入队列的过程中,我们打算用Symfony提供的序列化器代替PHP原生的。我们选择序列化的格式为JSON。但是出现了问题,发现某些属性的数据丢失了。原来需要对序列化的对象进行处理。

要让消息能被JSON序列化需要确认以下

  • 消息对象需要实现一下特定接口 (非必要)
  • 消息对象能被正确encode和decode
  • 消息对象的构造函数必须为public,且只使用标量值。如果必须要使用类,需要确保这些类实现了stringable接口。

消息需要实现特定接口

SyncCommandInterface::class,
AsyncCommandInterface::class,
EventInterface::class,
QueryInterface::class,

具体措施

消息的构造函数必须为public,且使用仅使用标量作为参数

为什么消息对象的构造函数必须为public,为啥必须用标量参数。

我猜想,因为当消息被反序列化的时候,系统只会用最简单的new方法来重新创建消息实例。

  • public类的属性

使用readonly修饰属性防止意外修改

使用标量值

对象必须实现stringable

减少复杂性。比如不使用实体对象entity,而使用id. 这点在Symfony的文档内也有介绍。

参考

分析feature toggle类

项目中有些功能,只在特定调价下可供使用。所以Ben写了一个feature toggle类。故名思义就是提供功能切换的类。

背景需求

1 需要按照storefront来实现功能的开关

2 对于没有通过storefront的,还可以通过一个optimizely进行开关

首先实现了一个上下文对象,context。context里包括了 userid 和 storefront。

然后定义了一个toggle接口。只含有一个enable方法。然后实现了3个具体的toggle类。他们是

  • optimzly
  • storfont
  • oneof

配置里,使用oneof来注入到service中。oneof的构造函数接受一组(迭代器)toggles。当他被询问enable的时候,它会逐个询问它的迭代器里的每个toggle。只要有一个返回true,它就返回true。所以名字叫做oneof。

配置oneof的时候,给他配置2个toggle。一个是storefront_de。一个是optimzley。
然后在storefornt_de只配置里storefront
optimizley 是依赖外部的optimizely服务的。所以配置需要一个key和客户端。

整个实现结构清晰。明确。非常赞。

在Symfony中如何获取所有的message handler

在测试的时候,需要获取symfony的里所有的的message handler,该如何做到呢?

通过 messenger.message_hanlder 就可以。

比如你的测试需要测试所有的 message handler

1 在容器中注册一个provider ,比如 MessengerHanlderProvider

  App\Tests\Functional\StaticCodeAnalysis\MessengerHandlerProvider:
    public: true
    arguments:
      $handlers: !tagged_iterator messenger.message_handler
2 然后实现该provider类 class App\Tests\Functional\StaticCodeAnalysis\MessengerHandlerProvider ,它只有一个作用,就是接受一个返回所有handler迭代器,它的构造函数的为 !tagged_iterator messenger.message_handler
  public function __construct(iterable $handlers)
    {
        $this->handlers = iterator_to_array($handlers);
    }

意思就是返回给我所有的tag标记部位MessageHandler的类。因为我们在用Messenger定义消息的hanlder的时候,都按照教程,给每个定义为hanlder的类加上这个标签。比如:

#[AsMessageHandler]
readonly class FinalizeCancellationsHandler
{... }

#[AsMessageHandler]
class SyncCancellationsHandler
{...}

这样就可以轻松获取所有的messenger 的handler了。

3 在测试中需要用到返回所有消息handler的时候,只要问容器去要就可以了。比如

static::getContainer()->get(MessengerHandlerProvider::class);

相关视频教程

https://symfonycasts.com/screencast/messenger/message-class

Manjaro启动慢的问题一直没有解决

因为发生在系统升级后,最好确定一下上次升级发生了什么

下来检查当前的内核和驱动版本

mhwd -l
mhwd -li
mhwd-kernel -li

ChatGPT介绍说:

这三个命令都与 Manjaro Linux 发行版中的硬件和内核管理有关。

  1. mhwd -l: 这个命令用于列出可用的硬件驱动程序。“mhwd“ 是 Manjaro Hardware Detection 的缩写,它是用于自动检测、安装和配置硬件驱动程序的工具。通过运行 mhwd -l,你可以看到系统支持的各种硬件设备(如显卡、声卡等)的驱动程序列表。
  2. mhwd -li: 这个命令用于列出当前安装的硬件驱动程序信息。通过运行 mhwd -li,你可以获取有关已安装硬件驱动程序的详细信息,包括驱动名称、版本、设备等。
  3. mhwd-kernel -li: 这个命令用于列出已安装内核的信息。Manjaro Linux 支持多个内核版本,你可以在启动时选择使用哪个内核。通过运行 mhwd-kernel -li,你可以查看当前系统中已安装的内核列表以及每个内核的详细信息。

这些命令对于管理硬件驱动程序和内核版本在Manjaro系统上非常有用,可以帮助你了解系统中的硬件配置以及当前的驱动程序和内核状态。

但是目前不能启动,系统卡在了无线网卡获取ipv6这里。

1 从USB启动

版本是Manjaro 6.1.30-1-MANJARO

2. mount 硬盘
(参考 https://forum.manjaro.org/t/howto-chroot-from-or-into-any-linux-distribution/34071 )

打开终端 切换到root (su)
创建临时的 /chroot (mkdir /chroot )
然后mount /dev/sda2 /chroot 
然后mount其他

mount --bind /proc /chroot/proc
mount --bind /dev /chroot/dev
mount --bind /sys /chroot/sys

chroot /chroot



考虑到卡在无线网卡哪里,打算先关掉无线网卡的模组。
引导系统的是linux61,另外mhwd-kernel -li 显示还有linux64.
(告诉你如何在grub启动的时候,显示输出 https://forum.manjaro.org/t/really-really-slow-boot-time/118130/4)

我的无线网卡是,当初安装是费了周折的(教程)
Broadcom BCM4322 802.11a/b/g/n Wireless LAN


有人指出原因是没有swap分区。添加了swap文件后(教程 添加swap文件代替分区),还是无效。

决定重装系统

1. 安装linux6.1
2. 第一次启动后,关闭grub的 quiet splash 重新生成配置
3 重启 应该可以看到文字界面,不再出现启动画面
4 uname -a 当前内核
Linux mbp51 6.1.30-1-MANJARO May 24 22:51:44 UTC 2023 x86_64 GNU/Linux

5 升级系统 sudo pacman -Syyu
6 第一次启动失败 系统卡在登录画面没有反映。 看到有短暂的fireware错误提示
7 强行重启 正常 登录后 uname -a 查看版本 
  Linux mbp51 6.1.44-MANJARO #1 SMP PREEMPT_DYNAMIC Wed Aug 9 09:02:26 UTC 2023 x86_64 GNU/Linux

8 系统提示有新内核 。先查看当前内核 mhwd-kernel -li
返回  6.1.44-MANJARO(linux61) . 升级内核教程
使用GUI打开内核管理,选择6.4.9-1 
安装完后,没有其他提示。手动重启系统。
再用uname -a显示系统是
Linux mbp51 6.4.9-1-MANJARO #1 SMP PREEMPT_DYNAMIC Web Aug 9 08:32:12 UTC 2023 x86_64 GNU/Linux

看来内核升级好了

9 如果要安装无线网卡,还需内核的头文件(安装内核的头文件 教程)
sudo pacman -Sy linux-headers

我选择 6.4 完成

10. 选重启。系统退出,但是最后显示卡在了 Rebooting. 
11. 强行按电源5秒 重启 但是系统卡在了某个地方 和我上次差不多。看来还是内核的问题。
12. 再次强按电源重启 能计入图形登录界面了 登录日志如下: http://ix.io/4Dm8 
13. 测试重启。 还是卡在Rebooting.那。这个故障和这个描述很类似。某个firmware升级后发生的。再结合我上面的log,我发现有关于B43的firmware错误提示。我觉得是无法找到网卡的驱动
 


Aug 13 17:31:25 mbp51 kernel: b43-phy0: Found PHY: Analog 8, Type 4 (N), Revision 4 Aug 13 17:31:25 mbp51 kernel: b43-phy0: Found Radio: Manuf 0x17F, ID 0x2056, Revision 3, Version 0 Aug 13 17:31:25 mbp51 kernel: b43 ssb0:0: Direct firmware load for b43/ucode16_mimo.fw failed with error -2 Aug 13 17:31:25 mbp51 kernel: b43 ssb0:0: Direct firmware load for b43/ucode16_mimo.fw failed with error -2 Aug 13 17:31:25 mbp51 kernel: b43 ssb0:0: Direct firmware load for b43-open/ucode16_mimo.fw failed with error -2 Aug 13 17:31:25 mbp51 kernel: b43 ssb0:0: Direct firmware load for b43-open/ucode16_mimo.fw failed with error -2 Aug 13 17:31:25 mbp51 kernel: b43-phy0 ERROR: Firmware file „b43/ucode16_mimo.fw“ not found Aug 13 17:31:25 mbp51 kernel: b43-phy0 ERROR: Firmware file „b43-open/ucode16_mimo.fw“ not found Aug 13 17:31:25 mbp51 kernel: b43-phy0 ERROR: You must go to https://wireless.wiki.kernel.org/en/users/Drivers/b43#devicefirmware and download the correct firmware for this driver version. Please carefully read all instructions on this website.

14 再次强行重启。可以进入。开始安装驱动 (教程

sudo pacman -Sy broadcom-wl-dkms

显示安装完成 b43 等等。输入sudo reboot,最后还是卡在rebooting那里。

15. 强行重启动,卡在了 usb 4-1.3那里. 按下重启,画面又刷新了一次。这次停在systemd-shutdown 20秒钟后 然后停在shutdown[1]: Power Off

重复了几次都是一样。看来就是6.4的内核和网卡驱动无法兼容。尝试退回到6.1内核。

类似问题,这个人同样有网卡bs4322驱动不了的问题

决定再重来

安装后的log: http://ix.io/4Dne

里面有提示

b43/ucode16_mimo.fw

b43-open/ucode16_mimo.fw

这2个firmware没找到,并给了链接。根据命令 lspci -nn -d 14e4: 查到我的网卡的PCI id是

14e4:432b

PCI-IDSupported?Chip IDModesPHY versionAlternative
14e4:432bpartiallyBCM4322a/b/g/nN (r4)wl
表示部分支持

1 按装6.1的头文件,伴随一次升级:
sudo pacman -Sy linux-headers

本次升级后log http://ix.io/4Dno

sudo pacman -Sy broadcom-wl-dkms

这次安装显示 dkms install –no-depmod broadcom-wl/6.30.223.271 -k 6.1.44-1-MANJARO depmod 6.1.44-1-MANJARO

重启

安装成功 log 已经没有Broadcom 43xx driver loaded错误提示。

总结

似乎只要1安装头文件 ,2装broadcom-wl驱动即可。

第一次启动测试

拔掉有线网卡和usb引导 成功

第二次启动测试