软件系统与硬件和建筑系统最大的差异在于软件是可扩展的。一颗 CPU 生产出来后不会返回工厂加工以增加新功能;金字塔矗立千年,但其结构与建成时并无两样。相比之下,软件系统完全相反——如果一个软件系统开发出来后再也没有任何更新,反而说明这套系统没有发展、没有生命力。真正有生命力的软件系统,都是在不断迭代和发展的。
今天我们进入架构可扩展模式的学习。这部分内容包括分层架构、SOA 架构、微服务和微内核等。
软件系统的这种天生可扩展特性,既是魅力所在,又是难点所在。魅力体现在我们可以通过修改和扩展,让软件系统具备更多的功能和特性。难点体现在如何以最小的代价去扩展系统,因为很多情况下牵一发动全身,扩展时可能出现到处都要改,到处都要推倒重来的情况。
幸运的是,可扩展性架构的设计方法虽然繁多,但万变不离其宗,所有可扩展性架构设计背后的基本思想都可以总结为一个字:拆!
拆,就是将原本大一统的系统拆分成多个规模小的部分,扩展时只修改其中一部分即可,无须整个系统到处都改,通过这种方式来减少改动范围,降低改动风险。
说起来好像挺简单,毕竟"拆"我们见得太多了。但面对软件系统,拆就没那么简单了,因为我们并不是要摧毁一个软件系统,而是要通过拆让软件系统变得更加优美。形象地说,软件系统中的"拆"是建设性的,因此难度要高得多。
按照不同的思路来拆分软件系统,就会得到不同的架构。常见的拆分思路有如下三种:
面向流程拆分:将整个业务流程拆分为几个阶段,每个阶段作为一部分。
面向服务拆分:将系统提供的服务拆分,每个服务作为一部分。
面向功能拆分:将系统提供的功能拆分,每个功能作为一部分。
理解这三种思路的关键就在于如何理解"流程""服务""功能"三者的联系和区别。从范围上来看,从大到小依次为:流程 > 服务 > 功能。
以 TCP/IP 协议栈为例:
流程 对应 TCP/IP 四层模型,因为 TCP/IP 网络通信流程是:应用层 → 传输层 → 网络层 → 物理+数据链路层,不管最上层的应用层是什么,这个流程都不会变。
服务 对应应用层的 HTTP、FTP、SMTP 等服务,HTTP 提供 Web 服务,FTP 提供文件服务,SMTP 提供邮件服务。
功能 每个服务都会提供相应的功能。例如,HTTP 服务提供 GET、POST 功能,FTP 提供上传下载功能,SMTP 提供邮件发送和收取功能。
以一个简单的学生信息管理系统为例:
面向流程拆分:展示层 → 业务层 → 数据层 → 存储层
面向服务拆分:将系统拆分为注册、登录、信息管理、安全设置等服务。
面向功能拆分:每个服务都可以拆分为更多细粒度的功能。例如注册服务可拆分为手机号注册、身份证注册、学生邮箱注册三个功能。
不同的拆分方式,本质上决定了系统的扩展方式。当我们谈可扩展性时,很多同学都会有一个疑惑:就算是不拆分系统,只要在设计和写代码时做好了,同样不会出现到处改的问题啊?
在一个理想的环境,团队都是高手,每个程序员都很厉害,那确实不拆分也没有问题。但现实却是:团队有菜鸟程序员,到底是改 A 处还是改 B 处,完全取决于他觉得哪里容易改;有的程序员比较粗心;有的程序员某天精神状态不太好……所有这些问题都可能出现,这时候你就会发现,合理的拆分,能够强制保证即使程序员出错,出错的范围也不会太广,影响也不会太大。
面向流程拆分的优势:扩展时大部分情况只需要修改某一层,少部分情况可能修改关联的两层,不会出现所有层都同时要修改。
面向服务拆分的优势:对某个服务扩展或者要增加新服务时,只需要扩展相关服务即可,无须修改所有的服务。
面向功能拆分的优势:对某个功能扩展或者要增加新功能时,只需要扩展相关功能即可。
不同的拆分方式,将得到不同的系统架构:
这几个系统架构并不是非此即彼的,而是可以在系统架构设计中进行组合使用。例如,整体系统采用微服务架构,拆分为注册、登录、信息管理、安全服务等独立子系统;其中的注册服务子系统本身采用分层架构;登录服务子系统采用微内核架构。
以下示例演示面向服务拆分的简单实现,展示如何通过拆分降低改动范围。
python"""
面向服务拆分示例:学生信息管理系统
核心思想:将系统拆分为独立服务,扩展时只需修改相关服务
"""
# 定义各个服务的接口
class RegisterService:
"""注册服务"""
def register(self, username, password):
print(f"用户 {username} 注册成功")
return {"status": "success", "service": "register"}
def add_registration_method(self, method_name):
"""扩展注册方式(如增加学号注册)"""
print(f"新增注册方式:{method_name}")
# 只需修改本服务,无须修改登录服务、信息管理服务等
setattr(self, f"register_by_{method_name}",
lambda u, p: print(f"使用{method_name}注册用户 {u}"))
class LoginService:
"""登录服务"""
def login(self, username, password):
print(f"用户 {username} 登录成功")
return {"status": "success", "service": "login"}
class InfoService:
"""信息管理服务"""
def query_info(self, username):
print(f"查询用户 {username} 的信息")
return {"username": username, "service": "info"}
# 服务组合类
class StudentManagementSystem:
def __init__(self):
self.register_service = RegisterService()
self.login_service = LoginService()
self.info_service = InfoService()
def register(self, username, password):
return self.register_service.register(username, password)
def login(self, username, password):
return self.login_service.login(username, password)
# 测试:演示扩展时的影响范围
if __name__ == "__main__":
system = StudentManagementSystem()
print("=== 正常功能 ===")
system.register("zhangsan", "password123")
system.login("zhangsan", "password123")
print("\n=== 扩展注册服务:增加学号注册方式 ===")
# 扩展注册服务时,只需要修改 RegisterService
# LoginService 和 InfoService 完全不受影响
system.register_service.add_registration_method("student_id")
print("\n=== 调用新增的注册方式 ===")
if hasattr(system.register_service, 'register_by_student_id'):
system.register_service.register_by_student_id("2024001", "pass456")
输出示例:
=== 正常功能 === 用户 zhangsan 注册成功 用户 zhangsan 登录成功 === 扩展注册服务:增加学号注册方式 === 新增注册方式:student_id === 调用新增的注册方式 === 使用student_id注册用户 2024001
这个示例展示了面向服务拆分的核心优势:当需要增加新功能时,只需要修改对应的服务类,其他服务完全不受影响,从而限制了改动的范围。