DDD系列 实战一 应用设计案例 (golang)

news2025/7/15 4:30:26

DDD系列 实战一 应用设计案例 (golang)

基于 ddd 的设计思想, 核心领域需要由纯内存对象+基础设施的抽象的接口组成

  • 独立于外部框架: 比如 web 框架可以是 gin, 也可以是 beego
  • 独立于客户端: 比如客户端可以是 web, 可以是移动端, 也可以是其他服务 rpc 调用
  • 独立于基础组件: 比如数据库可以是 MySQL, 可以是 MongoDB, 甚至是本地文件
  • 独立于第三方库: 比如加密库可以是 bcrypt, 也可以是其他加密库, 不会应为第三方库的变更而大幅影响到核心领域
  • 可测性: 核心领域的 domain 是纯内存对象, 依赖的基础设施的接口是抽象的, 可以 mock 掉, 便于测试

怎么实现呢? 下面我根据一个案例来一步步展示通过 DDD 重构三层架构的过程

案例描述

本案例将会创建一个应用, 提供如下 web 接口 (目前使用 golang 实现)

  • 注册 POST /auth/register
  • 登录 POST /auth/login
  • 获取用户信息 GET /user
  • 转账 POST /transfer

DDD 是为了复杂系统变化而设计的, 如果系统简单, 需求变动不大. 那么脚本代码才是最好的选择, 不仅能快速开发, 而且理解.

所以这次案例里, 为了展示 DDD 设计的高拓展性, 需求主要强调 变化 的场景.

注册

  • 目前通过账号和密码注册, 以后可能增加根据手机号邮箱等注册
  • 用户的密码需要加密, 目前使用 hash 加密, 后面可能使用其他加密
  • 目前保存数据使用的 MySQL, 以后可能使用其他数据库

登录

同理注册

鉴权

除了注册登录, 其他接口都需要鉴权

  • 目前使用 redis 鉴权, 以后可能会更换为 jwt 鉴权, 需要有切换的能力

转账 (核心)

一个用户转账给另一个用户

  • 需要支持跨币种转账
  • 目前转账的汇率从第三方 (微软的 api) 获取, 以后可能会考虑变更或者做缓存
  • 目前转账收取手续费 10%, 以后可能根据用户 vip 等级收取不同的手续费
  • 需要保存账单, 以便审计和对账用
  • 目前账单是保存在 MySQL 中, 以后可能会考虑保存到其他数据库或者消息队列消费

接口

  • 目前提供 web 接口, 以后可能会提供 rpc 或者其他接口

什么是 MVC 三层架构?

对于一般的后端开发来说, MVC 架构都不会陌生. 现在几乎所有的 Web 项目, 都是基于这种三层架构开发模式. 甚至连 Java Spring 框架的官方 demo, 都是按照这种开发模式来编写的.

后端的三层架构就是: Controller, Service, Repository. Controller 负责暴力接口, Service 负责业务逻辑, Repository 负责数据操作.

下面是转账服务的核心 Service (忽略所有错误和参数验证和事务)

// TransferService 转账服务
// @param fromUserID 转出用户ID
// @param toUserID 转入用户ID
// @param amount 转账金额
// @param currency 转账币种
func (t *TransferService) Transfer(fromUserID string, toUserID string, amount decimal.Decimal, currency string ) {
    // 读数据
    var fromUser User
    var toUser User

    t.Db.Where("id = ?", fromUserID).First(&fromUser)
    t.Db.Where("id = ?", toUserID).First(&toUser)

    // 币种验证
    if fromUser.Currency != currency || toUser.Currency != currency {
        return errors.New("currency not match")
    }

    // 获取汇率
    var rate decimal.Decimal
    if fromUser.Currency == toUser.Currency {
        rate = decimal.NewFromFloat(1)
    } else {
        // 通过微软的 api 获取汇率
        rate = t.MicroService.GetRate(fromUser.Currency, toUser.Currency)
    }

    // 计算需要的金额
    fromAmount = amount.Mul(rate)

    // 计算手续费
    fee = fromAmount.Mul(decimal.NewFromFloat(0.1))

    // 计算总金额
    fromTotalAmount = fromAmount.Add(fee)

    // 余额验证
    if fromUser.Balance.LessThan(fromTotalAmount) {
        return errors.New("balance not enough")
    }

    // 转账
    fromUser.Balance = fromUser.Balance.Sub(fromTotalAmount)
    toUser.Balance = toUser.Balance.Add(amount)

    // 保存数据
    t.Db.Save(&fromUser)
    t.Db.Save(&toUser)

    // 保存账单
    t.Db.Create(&Bill{
        FromUserID: fromUserID,
        ToUserID: toUserID,
        Amount: amount,
        Currency: currency,
        Rate: rate,
        Fee: fee,
        BillType, "zhuanzhang",
    })
    
    return nil
}

