背景
最近项目上连续出现了2个跟单一职责原则相关问题,其中一个问题场景如下:
问题一:网络keep-alive配置修改造成线上相关接口不可用。
为了复用网络连接,我们设置了keep-alive时间,在短时间内不让网络断开,提高网络通信效率。为了更好的控制keep-alive时间,我们对该时间进行了配置处理。最近服务器为了进行网络优化,在后台主动调整了该配置值(也有跟客户端同步相关修改),2天后客户端发现相关数据指标显著异常,经过确认确实跟该配置相关。
分析原因:原来该配置控制了2个不同的业务场景,一个场景使用该时间的单位是毫秒(ms),另一个场景使用该时间的单位却是分钟(min)。本次出问题的就是使用分钟的场景。
问题伪代码:
long keepAliveTime = 获取配置keep-alive的时间;
long keepAliveMin = keepAliveTime / (60*1000); //把时间从ms转为min
业务上使用keepAliveMin。
发现上面代码潜在的异常了没? 当keepAliveTime 配置的时间小于1分钟的时候,keepAliveMin计算出来的值为0,这就导致线上相关接口不可用。
问题本质:
该问题看似是一个数据未兜底的问题,其实不然。如果只是数据未兜底问题的话,为何客户端和服务器开发同学都未发现这个问题呢?所以该问题的本质还是职责单一问题,该配置同时对2个业务场景提供能力。 客户端开发同学是后面接手的,这就导致评估的时候只评估到正常使用用的场景,另外一个场景就没有考虑到。
请问,这到底是现有同学评估不到位,还是设计上存在缺陷呢?个人倾向于后者,我们常说安全隐患最容易忽略的就是人为因素,其实写代码也是一样,不可能靠个人的力量每次都能发现类似的问题(人此时也是不可靠的),那就需要我们开发时从技术设计角度上确保开发质量(即我在代码实现时设计一套规则,后面维护的人都可以一眼看出该规则,只要该规则不破坏那业务就不会出问题)。
回到这个问题上,我们如何规避这个问题呢?
- 确保每个配置项职责单一。 即只对一个业务场景生效,不能同时对多个业务生效。
- 数据和业务内聚。 提供统一的获取keep-Alive的接口方法出来,业务上想要获取Keep-Alive的值,只能访问该方法,这样可以做到数据内聚,内聚的好处就是只需要维护这一个方法,而无需关注业务上何时何地的调用它。 若做到这点的话,维护的同学评估这一个方法的影响是不是就简单多了,即使上述有可能为0的风险我想也是可以评估出来的。
什么是单一职责
很多人说单一职责就是只做一件事。这个说法对吗?我想是不完善的,我们看看单一职责的定义:
单一职责定义:只对一个群体提供职责,并且这些职责都封装在一个类中。
这里怎么理解这个 “群体” 呢?可以把它理解为对象,可以是任意对象,但必须是同一种对象。比如我们提供一个获取薪水的方法,你不能既把它提供给财务使用,同时又提供给总经理使用。因为两者不属于同一个使用群体,即使现阶段两者可以共用一个方法,但是他们的业务发展和使用场景是完全不同的,随着迭代肯定会产生差异。上述配置的案例中的群体可以理解为业务对象场景,只对一个业务场景提供职责。
优势
单一职责原则设计的代码具有下面这些优势:
- 高内聚:类应该具有高内聚性,即类中的所有方法都应该紧密相关,共同完成一个特定的功能。
- 低耦合:类之间的耦合度应该低,一个类的变更不应该影响到其他类。
- 可维护性:当类具有单一职责时,修改和维护这个类更加简单,因为修改的影响范围有限。
- 可测试性:职责单一的类更容易编写单元测试,因为测试用例可以专注于测试类的单一功能。
- 灵活性和扩展性:当需要扩展功能时,单一职责的类更容易进行扩展,因为它们的职责清晰明确。
- 变化的独立性:如果一个类只有一个职责,那么它的变化不会影响其他功能,这使得系统更加稳定。
劣势
单一职责原则也并非毫无缺点,其缺陷如下:
- 类的数量增加: 为了严格遵循单一职责原则,可能需要将原本功能较为复杂的类拆分成多个职责单一的类,这无疑会增加系统中类的数量。类的增多可能会带来额外的管理成本和复杂性。
- 需要精细划分职责: 为了满足单一职责原则,需要对问题领域进行深入的理解和分析,以便正确划分职责。如果划分不当,可能导致类的职责过于细碎,增加了代码的繁琐性。
- 代码维护的成本增加: 当一个需求或变更影响到多个相关类或模块时,需要对多个单一职责的类或模块进行修改。这可能增加代码维护的成本和工作量。
- 可能导致系统复杂性增加: 虽然从单个类的角度来看,遵循单一职责原则确实简化了设计,但当放眼整个系统时,由于类的数量增多,类之间的关系也可能变得更加复杂。这就需要开发者在设计和维护时投入更多的精力来确保系统的稳定性和一致性。
- 过度拆分可能限制系统的灵活性和创新性: 在某些情况下,为了提高系统的灵活性和可扩展性,可能需要对某些类进行职责的合并或拆分。这需要开发者根据具体的场景和需求进行权衡和决策。
综上所述,单一职责原则在提高代码的可读性、可维护性和可复用性的同时,也可能带来一些挑战,如增加类的数量、需要精细划分职责、增加维护成本等。因此,在实际应用中,开发者需要根据项目的具体需求和团队的实际情况来权衡利弊,灵活运用这一原则。
一切皆可应用单一职责
一切都可以应用单一职责。不仅仅是类,包括方法,变量等都可以应用单一职责场景。
- 类:给某个群体提供特定的功能。定义行为接口
- 方法:只提供给一个业务场景使用。若需要提供给不同场景使用,可以考虑重载。
- 常量变量:只提供给一个具体场景使用。尽可能将其内聚在业务内部,不让其他模块找到就可以完全规避。
单一职责原则为什么比较难推广?
- 识别职责的难度
首先需要仔细分析类的职责,明确它应该做什么,这通常需要对系统的需求有深入的理解。如果职责划分不清晰,可能会导致类的职责过于分散或集中,难以在设计、实现和测试过程中进行有效的管理和控制。
这里有个小建议:可以先将类的功能职责用接口表示出来,若能用接口表示出来则说明我们对其承担的职责是比较明确的。
- 划分职责的挑战
如果发现一个类承担了多个职责,应该考虑将这些职责划分到不同的类中。每个类只负责一个职责,并且这个职责应该是相对独立且内聚的。这需要开发者具备良好的设计能力和对业务逻辑的深刻理解。
- 重构代码的工作量
根据职责的划分,对代码进行重构,将原有的类拆分为多个类,每个类只包含与其职责相关的属性和方法。这个过程可能会涉及到大量的代码修改和测试,增加了工作量。
重构过程中可以先梳理各个业务模块提供的功能行为,然后做好边界和依赖控制。
- 持续审查的需要
随着系统的不断发展和变化,类的职责也可能会发生变化。因此,需要持续审查类的职责,确保它们仍然符合单一职责原则。这要求团队有持续集成和代码审查的实践。
注意:在审查过程中需要意识到是否有设计原则问题,功能是否内聚,是否可扩展,可维护。
- 团队成员对原则的理解差异
团队成员对单一职责原则理解不一致,可能导致代码风格不统一,影响代码质量。需要进行统一的培训和规范制定,以确保团队成员对原则有共同的理解。
- 权衡类的职责和系统复杂性
虽然单一职责原则有助于设计出更加灵活和可维护的软件系统,但过度拆分也可能导致类的数量过多,增加系统的复杂性。因此,在实际应用中,需要根据具体情况权衡利弊,合理划分类的职责。
- 对项目影响的评估
在推广单一职责原则时,需要评估对现有项目的影响,包括可能的重构成本、时间投入和潜在的风险。
我们可以做些什么
- 培训和教育:对团队成员进行培训,提高他们对单一职责原则的理解。
- 代码审查:通过代码审查来确保代码遵循单一职责原则。
- 示范和案例学习:通过示范项目或案例学习,展示单一职责原则的实际应用和好处。
- 持续反馈和改进:鼓励团队成员提出改进建议,并持续优化设计和代码。
- 制定明确的设计标准:为项目制定明确的设计标准和规范,确保遵循单一职责原则。
总结
我们首先简单复盘了一下线上配置数据异常的问题,然后介绍了单一职责定义和其优劣势;同时指出一切皆可应用单一职责思想,这并不是类的特例;最后指出在项目中推广单一职责原则的难处以及我们可以做哪些。
最后问大家一个问题:在你推广单一职责原则的时候,你的同事跟你说:"这样也可以完成功能呀,先这样吧,没必要。" 你会怎么办呢? (一般这类问题的影响往往不是短时间内可以体现出来的,比如上面的配置也是好几年前的业务了)