设计原则与微服务:SRP、ISP、DIP 的落地实践
聊到微服务,很多人第一反应是「把大系统拆小」。但怎么拆?拆到什么粒度?接口怎么设计?服务之间怎么依赖?
这些问题,其实 C++ 面向对象设计原则早就给出了答案。说白了,SRP、ISP、DIP 这三个原则,放到微服务架构里,就是拆服务、定接口、管依赖的黄金法则。
我这些年做架构设计,踩过不少坑。今天就把这些经验掰开揉碎,跟你聊聊。
微服务拆分与 SRP:一个服务只做一件事
SRP(单一职责原则)在类设计里是说「一个类只有一个引起变化的原因」。放到微服务里,就是「一个服务只负责一个业务能力」。
我在项目中遇到过一个典型的反例:有个「用户服务」,里面既管用户注册登录,又管订单查询,还管商品推荐。结果每次改订单逻辑,用户服务就得重新部署,推荐算法一更新,用户登录也跟着受影响。这就是典型的职责混杂。
怎么判断是否该拆?我习惯用这个 checklist:
- 这个服务是否服务于多个不同的业务方?
- 修改一个功能时,是否会影响其他功能的稳定性?
- 这个服务的数据存储是否被多个不相关的模块共享?
如果以上任意一条回答「是」,就该考虑拆分了。
服务接口与 ISP:别让调用方被迫依赖不需要的东西
ISP(接口隔离原则)说的是「客户端不应该依赖它不需要的接口」。在微服务里,就是服务暴露的 API 要精准,不要搞大而全的「万能接口」。
你想想看,如果一个服务暴露了 20 个接口,调用方只需要其中 3 个,那剩下的 17 个接口就成了「噪音」。更糟糕的是,这 17 个接口的变更,可能会触发不必要的版本升级和回归测试。
我见过一个团队,把「用户服务」的接口设计成一个大接口:
// 反面案例:大而全的接口
class UserService {
virtual UserInfo getUserInfo(int userId) = 0;
virtual void updateUserProfile(int userId, Profile profile) = 0;
virtual OrderList getOrders(int userId) = 0; // 订单相关
virtual void addCoupon(int userId, Coupon coupon) = 0; // 优惠券相关
virtual PaymentMethod getPaymentMethod(int userId) = 0; // 支付相关
};
这个接口的问题在哪?调用方如果只需要查用户信息,却要依赖整个 UserService 的接口定义。一旦订单模块的接口签名变了,所有调用方都得重新编译部署。
正确的做法是按业务领域拆分接口:
// 正确做法:按调用方需求拆分
class IUserQuery {
virtual UserInfo getUserInfo(int userId) = 0;
};
class IUserProfile {
virtual void updateUserProfile(int userId, Profile profile) = 0;
};
class IUserOrder {
virtual OrderList getOrders(int userId) = 0;
};
我个人习惯是:每个接口的职责范围,以「一个调用方的完整业务场景」为边界。比如订单服务需要查用户信息,那就只暴露 IUserQuery,别把 IUserProfile 也塞进去。
服务依赖与 DIP:依赖抽象,不要依赖具体实现
DIP(依赖倒置原则)在类设计里是「高层模块不应该依赖低层模块,两者都应该依赖抽象」。在微服务里,就是服务之间的调用应该依赖接口契约,而不是直接依赖具体的服务实例。
为什么重要?因为微服务是分布式系统,服务实例随时可能变化——扩容、缩容、升级、故障。如果代码里硬编码了某个服务实例的地址,那这个系统就脆弱得像纸糊的一样。
我常用的做法是引入「服务注册与发现」机制,配合接口抽象:
// 依赖抽象接口
class IOrderService {
virtual Order createOrder(const OrderRequest& req) = 0;
};
// 客户端只依赖 IOrderService,不关心具体实现
class OrderClient {
std::shared_ptr<IOrderService> orderService_;
public:
OrderClient(std::shared_ptr<IOrderService> svc)
: orderService_(svc) {}
Order placeOrder(const OrderRequest& req) {
return orderService_->createOrder(req);
}
};
这样,底层是 gRPC 调用还是 HTTP 调用,是单机部署还是集群部署,对调用方来说都是透明的。
我见过一个团队,两个服务之间直接通过共享数据库来通信——A 服务往表里写数据,B 服务定时扫表。这就是典型的「依赖具体实现」,耦合度极高。后来改成通过消息队列 + 接口契约,两个服务才真正解耦。
三者关系:一张图说清楚
这三个原则不是孤立的,它们共同构成了微服务设计的骨架。我画了张图,帮你理清它们的关系:
从这张图可以看出,三个原则是层层递进的关系:
- SRP 决定了「拆成几个服务」——这是架构的骨架
- ISP 决定了「每个服务暴露什么接口」——这是服务的门面
- DIP 决定了「服务之间怎么调用」——这是系统的血脉
三者缺一不可。只做 SRP 不做 ISP,服务拆了但接口还是大而全,等于白拆。只做 ISP 不做 DIP,接口设计得再精细,底层依赖一变化还是得改代码。
嗯,今天就聊到这。这三个原则看起来简单,但真正落地的时候,每个细节都值得反复推敲。下次你拆服务的时候,不妨拿这三个原则对照一下,看看有没有可以优化的地方。