我们可以看到, MVC 的 Service 层一般非常臃肿, 包含各种参数校验(这里省略了很多参数校验和错误处理), 逻辑计算, 数据操作, 甚至还包含了一些第三方服务的调用.

这样的代码, 也称之为 "事务脚本", "胶水代码", "面向过程代码" 等等. 优点是简单容易理解, 缺点是代码臃肿, 代码可维护性差, 代码可拓展性差, 代码可测试性差.

问题1: 代码可维护性差

代码可维护性 = 当依赖变化的时候, 需要修改多少代码

参考上面的代码, 我们发现胶水代码的可维护性比较差主要因为以下原因

  • 数据结构不稳定: user 是一个存数据类, 通过 gorm 映射了 MySQL 中的 user 表. 这里的问题是 MySQL 属于外部依赖, 长远来看都可能改变. 比如 ID 变成 int64 类型, 比如表中的字段名改变; 比如数据量大了需要分库分表; 比如使用 Redis 或则 MongoDB 代替 MYSQL
  • 依赖库的不稳定: 比如 grom 升级导致 api 不一致; 比如我们需要用 beego 替代 grom; 比如我们需要用 goweb 框架代替 gin
  • 依赖服务的不稳定: 比如我们依赖的 MicroServer 升级导致 api 不一致; 比如我们需要用 GoogleService 替代 MicroService

问题2: 代码可拓展性差

可拓展性=增加或者修改需求的时候, 需要修改多少代码

参考上面的代码, 如果我们今天要增加一个充值的功能, 我们可以发现上面的代码基本没有可以复用的逻辑

充值功能需要将银行卡的钱充值到余额, 银行卡可能是其他银行的银行卡, 其他银行卡的用户的数据结构可能不一致

  • 数据格式不兼容: 其他银行卡的用户的数据结构可能不一致, 导致数据校验, 数据读写, 错误处理, 金额计算, 手续费计算 等等逻辑都需要重新写
  • 业务逻辑无法复用: 因为数据结构的不一致, 所以业务逻辑基本需要将原来的复制过去, 然后重新改
  • 业务逻辑和数据储存耦合严重: 当业务逻辑变得越来越复杂的时候, 新增的业务逻辑可能需要新的数据结构, 转账功能和充值功能都需要改代码

一般的胶水代码做需求都非常快, 但是可复用的逻辑很少, 一旦涉及到新增有相同但是又不同的逻辑或者修改需求, 需要修改的代码很多. 如果有地方的代码忘了改就是一个 bug. 在反复变化的需求中, 代码的可拓展性显得很差

问题3: 代码可测试性差

可测试性 = 运行每个测试用例所花费的时间 * 每个需求所需要增加的测试用例数量

除了部分工具类、框架类和中间件类的代码有比较高的测试覆盖之外,我们在日常工作中很难看到业务代码有比较好的测试覆盖,而绝大部分的上线前的测试属于人肉的“集成测试”。低测试率导致我们对代码质量很难有把控,容易错过边界条件,异常case只有线上爆发了才被动发现。而低测试覆盖率的主要原因是业务代码的可测试性比较差。

参考上面的代码, 这种代码可测试性比较差的原因是:

  • 基础设施搭建困难: 比如代码中依赖了数据库, 第三方服务 等外部依赖, 想要跑测试用例需要将所有的依赖都跑起来. 本案例作为 demo 依赖还是比较少的. 一般项目有 Reids MySQL ES 消息队列 再加 10 个三方微服务, 请问怎么写单元测试?
  • 耗时长: 如果跑测试用例的时间超过 1 分钟, 大部分程序员就不会去跑测试用例了, 这种测试即使所有的依赖都搭建起来的了, 各种 IO 网络 都非常耗时, 测试时间比较长. 面对这种困境, 程序员一般选择 "颅内测试"
  • 测试用例复杂: 假如一个脚本中有 3 个步骤, 每个步骤对应了 N 个状态, 那么测试用例的数量就是 N*N*N当胶水代码中的步骤越来越多, 测试用例将会呈现指数级增长

