架构知识整理与思考(其四)
书接上回
建议,没有看过上一章的可以看一下,上一章“架构知识整理与思考(其二)”
感觉这都成链表了。
三生万物 软件架构
终于,我们进入了具体的软件架构讨论中。
软件架构是什么?相关定义如下:软件架构是指软件系统的高层设计,包括其结构、组件及其交互方式。它为软件系统提供了一个蓝图,描述了系统的主要组成部分、它们之间的关系以及设计和演化的关键决策。软件架构决定了系统的可维护性、扩展性、性能和可靠性。他包含整个系统的组件,关系,连接件,约束和原则。
很官方是说法,但是我们可以从结果导向来描述:
软件架构的终极目的就是最大化程序员的生产力,同时最小化系统的总运营成本。
阶段
开发阶段:人员多的情况下,可能会按照业务模块划分为多个事业部。
部署阶段:注意边界,减少服务
运行阶段:不要妄图使用多增加服务器资源的形式,加快响应。
当然这个很重要,但作为架构师应该更加注意如何设计优雅,高效的系统(事实上很多软件的cpu,内存资源都没有得到充分应用)
维护阶段:
维护的成本 取决于两个点:探秘和风险
探秘是对原本系统的挖掘,主要来自我们对于现有软件系统的挖掘,目的是确定新增功能或被修复问题的最佳位置和最佳方式。而“风险(risk)”,则是指当我们进行上述修改时,总是有可能衍生出新的问题,这种可能性就是风险成本。
保持可选项
之前也说了,整个软甲架构设计不可能至上而下的进行设计,因为在项目前期,你永远都无法收集足够多的信息,你不知道业务需求的变化点,业务的扩展点。在这种基调下,我们产生了“保持可选项”这种做法。
“保持可选项”:说白了就是和核心业务无关的一些设计,可以往后推迟。比如:我们都需要数据库,但是用mysql,oracle 甚至 redis哪些中间件,这个可以推迟。
《架构整洁之道》作者在提到这样一段有趣的话:
“那么如果其他人已经替我们做出了决策呢?譬如说,我们的公司已经指定了某个数据库,或某种Web服务,或某个框架,这时应该怎么办?通常一个优秀的软件架构师会假装这些决策还没有确定,并尽可能长时间地让系统有推迟或修改这些决策的能力。”
重复问题再讨论
所有的程序员都讨厌重复的代码,毋庸置疑且理应如此。
但是今天我们再从另外一个角度来看待这些问题。
比如一个PO与VO,他们都是描述业务对象,必然有很多相似的字段,那么是不是他们应该抽象为一个基本序列化对象呢,其实不然。
PO与VO有着不同的应用场景,更有着不同的更新速率,PO面向业务操作,VO承载前端页面信息。所以尽管有着重复字段,他们不应该合并。
说白了:职责不同,关注点不同,生命周期不同,更新迭代速度不同,他们不应该同等看待。
但是相对来说,我们可以使用其他方法来进行避免大量的CV代码,比如BeanCopy(注意字段属性和名称呀)
边界
在整个架构体系中,我觉得边界是最为重要的元素之一,划分边界,制定职责是每个架构师的基本功。我觉得我们不应该想着制造一个巨无霸的系统,解决系统的100%的问题,而是应该制定好边界,分层,分域的解决业务问题,毕竟你要考虑交付,周期,市场。
我觉得一个企业面对的问题是复杂的,抽象化线下业务场景到线上系统,他必然是一系列系统的集合。比如一个企业可能有:游供应链系统,内部ERP系统,CRM会员系统,还有业务中台充当BUS总线,数据中台进行挖掘客户,微信商城小程序进行销售等等。他们共同构成了整个企业的业务形态。
当然以上说的是系统与系统之间的边界,一不小心又扯远了。
我们把目光先foucs到单个系统内。
这条红线就是一个系统内模块的边界,很明显红线上方是业务逻辑,红线下方是数据库访问逻辑。同样很明显,使用了DIP依赖倒置的方式实现了模块之间的解耦。
这个架构边界线体现了“面向接口编程”的思想和“分层设计”的原则,强调高层业务逻辑与底层实现的解耦。箭头表明了依赖关系和数据流动方向。
整个架构流线图体现了以下两点:
业务规则只依赖抽象接口。
数据库访问逻辑通过实现接口为业务规则提供支持。
不完全边界
事物的发展必然有两方面,一方面我们强调理论基础,需要厘清模块边界;一方面又必须要注意需求的变化性,业务的不明确性,资源的限制问题等,又没有必要制定过于明确的边界。(道理等同于保持可选项那一章节)。
不完全边界是整个技术架构中的常态,反映了实际业务需求与技术实现之间的权衡。(架构师就是在不断的权衡中做出选择)
核心目标不是追求绝对的边界清晰,而是根据实际情况做出合理选择,保持架构的弹性和可维护性:
不完全边界是一种妥协,它牺牲了一部分边界的独立性,换取性能、效率或开发成本的优化。
合理性和控制力是关键:边界模糊或不完全的部分需要可控,且不应导致整体系统失去灵活性或可扩展性。
不完全边界的特点: 部分职责重叠、依赖存在、边界模糊、效率权衡(处于资源和性能的考虑,打破某些边界的独立性,选择更为紧密的集成方式)
理论基础:YAGNI原则
YAGNI 强调只实现当前明确需要的功能,而不为可能用到的功能预留过多复杂的架构。
如何体现 YAGNI 原则:
• 减少过度拆分:在初期,系统可以通过模块化组织,但避免过早地为每个模块独立编译和部署。
• 灵活应对变化:通过合理的接口设计,延迟决定完全解耦的边界,等需求成熟后再完善。
• 避免浪费资源:以最小的开发和维护成本满足当前业务需求,降低不必要的投资。
如何设计良性的不良边界
独立编译与部署后合并组件
这种方式是实现不完全边界的一个实际操作:将系统逻辑分解成多个模块(或微服务)进行开发,但最终以单一部署单元的形式发布。
这样做的好处是业务逻辑是清晰的,物理边界被弱化了,这样编译部署是快速的,且后期要转微服务也比较快。
采用策略模式实现单向边界
策略模式是一种经典的设计模式,可以帮助实现不完全边界的“方向性隔离“。
在边界不完全的情况下,策略模式通过定义行为接口,将逻辑的变化点隔离到实现类中。
核心模块只依赖接口,不关心具体实现,从而保持边界的稳定性。
门户模式
门户模式是一种为模块化架构引入边界的方式,但它强调模块的集中访问和管理。
核心思想:
提供一个统一的门户组件来协调不同模块或子系统之间的交互,减少模块间的直接依赖。门户组件通常通过接口或消息机制与模块交互。(有点像SOA下的BUS总线或者kafka消息流机制触达)
总结一下架构师在边界制定中的职责
架构师的一个重要任务是根据业务和技术需求,动态决定边界的设置和完全性程度。这包括以下几个方面:
1.预判未来可能的变化
识别扩展点:通过分析业务需求,判断哪些模块可能需要更灵活的边界。
2.延迟决策
控制架构复杂性:在项目初期尽量避免过度设计,推迟对边界完全性的决策。
通过接口保护边界:即使是目前的边界不完全,也要通过接口或适配器模式确保模块的逻辑不直接暴露。
3.平衡短期与长期利益
短期目标:快速交付、降低初始成本。
长期目标:保持架构的弹性,支持未来需求的变化。
架构师的职责:
- 识别哪些地方需要边界。
- 判断边界是否需要完全隔离。
- 在架构演进中,动态调整边界的实现方式。
通过这种方式,架构能够在复杂性和灵活性之间找到平衡,实现更合理的模块化设计。
浅谈隔离(部署单元)
边界的核心价值是什么?是资源的隔离,是为了更好地控制和优化资源的使用,同时实现系统的高可用性、可维护性和可扩展性
隔离的具体体现是什么?是线程,进程,或者说是一个个服务。
线程(内存与任务的隔离)
特性:
• 线程共享进程的内存空间。
• 隔离的粒度较小,更关注任务级别的独立性。
隔离方式:
• 线程之间通过栈内存隔离(每个线程有自己的栈空间)。
• 使用锁、信号量等机制防止共享资源的竞争问题。
适用场景:
• 并行计算:如 Web 服务器的多线程模型。
• I/O 密集型任务:通过多线程提高资源利用率。
进程级隔离:内存和资源隔离
特性:
• 进程有独立的内存空间,相互之间无法直接访问内存。
• 每个进程的资源(如文件描述符、网络端口)是独立的。
隔离方式:
• 操作系统内核提供进程间的隔离。
• 进程间通过 IPC(管道、消息队列、共享内存等)或网络通信。
适用场景:
• 需要更高可靠性和容错性,如分布式系统中的服务进程。
• 运行资源密集型任务的独立模块,如数据库进程。
服务级隔离:部署和扩展隔离
特性:
• 服务是一个独立的进程,但更强调“业务功能”的独立性。
• 服务可以部署在不同的机器上,甚至可以通过容器技术在相同机器上运行多个服务实例。
隔离方式:
• 网络协议(如 HTTP/gRPC)是服务间通信的主要形式,边界更为明确。
• 数据库、配置、缓存等资源通常是服务私有的,避免跨服务共享。
适用场景:
• 微服务架构:服务根据业务功能独立,易于扩展和部署。
• 多团队协作:每个团队负责自己的服务,解耦业务逻辑。
总结
边界划分的最终目的,是为了更好地控制和优化资源的使用,同时实现系统的高可用性、可维护性和可扩展性:
资源独立:
• 每个边界内的单元能够独立占用和管理资源(如内存、CPU、网络端口等),减少干扰。
• 进程和服务的边界隔离可以有效限制单点故障的影响。
容错性增强:
• 线程边界较弱,线程崩溃可能影响整个进程。
• 进程和服务边界较强,一个进程或服务崩溃不会直接影响其他进程或服务。
并行与扩展:
• 边界划分可以帮助系统更好地实现并行化(线程级并行、进程级并行)。
• 服务级边界允许通过水平扩展(增加服务实例)来提升系统吞吐量。
职责清晰:
• 在服务级隔离中,边界通过功能拆分变得更加明确(如按业务功能划分服务)。
• 边界明确后,团队或模块间的职责也更清晰,减少依赖冲突。
层次与策略
《架构整洁之道》中有这样一句话,我觉得概括了整个软件系统的:“本质上,所有的软件系统都是一组策略语句的集合。是的,可以说计算机程序不过就是一组仔细描述如何将输入转化为输出的策略语句的集合。”
层次
层次是架构中的逻辑分区,用来将系统的功能职责划分为不同的模块,每一层都有明确的职责和边界,并依赖于其他层次提供的功能。
特点:
抽象与隔离:
• 上层依赖下层提供的接口,不直接关注下层的具体实现。
职责分离:
• 每一层只关注特定的逻辑职责。】
顺序性:
• 通常遵循自顶向下调用(例如,UI 层调用业务层,业务层调用数据层)。
典型的分层架构:
• 表示层(Presentation Layer):负责与用户交互。
• 业务逻辑层(Business Logic Layer):封装核心业务逻辑。
• 数据访问层(Data Access Layer):管理数据库操作。
策略
策略是一种实现目标的方法或技术,通常是针对某个问题领域的解决方案,可以在不同的层次中实现。
特点:
灵活性:
• 不依赖具体的上下文,可以独立应用于不同场景。
针对性:
• 专注于解决某一类问题,例如缓存策略、负载均衡策略、事务管理策略。
实现多样性:
• 同一策略可以有多种实现(例如,缓存策略可以用 Redis,也可以用内存)。
典型的策略:
• 算法策略:选择不同的排序算法、加密算法等。
• 架构策略:微服务拆分策略、数据分区策略。
• 业务策略:订单分发策略、推荐系统中的推荐算法策略。
关系
层次与策略的关系可以总结为:层次提供组织框架,策略提供实现手段。
• 层次:解决的是系统的结构化问题,为架构提供一个清晰的逻辑框架。
• 策略:解决的是具体的实现问题,为层次的内部提供多样化的解决方案。
• 关系:层次提供稳定的骨架,策略注入灵活的血肉,使系统既保持统一性又具备适应性。
简单来说,层次是“骨架”,策略是“肌肉”,两者共同组成了健壮的系统架构。
整洁架构
一个良好的架构,应该具有以下特性:
独立于架构,可被测试、独立于UI、独立于数据库、独立于外部系统。
独立于架构(以下描述在我DeepSeek上的深度联想,真香啊,我写在此篇时恰好是1.28日除夕晚)
概念解构:何为"独立于架构"?
1.空间维度解耦:
// 错误示范:业务逻辑与框架耦合
class OrderController {
async createOrder(@Res() response: ExpressResponse) {
const data = await this.service.create(req.body);
response.status(201).json(data); // 深度绑定HTTP框架
}
}
// 整洁架构实现
interface OrderPresenter {
present(order: Order): void;
}
class HttpOrderPresenter implements OrderPresenter {
constructor(private response: Response) {}
present(order: Order) {
this.response.status(201).json(adaptToDTO(order));
}
}
关键点:业务代码不感知任何传输协议(HTTP/GRPC/CLI)
2.时间维度隔离:
// 传统分层架构的渗透性腐蚀
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // JPA注解污染领域模型
}
// 整洁架构的防护策略
class UserEntity {
private UserId id; // 领域自有的ID概念
}
class UserJpaEntity {
@Id
private Long persistenceId; // ORM模型
}
本质:领域模型的生命周期应超越任何持久化框架的存续周期
实践悖论:架构既是手段也是约束
1.架构选择的量子态现象:
业务阶段 | 适用架构模式 | 解耦策略 |
---|---|---|
探索期 | 单体+模块化 | 接口隔离 |
成长期 | 微服务+事件驱动 | 上下文映射 |
成熟期 | 领域驱动设计 | 限界上下文 |
转型期 | 函数式架构 | 不可变数据结构 |
2.典型反模式警示:
# 框架主导的架构(Django式渗透)
class Order(models.Model): # ORM模型直接作为领域对象
user = models.ForeignKey(User)
total = models.DecimalField()
def save(self, *args, **kwargs):
# 业务逻辑混入持久化操作
if self.total < 0:
raise ValueError
super().save(*args, kwargs)
# 整洁架构改造方案
class Order:
def __init__(self, user_id, items):
self.user_id = user_id
self.items = items
@property
def total(self):
return sum(item.price * item.quantity for item in self.items)
class OrderRepository:
def save(self, order: Order):
orm_order = OrderORM.convert(order)
orm_order.save()
救赎之道:业务规则的执行不依赖任何持久化机制
动态平衡法则
-
架构适应度函数:
function architectureFitness(context) { const { changeFrequency, teamSize, bizComplexity } = context; // 解耦度计算公式 const decouplingFactor = Math.log2(changeFrequency * bizComplexity); // 架构复杂度阈值 const maxLayers = teamSize / 3; // 康威定律量化 return decouplingFactor <= maxLayers ? '适度架构' : '过度设计'; }
-
决策平衡矩阵:
变更维度 | 过早抽象成本 | 延迟决策成本 | 平衡点判断标准 |
---|---|---|---|
支付渠道扩展 | 高 | 低 | 实现基础接口即可 |
多租户改造 | 低 | 高 | 需要预留身份隔离抽象层 |
国际化支持 | 中 | 中 | 使用策略模式动态加载资源 |
案例推演:电商系统演进史
每个架构演进节点都在证明:真正的架构是业务诉求在特定时空条件下的最优映射。当我们在代码中写下interface OrderService
时,本质上是在构建抵御技术变迁的第一道防线,但同时也需警惕抽象层变成新的"架构牢笼"。
架构师的元认知
1.破界思维训练法:
def challenge_assumptions(arch):
for layer in arch.layers:
if layer.is_core:
print(f"尝试将{layer.name}替换为不同技术栈")
print(f"改造成本评估:{layer.calculate_rewrite_cost()}")
class Architecture:
def __init__(self):
self.layers = [
Layer("Web", False),
Layer("Domain", True), # 真正的核心
Layer("Infra", False)
]
2.架构健康度检查表:
[ ] 业务规则是否能在不启动Web容器的情况下测试
[ ] 能否在1天内替换数据库类型
[ ] 领域模型是否包含任何框架注解
[ ] 新业务需求是否主要修改领域层
[ ] 基础设施层是否可插拔
最终极的"独立于架构",是培养出在代码中看见业务本质,在需求中洞见架构趋势的双重视野。就像优秀的建筑大师既能驾驭钢筋混凝土,又能让空间服务于人的真实需求一样,真正的架构师要让技术架构在业务土壤中自然生长,而非强行扭曲业务来适应架构范式。
总结
架构设计的理想目标
• 技术无关性:
• 不依赖于特定技术栈或框架,随时可以替换实现。
• 业务驱动:
• 以业务逻辑为核心,技术细节服务于业务需求。
• 易于扩展:
• 增加新功能时不需要修改现有代码,只需添加新的模块。
• 敏捷性:
• 支持快速迭代开发和变更响应。
• 可维护性:
• 代码清晰、可测试,易于排查问题和维护。
• 高性能:
• 架构在满足上述目标的同时,应考虑系统的性能优化。
总结:架构设计的本质
一个好的架构就是在不断变化中保持稳定的能力:
• 它能在需求变更时快速适配。
• 它能隔离变化,将影响最小化。
• 它能在复杂的技术栈下,始终以清晰的边界和职责为中心。
通过明确的边界和独立性设计,你的系统能够灵活面对变化,同时在演进中始终保持核心逻辑的稳定性和一致性。
横跨型变更(Cross-Cutting Concern)
定义:
指需要同时修改多个系统层级(展示层/应用层/领域层/基础设施层)或跨越多个限界上下文的变更。这类变更往往涉及系统核心业务规则的调整,或全局性技术栈升级。
典型场景:
- 多租户改造:需要在UI、权限校验、数据存储等各层增加租户隔离逻辑
- 国际化支持:涉及前端语言包、后端多时区处理、数据库字符集调整
- 安全审计增强:从接口鉴权到数据落盘的全链路审计跟踪
- 核心领域模型重构:如电商系统将"订单"拆分为"交易单"和"物流单"
破坏力评估公式:
变更成本 = 影响模块数 × 技术债务系数 × 团队认知差异度
横跨型变更的四大挑战
架构腐蚀加速
每次横跨变更都会在架构中留下"补丁",逐渐形成耦合网络
测试复杂度激增
需要同时验证:
- 领域逻辑正确性
- 上下游服务兼容性
- 数据一致性
- 性能基线
团队协作瓶颈
技术债利滚利效应
未妥善处理的横跨变更会指数级增加后续修改成本
六维处理策略
1.防腐层设计(Anti-Corruption Layer)
// 旧订单服务适配器
public class LegacyOrderAdapter {
public ModernOrder convert(LegacyOrder legacyOrder) {
return ModernOrder.builder()
.id(legacyOrder.getOrderNo())
.amount(legacyOrder.getTotal().subtract(legacyOrder.getTax()))
.build();
}
}
// 新领域模型
public class ModernOrder {
private OrderId id;
private Money amount;
}
作用:隔离新旧系统,控制变更影响范围
2.事件溯源(Event Sourcing)
class OrderService:
def __init__(self, event_store):
self.event_store = event_store
def create_order(self, command):
event = OrderCreatedEvent(
command.order_id,
command.items,
datetime.now()
)
self.event_store.publish('order_stream', event)
# 查询端通过投影构建视图
class OrderProjection:
def __init__(self):
self.orders = {}
def apply_event(self, event):
if isinstance(event, OrderCreatedEvent):
self.orders[event.order_id] = {
'items': event.items,
'status': 'created'
}
优势:核心业务变更只需追加新事件类型,不影响已有逻辑
3.上下文映射(Context Mapping)
## 支付上下文与订单上下文的协作
- 关系类型:客户-供应商
- 通信协议:RESTful API + 回调通知
- 防腐策略:
* 订单侧定义PaymentGateway抽象接口
* 支付侧提供OpenAPI规范文档
- 同步机制:每季度进行契约测试对齐
4.特性开关(Feature Toggle)
// 功能标记配置
const featureFlags = {
NEW_TAX_CALCULATION: true,
LEGACY_CHECKOUT: false
};
// 业务逻辑分支
function calculateTax(order) {
if (featureFlags.NEW_TAX_CALCULATION) {
return applyRegionalTaxRules(order);
} else {
return legacyTaxCalculation(order);
}
}
价值:实现灰度发布,降低全链路变更风险
5.CQRS模式
// 命令端
public class OrderCommandService {
public async Task Handle(CreateOrderCommand cmd) {
var order = new Order(cmd.Items);
await _repository.Save(order);
await _eventBus.Publish(new OrderCreatedEvent(order.Id));
}
}
// 查询端
public class OrderQueryService {
public OrderDto GetOrder(Guid id) {
return _readModelCache.Get(id);
}
}
收益:分离读写模型,独立演进查询逻辑
6.契约测试(Pact Testing)
# consumer订单服务的契约定义
interactions:
- description: 创建订单后支付
request:
method: POST
path: /payments
body:
orderId: "123"
amount: 100.00
response:
status: 201
headers:
Content-Type: application/json
作用:保障跨服务变更的兼容性
谦卑对象
谦卑对象模式最初的设计目的是帮助单元测试的编写者区分容易测试的行为与难以测试的行为,并将它们隔离。其设计思路非常简单,就是将这两类行为拆分成两组模块或类。其中一组模块被称为谦卑(Humble)组,包含了系统中所有难以测试的行为,而这些行为已经被简化到不能再简化了。另一组模块则包含了所有不属于谦卑对象的行为
实现细节
刚刚其实也说了,架构的一些细节:
这里额外在提一下:
数据库只是实现细节,web是实现细节,应用程序框架也是实现细节。
写在最后的一些话
我不太能保证后面还有没有一个总结和实战了,对了我有个思维导图,GitMind的,我先开一下,地址是:
链接: https://gitmind.cn/app/docs/mkrarxey
密码: 6787
然后,看这本书始于一会回家探亲,然后巨空,找不到好看的小说书。于是开始了看起了这个,后面越看越嗨。
再后来,我担任了我们公司的华东区域技术经理兼架构师(口头上的,ppt任命的),然后主导一个复杂项目的落地。也是因为第一次担任技术负责人,总想着尽善尽美,结果最小MVP未评估好,两次严重Delay。
整个过程中,令我感触最大的不是技术栈,选型啥的,而是项目管理。资源紧缺,反加班情绪高涨,客户无故增加需求,虚空撤我职,种种狗屁倒灶,却又真实存在的事情。
于是更加想找点方法论,比如:
1.项目什么阶段,应该做什么样的设计?(通过组件张力图,表示REP,CCP,CRP在不同的阶段有不同的表示)
2.技术债务问题,如何处理?(重构+前期的预设+Sonnar代码扫描)
3.短期利益与长期目标如何平衡?(根据项目资源,交期,进行平衡。迭代演进过程)
4.需求与业务的变化性与框架模块边界的定义应该如何?(迭代演进)
5.如何在有限资源下,保证主流程贯通(测试用例驱动,V模型)
6.架构的一些权衡点的考虑(YAGNI原则,有些指标天生互斥,一如安全性和性能,在架构上属于权衡点,只能选其一)
后面的这些是AI的补充:
7.团队认知差异导致重复造轮子(每日站会同步进展+模块owner机制)
8.文档资产随时间失效(代码即文档+架构决策记录ADR)
9.自动化基建滞后(首个迭代即搭建CI/CD流水线)
10.技术选型短视(建立技术雷达评估矩阵)
11.第三方依赖失控(建立供应商AB方案+版本门禁)
12.上线风险评估缺失(预埋暗开关+灰度发布策略)
13.过度设计反噬(建立架构适应度函数验证,这个可以有,我最近在看的《分布式系统架构-架构策略与难题求解》,恰巧看到这一个章节)
14.性能问题滞后暴露(在MVP阶段建立性能基线与监控)
15.跨团队协作低效(统一契约测试+接口模拟服务)
16.技术债可视化不足(SonarQube技术债看板量化)
17.生产环境止血能力弱(构建混沌工程演练机制)
最后的最后,如果我后面有时间,我会根据我实际的经验+《分布式系统架构-架构策略与难题求解》这本书内的一些实操,写一篇“架构知识整理(其五)”作为补充(PS:我之前还想着给一张当时在架构考试的时候,那个的架构设计风格的和耦合关系图,但是后面想想,实在有点太偏理论)。
而现在,我想去在我的电脑上玩玩k8s,我刚刚下载了Ubuntu的轻量虚拟机工具。当然还有空的话,我也要更新一下自己的简历去了。