zenith-docs 1.0.0 Help

六大原则之单一职责

从这篇开始我们将来说说面向对象设计中的六大设计原则,分别是单一职责原则、开闭原则、里氏替换原则、迪米特法则原则、接口隔离原则、依赖倒置原则。而这篇我们先来说单一职责。

什么是单一职责

单一职责原则(Single Responsibility Principle, SRP),最初是罗伯特·马丁(Robert C.Martin)在《敏捷软件开发:原则、模式与实践》一书中提出的。

从字面意思来讲,就是一个模块(小到函数、大到服务、系统),职责需要单一,不要承担过多的职责。举一个例子:

public class User { public void register() { // 注册 // 注册送积分 } }

我觉得这个 User 类中的 register 方法就不符合单一职责。看似它只是做了与注册相关的事情:

  • 注册,可能是往数据库中插入一条用户记录

  • 注册送积分,更新用户的积分数值

送积分这件事,应该由积分相关的实体方法去处理,而不是有注册这个方法直接去处理。今天是注册送积分,明天还要送会员呢?我们再来看重构的版本:

public class User { public void register() { // 注册 // 采用事件机制或者观察者模式,通知 Score 对象,执行注册送积分相关逻辑 } }

然后我们抽象一个 Score 的对象,来处理所有和积分相关的逻辑:

public class Score { public void incrAfterRegister() { // 注册送积分 } }

所谓职责单一,就是各司其职。分而治之,不要让单一对象承担过多的任务,不利于扩展维护。

深入理解单一职责

从上面的例子中,我们可以提炼出采用单一职责的一些好处:

  • 易于理解 :每个部分只做一件事,使得理解系统的各个部分变得更加容易。

  • 易于修改 :当系统需要变更时,影响范围局限于具有特定职责的部分,减少了意外影响其他功能的风险。

  • 易于扩展 :新增功能时,通常可以通过添加新的组件来实现,而不是修改现有的组件,这样可以避免引入新的错误。

  • 便于复用 :由于每个组件都是独立的,因此在不同的系统中复用某个组件变得更加容易。

单一职责体现了计算机中最基础的思想:分而治之(Divide and Conquer)。但是对于如何分、什么时候分、粒度划分多细,这些问题是比较主观的。或者说需要根据不同的项目、不同的场景来具体分析的。

仍旧是上面这个注册的例子,在项目初期、或者一些小型项目中、或者一些需求固定不易变化的项目中,这么写其实完全是可以的。也不能说是违反了单一原则,是否违反是和设计者设计这个模块时,他期望的粒度有关系的。一般来说,越底层的模块其粒度越细、职责越是单一,越往上层,粒度越粗、职责越大。 所以,切记单一职责是需要具体问题具体分析的

另外,代码和架构都不是一步到位的,需要根据时间的变迁、需求的变更,不断演进的。 一开始是粗粒度的职责设计,后期可以逐渐细化,采用渐进式的架构演进思想。 一开始就采用高可扩展的设计,容易陷入过度设计。所以,我个人坚持在写代码的过程中,不断进行重构,而不是一步到位的设计。

通过面向接口编程来实现单一职责

在实际的开发中,我们会经常通过拆分接口的方式来实现单一职责。比如说,还是以注册为例:在实际工作中,我们的用户可能分为多种类型,不同类型的用户其注册流程也不一样。针对这样的需求,我们可以很快写出如下的代码:

public void register(String type) { if (type.equals("Teacher")) { // to do some things } else if (type.equals("student")) { // to do some things } }

上面的代码就违反了单一职责的原则,并且不好扩展,比如说我要增加校外人员的注册呢?是不是还要增加一个 esle if 的分支,这段代码还需要重新测试过,新增的分支会不会影响到其他类型用户的注册。并且随着逻辑的增加,这个 register 会越发冗长,难以理解和维护。

接着我们通过拆分接口的方法来重构这段代码,使其符合单一原则。首先我们创建了一个名为 IUser 的接口,然后创建 Teacher 以及 Student 两个用户类都实现 IUser 接口:

public class Teacher implements IUser { @Override public void register() { // to do some things } } public class Student implements IUser { @Override public void register() { // to do some things. } }

在原本的 register 方法中,我们就可以面向接口编程了:

public class User { public void register(String type) { factory(type).register(); } public IUser factory(String type) { if (type.equals("Teacher")) { return new Teacher(); } return new Student(); } }

你可能会有疑惑,在 factory 方法中我们仍旧是使用 if...else... 这样的分支结构,这样写不是不好扩展嘛?不是的,理由主要如下:

  • 即使在 factory 方法中使用 if...else... 也比在 register 方法中使用要好,将变化隔离到了 factory 方法中;

  • 在实际场景中,我们会使用反射机制或者容器注入机制来消除 factory 方法中的分支结构;

通过重构,现在这个方法的实现非常的简洁明了、易于扩展。重构后的 UML 图如下:

5p bdahe keggc k tc9j i sh

总结

在本文中,我们深入探讨了单一职责原则(SRP)及其在软件开发实践中的应用。我们首先概述了SRP的基本概念,即一个类或模块应该只有一个改变的理由,强调了将职责细分以提高代码的可维护性和可扩展性的重要性。

随后,我们讨论了 SRP 背后的哲学基础——“分而治之”原则,并指出了在实际开发中根据项目需求、阶段和规模灵活应用SRP的重要性。我们讨论了如何根据具体情况权衡设计的粒度,避免过度设计同时确保代码的灵活性和可维护性。

通过一个具体的例子,我们展示了在实际开发中如何识别和重构违反 SRP 的代码。我们通过将注册功能中的不同职责分离到不同的类中,并利用接口来定义这些类的行为,来演示了面向接口编程如何帮助实现 SRP。这不仅提高了代码的清晰度和可维护性,也增强了系统的可扩展性。

Last modified: 04 August 2024