一般的这样的胶水代码, 当测试比较复杂的时候, 开发人员无法写这个方法的单元测试, 依赖测试人员的 "人肉测试" 或者开发人员的 "颅内测试".

每次代码变动, 之前的测试就可能变得不可靠, 有需要重新 "人肉测试" 或者 "颅内测试", 如果每天变动 2 次代码, 就陷入了无限的测试和代码 review 的风暴中

问题总结

我们重新来分析一下为什么以上的问题会出现?因为以上的代码违背了至少以下几个软件设计的原则:

  • 单一性原则(Single Responsibility Principle):单一性原则要求一个对象/类应该只有一个变更的原因。但是在这个案例里,代码可能会因为任意一个外部依赖或计算逻辑的改变而改变。
  • 依赖反转原则(Dependency Inversion Principle):上层模块不要依赖底层,应该依赖底层的抽象, 面向接口编程。在这个案例里外部依赖都是具体的实现, 比如 MicroService 虽然是一个接口类,但是它对应的是依赖了MicroSoft提供的具体服务,所以也算是依赖了实现。同样的 grom.DB 实现也属于具体实现。
  • 开闭原则(Open Closed Principle):对拓展开放, 对修改关闭。在这个案例里的金额计算属于可能会被修改的代码,这个时候该逻辑应该需要被包装成为不可修改的计算类,新功能通过计算类的拓展实现。

我们需要对代码重构才能解决这些问题。下面我们就来看下如何使用 DDD 重构我们上面的胶水代码

如何使用 DDD 重构?

怎么重构呢? 主要分为 2 方面:

  • 抽离逻辑: 贫血的类数据没有逻辑, 充血模型的类既有数据又有逻辑. DDD 的思想是将数据校验和逻辑抽离到存内存的类中, 这种类不能有任何的依赖
  • 抽象接口: 将依赖抽象一个接口, 不依赖具体的实例, 依赖接口

参考上面的代码, 我们画一张流程图描述一下主要的步骤:

架构流程图
架构流程图
  • TransferController 依赖 TransferService, TransferService 依赖 MicroService 和 GROM
  • 业务层中黑色字体代表业务逻辑, 红色字体代表需要依赖基础设施的输入输出
  • 读数据, 更新金额, 保存账单依赖于 GROM
  • 获取汇率 依赖 MicroService

第一步 抽离逻辑

业务层中黑色字体代表业务逻辑, 包含 参数检查, 金额计算和转账

MVC 三层架构是一种贫血模型, 贫血模型的类数据没有逻辑, 充血模型的类既有数据又有逻辑

我们要做的是: 将 Service 中的逻辑抽离到类中

(1) 使用 Domain Object 代替基础数据类型

// 基础数据类型
Transfer(fromUserID string, toUserID string, amount decimal.Decimal, currency string ) error
// 领域类
Transfer(fromUserID, toUserID *model.UserID, amount *model.Amount, currencyStr string) error

Domain Object 就是一种充血模型, 既有数据又有逻辑, 比如上面的 model.UserID这个对象的构造方法如下

func NewUserID(userID string) (*UserID, error) {
 // 参数检查
 return &UserID{
  value: userID,
 }, nil
}

我们可以发现参数检查在构造方法里面, 这样就能保证传入的参数一定是经过参数检查的, 如果所有的 Service 方法都用 Domain Object 替代基础数据类型, 这样就不用担心参数校验的问题了

而且 Domain Object 是纯内存的, 没有任何依赖, 十分方便测试

(2) 使用 Domain Service 封装业务逻辑

之前我们的计算金额和转账逻辑都是由 Controller 下面的 Service 层实现的 因为 Service 需要太多依赖, 比如 MicroService 和 GROM, 所以我们很难对这一块逻辑进行测试 如果我们把这这块逻辑抽离到没有任何依赖的纯内存的 Domain Service 中, 就能很方便的测试

func (*TransferService) Transfer(fromUser *User, toUser *User, amount *Amount, rate *Rate) {
 // 通过汇率转换金额
 fromAmount := rate.Exchange(amount)

 // 根据用户不同的 vip 等级, 计算手续费
 fee := fromUser.CalcFee(fromAmount)
    
    // 计算总金额
 fromTotalAmount := fromAmount.Add(fee)

 // 转账
 fromUser.Pay(fromTotalAmount)
 toUser.Receive(amount)
}

