设计原则之【单一职责原则】
设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。
以心法为基础,以武器运用招式应对复杂的编程问题。
来吧,通过生活中一个小场景,一起系统学习这6大设计原则。
表妹旅个游,修手机花了一半的时间
表妹:😢哥啊,这次去旅游,真是太扫兴了。😢
我:发生什么事情啦?
表妹:手机摄像头摔坏了,😔光修手机就花了两三天
我:这么不小心,那你没有带相机专门用来拍照嘛?
表妹:没有,有了智能手机,还要带个相机,实在太麻烦了😓
你看,智能手机虽然功能很多,给我们生活带来了很大的便利,但是,一不小心把手机摔坏了,在修手机的过程中,就影响了你的通讯和音视频等功能。
那如果是换成职责更加单一的单反呢?即使不小心把单反摔坏了,也不会影响你的通讯、音视频等功能。
这不就是我们软件开发中的,单一职责原则嘛。
就一个类而言,应该仅有一个引起它变化的原因。
字面意思是,一个类或者模块只负责完成一个职责(或者功能)。
注意:这个原则描述的对象有两个,一个是类(class),一个是模块(module)。模块是更加抽象的概念,或者是看做比类更加粗粒度的代码块,模块中包含多个类。
接下来,从“类”设计的角度来讲解这个设计原则,对于模块也是相通的。
进一步解释,如果这个类包含了两个或多个业务不相干的功能,那么这个类的职责就不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
道理都懂,但是怎么样判断类的职责是否足够单一呢?
在真实的软件开发中,对于一个类是否职责单一的判定,其实是很难拿捏的。
比如,在一个社交产品中有这么一个类:
1 public class UserInfo { 2 private long userID 3 private String userName 4 private String email 5 private String telephone 6 private String province // 地址信息 7 private String city // 地址信息 8 private String region // 地址信息 9 private String detailedAddress // 地址信息 10 // 省略其他属性 11 }
对于这个类,是否满足职责单一原则?有两种不同的观点,一种观点认为,该类包含了跟用户相关的信息,满足单一职责原则;另一种观点认为,地址信息在该类中占比较重,应该拆分出来,UserInfo只保留除地址以外的信息。
至于哪种观点正确?实际上,应该结合实际的应用场景。如果该类中的地址信息跟其他信息一样,只是用来展示,那么UserInfo现在的设计就是合理的。但是如果现在这个社交产品发展得比较好,需要添加电商模块,那么在电商物流中,就会用到用户的地址信息,为了让电商模块更好的复用这部分信息,并且易于后期维护,就需要将地址信息从UserInfo中拆分处理,独立成用户地址信息。
所以你看,不同的应用场景、不同阶段的需求背景下,对一个类的职责是否单一的判定,可能是不一样的。
尽管如此,我们也不应该过度设计,并不是类的职责越单一越好。
我们来看一个实现了简单协议的序列化和反序列化功能的Serialization类。
1 /* 2 Protocol format:identifier-string;{key:value} 3 For example:UEUEUE;{"name":"zhangsan"} 4 */ 5 public class Serialization { 6 private static final String IDENTIFIER_STRING = "UEUEUE;"; 7 private Gson gson; 8 9 public Serialization { 10 this.gson = new Gson(); 11 } 12 13 public String serialize(Map<String, String> object) { 14 StringBuilder text = new StringBuilder(); 15 text.append(IDEXTIFIER_STRING); 16 text.append(gson.toJson(object)); 17 return text.toString(); 18 } 19 20 public Map<String, String> deserialize(String text) { 21 if (!text.startsWith(IDENTIFIER_STRING)) { 22 return Collections.emptyMap(); 23 } 24 String gsonStr = text.substring(IDENTIFIER_STRING.length()); 25 return gson.fromJson(gsonStr, Map.class); 26 } 27 }
如果我们想要让其职责更加单一,可以将其进一步拆分,分别是只负责序列化工作的Serializer类和只负责反序列化的Deserializer类。如下所示:
1 public class Serializer { 2 private static final String IDENTIFIER_STRING = "UEUEUE;"; 3 private Gson gson; 4 5 public Serialization { 6 this.gson = new Gson(); 7 } 8 9 public String serialize(Map<String, String> object) { 10 StringBuilder text = new StringBuilder(); 11 text.append(IDEXTIFIER_STRING); 12 text.append(gson.toJson(object)); 13 return text.toString(); 14 } 15 } 16 17 public class Deserializer { 18 private static final String IDENTIFIER_STRING = "UEUEUE;"; 19 private Gson gson; 20 21 public Serialization { 22 this.gson = new Gson(); 23 } 24 25 public Map<String, String> deserialize(String text) { 26 if (!text.startsWith(IDENTIFIER_STRING)) { 27 return Collections.emptyMap(); 28 } 29 String gsonStr = text.substring(IDENTIFIER_STRING.length()); 30 return gson.fromJson(gsonStr, Map.class); 31 } 32 }
虽然拆分之后,类的职责更加单一了,但也随之带来了新的问题。比如,修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”;或者是序列化方式从JSON改为XML,那么这两个类都需要做相应的修改,代码的内聚性没有拆分前的Serialization高了。如果我们只对Serializer类做了修改,而忘记修改Deserializer类的代码,那么就会导致序列化和反序列化不匹配的问题。可见,拆分后,代码的可维护性变差了。
可见,评价一个类是否足够职责单一,是没有明确的,可以量化的标准的。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况,就有可能说明这个类的设计不满足单一职责原则了:
-
类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
-
类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
-
私有方法过多,我们就要考虑是否将私有方法独立到新的类中,设置public方法,供更多的类使用,从而提高代码的复用性;
-
比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的Manager、Context之类的词语来命名,这就说明类的职责定义得可能不够清晰;
-
类中大量的方法都是集中操作类中的几个属性,比如,在UserInfo例子中,如果一半的方法都是在操作address信息,那就可以考虑将这几个属性和对应的方法拆分出来。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又是最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
你看,单反功能职责单一,诺基亚也相对职责单一,它们就具有易维护、易扩展、易复用,甚至还有可读性的特点。
总结
易维护
在不破坏原有代码设计、不引入新的bug 的情况下,能够快速地修改或者添加代码。
比如iPhone在维修摄像头的时候,如果一个手抖,就可能导致喇叭或麦克风被损坏,从而影响了通讯或音视频功能,因为它们都是在一个集成电路板上的。
但是,单反在维修镜头的时候,就不存在这种情况,因为它的职责更单一。
易扩展
在不修改或少量修改原有代码的情况下,可以通过扩展的方式添加新的功能代码。
中秋节了,拿着iPhone想要拍个月亮发朋友圈,但是不管怎么拍,效果都不好,看到朋友圈的月亮,又大又圆,心想:要是能换个好一点的镜头就好了。
这个时候,只见隔壁老铁把单反装在三脚架上,然后装上长焦镜头,稍微对一下焦,“咔嚓”一声,我凑过去一看,“哇,真的是又大又圆啊!”。
你看,单反可以根据不同的拍照场景,扩展不同的镜头。但是,iPhone尽管有不同的拍照模式,但还是比较有限的,你很难扩展镜头。
易复用
尽量较少重复代码的编写,复用已有的代码。
你看,单反在换镜头的过程中,就是在复用机身。但是,我发现我早年买的iPhone 4s的拍照效果比较差,想要换一个拍照效果更好的。所以,花重金买了iPhone 13,结果发现,拍照效果确实好了,但是,通讯、音视频娱乐等功能,就跟古董iPhone 4s重复了,这笔买卖好像有点不划算吧?
可读性
通过良好的编程规范以及注释,让代码清晰易懂。
职责单一的代码,更加清晰易懂。就好比,我们都知道单反就是用来拍照的,诺基亚就是用来通讯的。但是,iPhone功能太多了,有很多功能是我们基本不会用到的,甚至不知道的。
好啦,每个设计原则是否应用得当,应该根据具体的业务场景,具体分析。
参考资料
《大话设计模式》
极客时间专栏《设计模式之美》