第二步 抽象接口

我再贴一下我们之前分析的三层架构流程图, 业务层中红色字体代表需要依赖基础设施

架构流程图
架构流程图

依赖反转原则(Dependency Inversion Principle):上层模块不要依赖底层,应该依赖底层的抽象, 面向接口编程。在这个案例里外部依赖都是具体的实现,比如 MicroService 虽然是一个接口类,但是它对应的是依赖了MicroSoft提供的具体服务,所以也算是依赖了实现。同样的 grom.DB 实现也属于具体实现。

我们要做的就是抽象一层接口出来, 面向接口编程

(1) 抽象汇率获取服务

type RateService interface {
 GetRate(from *model.Currency, to *model.Currency) (*model.Rate, error)
}

然后 MicroRateService 是该接口的一种实现, 这种设计叫也做防腐层

防腐层不仅仅是多了一层调用, 它还可以提供如下功能

  • 适配器:很多时候外部依赖的数据、接口和协议并不符合内部规范,通过适配器模式,可以将数据转化逻辑封装到防腐层内部,降低对业务代码的侵入。在这个案例里,我们通过封装了Rate 和 Currency 对象,转化了对方的入参和出参,让入参出参更符合我们的标准。
  • 缓存:对于频繁调用且数据变更不频繁的外部依赖,通过在防腐层里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的,通过将缓存逻辑嵌入防腐层,能够降低业务代码的复杂度。
  • 兜底:如果外部依赖的稳定性较差,一个能够有效提升我们系统稳定性的策略是通过防腐层起到兜底的作用,比如当外部依赖出问题后,返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑一般都比较复杂,如果散落在核心业务代码中会很难维护,通过集中在防腐层中,更加容易被测试和修改。
  • 易于测试:类似于之前的接口,防腐层的接口类能够很容易的实现Mock或Stub,以便于单元测试。
  • 功能开关:有些时候我们希望能在某些场景下开放或关闭某个接口的功能,或者让某个接口返回一个特定的值,我们可以在防腐层配置功能开关来实现,而不会对真实业务代码造成影响。同时,使用功能开关也能让我们容易的实现Monkey测试,而不需要真正物理性的关闭外部依赖。

(2) 抽象 gorm

type UserRepo interface {
 Get(*model.UserID) (*model.User, error)
 Save(*model.User) (*model.User, error)
}

在接口层面做统一,不关注底层实现。比如,通过 Save 保存一个 Domain 对象,但至于具体是 insert 还是 update 并不关心. 是使用 MYSQL 还是使用 MongoDB 甚至是本地文件储存都不关心.

重构之后是什么样子?

重构之后的 Service 层如下, 为了区别我们抽离出来的纯内存的 TransferService, 我们将这原来的Service层命名为 TransferApp 代表 Application 层

重构之后的 UserApp.Transfer() 代码如下 (忽略所有的错误处理和事务):

func (u *UserApp) Transfer(fromUserID, toUserID *model.UserID, amount *model.Amount, currencyStr string) error {
 // 读数据
 fromUser := u.userRepo.Get(fromUserID)
 toUser := u.userRepo.Get(toUserID)
 toCurrency := model.NewCurrency(currencyStr)

    // 获取汇率
 rate := u.rateService.GetRate(fromUser.Currency, toCurrency)

 // 转账
 u.transferService.Transfer(fromUser, toUser, amount, rate)

 // 保存数据
 u.userRepo.Save(fromUser)
 u.userRepo.Save(toUser)

 // 保存账单
 bill := &bill_model.Bill{
  FromUserID: fromUser.ID,
  ToUserID:   toUser.ID,
  Amount:     amount,
  Currency:   toCurrency,
 }
 u.billApp.CreateBill(bill)

 return nil
}

重构之后的架构流程图如下, 我们新增了粉色部分的领域层:

架构流程图
架构流程图

新增的领域层分为 2 个部分

  • 抽离逻辑 (黑色字体): 有数据又有逻辑的 Domain Object 对象 和没有任何依赖的 Domain Service 对象, 纯内存, 方便测试
  • 抽象接口 (红色字体): 抽象的基础设施层的接口, 方便 mock 测试

如何组织代码结构?

参考上面的代码, 你或许已经跃跃欲试的尝试自己通过 DDD 的思想实现一下这个案例了 但是当你拿到需求时候, 打开 IDE 第一个问题就是: 如何组织代码结构? 要解决这个问题的前提我想是: 明确架构

MVC 三层架构

比如上面代码的例子

架构流程图
架构流程图

DDD 四层架构

比如上面代码的重构后的例子

架构流程图
架构流程图

对应的四层架构模型如下

架构流程图
架构流程图

DDD 六边形架构/洋葱架构/干净架构

在上面重构的代码里,如果抛弃掉所有Repository、ACL、Producer等的具体实现细节,我们会发现每一个对外部的抽象类其实就是输入或输出,类似于计算机系统中的I/O节点。这个观点在CQRS架构中也同样适用,将所有接口分为Command(输入)和Query(输出)两种。除了I/O之外其他的内部逻辑,就是应用业务的核心逻辑。基于这个基础,Alistair Cockburn在2005年提出了Hexagonal Architecture(六边形架构),又被称之为Ports and Adapters(端口和适配器架构)。

架构流程图
架构流程图

在这张图中:

  • I/O的具体实现在模型的最外层
  • 每个I/O的适配器在灰色地带
  • 每个Hex的边是一个端口
  • Hex的中央是应用的核心领域模型

在Hex中,架构的组织关系第一次变成了一个二维的内外关系,而不是传统一维的上下关系。同时在Hex架构中我们第一次发现UI层、DB层、和各种中间件层实际上是没有本质上区别的,都只是数据的输入和输出,而不是在传统架构中的最上层和最下层。

除了2005年的Hex架构,2008年 Jeffery Palermo的Onion Architecture(洋葱架构)和2017年 Robert Martin的Clean Architecture(干净架构),都是极为类似的思想。除了命名不一样、切入点不一样之外,其他的整体架构都是基于一个二维的内外关系。这也说明了基于DDD的架构最终的形态都是类似的。Herberto Graca有一个很全面的图包含了绝大部分现实中的端口类,值得借鉴。

架构流程图
架构流程图

这里不赘述该架构图描述, 可以参考原文: https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/

如何组织代码架构?

到目前为止, 大部分的 DDD 应用使用类似这样的架构, 比如著名的案例: https://github.com/victorsteven/food-app-server

├── application
│   ├── food_app.go
│   ├── food_app_test.go
│   ├── user_app.go
│   └── user_app_test.go
├── domain
│   ├── entity
│   └── repository
├── infrastructure
│   ├── auth
│   ├── persistence
│   └── security
├── interfaces
│   ├── fileupload
│   ├── food_handler.go

这是一种 "按层分包" 的架构, 对于从 MVC 三层架构重构到 DDD 来说只是分解了 Service 层, 比较容易理解

但这是细粒度的代码隔离。粗粒度的代码隔离至少是同样重要的,它是根据子域和有界上下文来隔离代码的。我称之为 "基于业务分包".

我之前一直使用的是 "按层分包" 的结构, 现在我是 "基于业务分包" 的忠实拥护者. 在我的案例中, 我无耻的将上面的按层打包改成下面的内容

├── bill  // 账单组件
│   ├── app.go
│   ├── model
│   └── repo.go
├── common      // 通用工具
│   ├── logs
│   └── signals
├── servers     // 通用 servers
│   ├── apps.go
│   ├── repos.go
│   ├── rpc
│   ├── servers.go
│   └── web
└── user  // 用户组件
    ├── app.go
    ├── auth_repo.go
    ├── model
    ├── rate_service.go
    ├── repo.go
    ├── rpc_server.go
    ├── transfer_service.go
    ├── web_auth_middleware.go
    └── web_handler.go

因为在编码实践中,我们总是基于一个业务用例来实现代码,在 "按层分包" 场景下,我们需要在分散的各包中来回切换,增加了代码导航的成本;另外,代码提交的变更内容也是散落的,在查看代码提交历史时,无法直观的看出该次提交是关于什么业务功能的。在业务分包下,我们只需要在单个统一的包下修改代码,减少了代码导航成本;另外一个好处是,如果哪天我们需要将某个业务迁移到另外的项目(比如识别出了独立的微服务),那么直接整体移动业务包即可。

总结

  • 抽离逻辑: 将 service 逻辑抽到 domain 类中, domain 是纯内存对象, 独立于任何外部依赖
  • 抽象接口: 根据依赖反转的设计思想, 不要依赖实现, 依赖接口; 如果是三方服务, 抽象一个防腐层出来保护自己的业务

DDD 不是银弹, 它是 复杂业务 的一种设计思想. DDD 的核心在于对业务的理解, 而不是对领域模型的熟悉程度, 不要花太多时间去研究理论

如果今天的内容你只能记住一件事情, 那我希望是: 抽离逻辑, 抽象接口

案例中的代码我已经提交到了 GitHub: https://github.com/dengjiawen8955/ddd_demo

下面的代码可以快速运行案例中的项目:

# 下载项目
git clone git@github.com:dengjiawen8955/ddd_demo.git  && cd ddd_demo
# 准备环境 (启动mysql, redis)
docker-compose up -d
# 准备数据库 (创建数据库, 创建表)
make exec.sql
# 启动项目
make

思考题

也许你听说过 "要做好微服务先做好 DDD" 这样类似的话, 因为 DDD 是指导微服务拆分的重要思想

通过上面的代码, 如果你想要拆分这个案例为微服务, 你会怎样拆分呢?

reference

  • https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
  • https://zhuanlan.zhihu.com/p/84223605

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17657.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构计算二叉树的深度和节点个数

2022.11.19 计算二叉树的深度和节点个数任务描述相关知识编程要求测试说明C/C代码任务描述 本关任务:给定一棵二叉树,计算该二叉树的深度、总节点个数和叶子节点个数。 相关知识 为了完成本关任务,你需要掌握:1.二叉树深度概念…

【Java八股文总结】之类

文章目录Q:一个Java文件中可以有多个类么(不含内部类)?一、Object类1、Object类的常见方法有哪些?2、 和 equals() 的区别3、HashCode()的作用?4、为什么要有hashCode?5、为什么重写equals()时必…

古人的雅趣

学习古人雅趣,为今日生活增添情趣。 目录 曲水流觞 九月九日重阳节赏菊 中秋赏月 一、曲水流觞 中国古代汉族民间的一种传统习俗,后来发展成为文人墨客诗酒唱酬的一种雅事。 夏历的三月上巳日人们举行祓禊(fx)仪式之后&#xf…

单流 TCP 100Gbps+ 难题的直观解释

关于 400Gbps 场景的概括,参见:400Gbps 网络面临的挑战 首先定义规则: 画坐标系,横轴为到达时间,纵轴为服务(处理)时间。每到达一个任务在横轴对应时间画一条垂直线。每处理一个任务在纵轴对应时间画一条水平线。任务…

【Linux】文件操作/文件描述符/重定向原理/缓冲区

目录 一.文件的概念 1.什么是内存级文件? 2.什么是磁盘级文件? 3.文件IO的过程 4.linux下, 一切皆文件 二.文件操作(C语言接口) 1.语言级文件接口与系统级文件接口 1).什么是语言级别的文件接口? 2).为什么要有语言级别文件接口, 直接用系统接口不好吗? 3).系统级…

老系统如何重构之最全总结

目录 1. 重构的概念 1.1 重构的定义 1.2 重构的分类 2 为什么重构 3 如何重构 3.1 说服业务方 3.2 确定重构的目标 3.3 老系统的熟悉与梳理 3.4 数据库的重构 3.5 前后端的系统重构 3.6 数据迁移与检查 3.7 系统检查联调测试 3.8 系统切换 1. 重构的概念 1.1 重构…

DlhSoft Gantt Chart Hyper Library for HTML5 Standard Edition

DlhSoft Gantt Chart Hyper Library 甘特图超级库包括一组交互式计划组件,可用于使用纯 JavaScript、TypeScript 或 Angular、React 或 Vue 等框架构建的启用时间线的应用程序 基于 JavaScript 的甘特图 可定制的网格列、汇总的工作分解结构、带有可拖动条和依赖线…

静态HTML网页设计作品 DIV布局家乡介绍网页模板代码---(太原 10页带本地存储登录注册 js表单校验)

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材,DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 家乡旅游景点 | 家乡民生变化 | 介绍自己的家乡 | 我的家乡 | 家乡主题 | HTML期末大学生网页设计作业 HTML:结构 CSS:样式 在…

基于ssm jsp超市在线销售平台的设计与实现

近年来,网络信息技术的迅猛发展,互联网逐渐渗透到人们日常生活中的方 方面面,而给我们的生活带来巨大变化的电子商务正在以前所未有的速度蓬勃发 展,电子商务也成为网络研究与应用的热点之一。网上商店是电子商务的重要方 面&…

【干货】教你在十分钟内编译一个Linux内核,并在虚拟机里运行!

前言 这篇文章将会简单的介绍如何在Linux系统上面,编译一个5.19的内核,然后在QEMU虚拟机中运行。 下载Linux内核源码 首先,我们需要下载Linux的代码: https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.19.10.t…

使用vue互联QQ音乐完成网站音乐播放器

🎶 文章简介:使用vue互联QQ音乐完成网站音乐播放器 💡 创作目的:记录使用APlayer播放器MetingJs实现 在线播放qq音乐、网易云音…等平台的音乐 ☀️ 今日天气:2022-11-19 小雨多云 天空灰蒙蒙的 🥲 &#x…

还在付费使用 XShell?我选择这款超牛逼的 SSH 客户端,完全免费

分享过FinallShell这款SSH客户端,也是xiaoz目前常用的SSH客户端工具,FinalShell使用起来方便顺手,但令我不爽的是tab数量变多的时候FinalShell越来越卡,而且内存占用也比较高。 最近发现一款使用使用C语言开发的跨平台SSH客户端W…

【无人机】基于Matlab的四旋翼无人机控制仿真

✅作者简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,matlab项目合作可私信。 🍎个人主页:Matlab科研工作室 🍊个人信条:格物致知。 更多Matlab仿真内容点击👇 智能优化算法 …

uni-app —— 下拉刷新 上拉加载

文章目录 前言一、下拉刷新 1.开启下拉刷新2.监听下拉刷新3.关闭下拉刷新二、上拉加载总结一、下拉刷新 1. 开启下拉刷新 在uni-app中有两种方式开启下拉刷新 需要在 ​​pages.json ​​​ 里,找到的当前页面的pages节点,并在​​style​​​ 选项中开…

这次把怎么做好一个PPT讲清-总体篇

文章目录一、背景二、图表化、图示化三、关键词设计四、版式层级五、逻辑关系图**1)常用逻辑****2)如何让逻辑关系图好看**六、对齐、分组和对比**对齐****分组****分组就是将同类得信息放在一起,靠的更近一点**那么,实现分组原则…

基于S32K144实现TPS929120的基本控制功能

文章目录前言1.TPS92910简介2.硬件调试平台2.1 灯板原理图2.2 参考电流2.3 器件地址3.TPS929120通信协议3.1 物理层3.2 数据链路层3.3 传输层2.3.1 读写时序2.3.2 帧格式说明2.3.3 寄存器lock与unlock2.3.4 输出通道控制4.使用S32K144驱动TPS929104.1 实现命令帧格式4.1.1 写寄…

【云原生】玩转Kubernetes实战(一):Pod、ConfigMap的使用

本文主要是利用Kubernetes 集群搭建出一个 WordPress 网站,用了三个镜像:WordPress、MariaDB、Nginx。 下面是其简单的架构图,用于直观的展示这个系统的内部逻辑关系: 简单来说,就是要通过本地地址http://127.0.0.1…

Spring AOP[详解]

一.需求引入 在开发过程中,总会有一些功能与业务逻辑代码耦合度不强(例如保存日志,提交事务,权限验证,异常处理),我们可以将这些代码提取到一个工具类中,需要使用时在调用工具类来实现. ​ 但是这样也会有弊端,那就是我们的代码已经开发完毕,后期如果需要增加公共功能就需要更…

Pinpoint--基础--03--安装部署

Pinpoint–基础–03–安装部署 前提 使用hd用户登陆 完成基础环境搭建https://blog.csdn.net/zhou920786312/article/details/118212302代码位置 https://gitee.com/DanShenGuiZu/learnDemo/tree/master/pinpoint-learn/demo11、安装环境准备 1.1、jdk1.8 基础环境搭建 包含…

一文搞懂MySQL表字段类型长度的含义

不知道大家第一眼看标题的时候有没有理解,什么是“字段类型长度”,这里我来解释下,就比如我们在MySQL建表的时候,比如下面这个建表语句: CREATE TABLE user (id int(10) DEFAULT NULL,name varchar(50) DEFAULT NULL,…