Loading... >> 单一职责原则(Single responsibility principle,SRP) 我们把它翻译成中文,那就是:**一个类或者模块只负责完成一个职责**(或者功能) 一个类应该仅仅只有一个引起它变化的原因。变化的方向隐含着类的责任 这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。 桥接模式 >> > > # 一、变化的原因 > > 正如 Robert Martin 所说,单一职责的定义经历了一些变化。在《敏捷软件开发:原则、实践与模式》中其定义是,“一个模块应该有且仅有一个变化的原因”;而到了《架构整洁之道》中,其定义就变成了“一个模块应该对一类且仅对一类行为者(actor)负责”。 > > `单一职责原则和一个类只干一件事之间,最大的差别就是,将变化纳入了考量。` > > `不要设计大而全的类,要设计粒度小、功能单一的类。` > > `一个模块最理想的状态是不改变,其次是少改变,它可以成为一个模块设计好坏的衡量标准。` > > 在真实项目中,一个模块之所以会频繁变化,关键点就在于能引起它改变的原因太多了。 > > ```cpp > // 用户类 > class User { > public: > // 修改密码 > void changePassword(String password); > // 加入一个项目 > void joinProject(Project project); > // 接管一个项目,成为管理员 > void takeOverProject(Project project); > ... > } > ``` > 新的需求来了,要求每个用户能够设置电话号码: > > ```cpp > void changePhoneNumber(PhoneNumber phoneNumber): > ``` > 为什么要增加电话号码呢?因为这是用户管理的需求。用户管理的需求还会有很多,比如,用户实名认证、用户组织归属等等。 > > 新需求,要查看一个用户加入了多少项目: > > ```cpp > int countProject(); > ``` > 为什么要查看用户加入多少项目呢?这是项目管理的需求。项目管理的需求还会有很多,比如,团队管理、项目权限等等。 > > 解决办法: > > 一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。 > > 解决这种问题,最好的办法就是把不同的需求引起的变动拆分开来。 > > 针对这里的用户管理和项目管理两种不同需求,我们完全可以把这个 User 类拆成两个类。 > > ```cpp > // 用户类 > class User { > public: > // 修改密码 > void changePassword(String password); > ... > } > > // 项目成员类 > class Member { > public: > // 加入一个项目 > void joinProject(Project project); > // 接管一个项目,成为管理员 > void takeOverProject(Project project); > ... > } > ``` > 这样的话,用户管理的需求只要调整 User 类就好,而项目管理的需求只要调整 Member 类即可,二者各自变动的理由就少了一些。 > > Note: > > 可参考:**5.大类:如何避免写出难以理解的大类? 专题** > > # 二、变化的来源 > >> 想要更好地理解单一职责原则,**重要的就是要把不同的关注点分离出来,分离的是不同的业务关注点,关注点越多越好**,粒度越小越好。这样稳定的类才会越多越好。 >> > > 那应该把哪些内容组织到一起呢?这就需要我们考虑单一职责原则定义的升级版,也就是第二个定义:一个模块应该对一类且仅对一类行为者负责。 > > 如果我们的软件结构不能够与组织结构对应,就会带来一系列麻烦,前面的那个例子只是一个小例子。 > > 当我们更新了对于单一职责原则的理解,你会发现,它的应用范围不仅仅可以放在类这样的级别,也可以放到更大的级别。 > > 要想理解好单一职责原则: > > 我们需要理解封装,知道要把什么样的内容放到一起; > > 我们需要理解分离关注点,知道要把不同的内容拆分开来; > > 我们需要理解变化的来源,知道把不同行为者负责的代码放到不同的地方。 > > # 三、如何去评判类是否职责单一 > >> 一个类的代码行数最好不能超过 200 行,函数个数及属性个数都最好不要超过 10 个。 >> > > 实际上,一些侧面的判断指标更具有指导意义和可执行性,比如下面这几条判断原则: > > **类中的代码行数、函数或属性过多**,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分; > > **类依赖的其他类过多,或者依赖类的其他类过多**,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分; > > **私有方法过多**,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性; > > **比较难给类起一个合适名字**,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰; > > **类中大量的方法都是集中操作类中的某几个属性**,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。 > > ## 1、类的职责是否设计得越单一越好? > >> 单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。 >> > > 下面这个例子的拆分反倒让维护性变得更差了 > > Serialization 类实现了一个简单协议的序列化和反序列功能 > > ```cpp > /** > * Protocol format: identifier-string;{json string} > * For example: UEUEUE;{"a":"A","b":"B"} > */ > class Serialization { > public: > Serialization() > { > gson_ = new Gson(); > } > > String serialize(Map<String, String> object) > { > StringBuilder textBuilder = new StringBuilder(); > textBuilder.append(IDENTIFIER_STRING); > textBuilder.append(gson.toJson(object)); > return textBuilder.toString(); > } > > Map<String, String> deserialize(String text) > { > if (!text.startsWith(IDENTIFIER_STRING)) > { > return Collections.emptyMap(); > } > String gsonStr = text.substring(IDENTIFIER_STRING.length()); > return gson.fromJson(gsonStr, Map.class); > } > > private: > static const String IDENTIFIER_STRING = "UEUEUE;"; > Gson gson_; > } > ``` > 如果让类的职责更加单一,**我们对 Serialization 类进一步拆分,拆分成一个只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer 类**。 > > ```cpp > class Serializer { > public: > Serializer() > { > gson_ = new Gson(); > } > > ~Serializer() > { > delete gson_; > } > > String serialize(Map<String, String> object) > { > StringBuilder textBuilder = new StringBuilder(); > textBuilder.append(IDENTIFIER_STRING); > textBuilder.append(gson.toJson(object)); > return textBuilder.toString(); > } > > private: > static const String IDENTIFIER_STRING = "UEUEUE;"; > Gson gson_; > } > > class Deserializer { > public: > Deserializer() > { > gson_ = new Gson(); > } > > ~Deserializer() > { > delete gson_; > } > > Map<String, String> deserialize(String text) > { > if (!text.startsWith(IDENTIFIER_STRING)) { > return Collections.emptyMap(); > } > String gsonStr = text.substring(IDENTIFIER_STRING.length()); > return gson.fromJson(gsonStr, Map.class); > } > private: > static final String IDENTIFIER_STRING = "UEUEUE;"; > Gson gson_; > } > ``` > 好处:Serializer 类和 Deserializer 类的职责更加单一了。 > > 坏处: > > 如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,**代码的内聚性显然没有原来 Serialization 高了**。 > > 如果我们仅仅对 Serializer 类做了协议修改,而忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差了。 > > 解决办法: > > 既不想违背高内聚的设计思想,也不想违背迪米特法则,那我们该如何解决这个问题呢?实际上,通过引入两个接口就能轻松解决这个问题。 > > 把组合的关系转为继承关系: > > ```cpp > class Serializable > { > public: > String serialize(Map<String, String> object) > { > } > > private: > static final String IDENTIFIER_STRING = "UEUEUE;"; > Gson gson_; > }; > > class Deserializable > { > public: > Map<String, String> deserialize(String text) > { > } > private: > static final String IDENTIFIER_STRING = "UEUEUE;"; > Gson gson_; > }; > > class Serialization : public Serializable, Deserializable > { > public: > @Override > String serialize(Map<String, String> object) > { > StringBuilder textBuilder = new StringBuilder(); > textBuilder.append(IDENTIFIER_STRING); > textBuilder.append(gson.toJson(object)); > return textBuilder.toString(); > } > > @Override > Map<String, String> deserialize(String text) > { > if (!text.startsWith(IDENTIFIER_STRING)) { > return Collections.emptyMap(); > } > String gsonStr = text.substring(IDENTIFIER_STRING.length()); > return gson_.fromJson(gsonStr, Map.class); > } > private: > static const String IDENTIFIER_STRING = "UEUEUE;"; > Gson gson_; > }; > > class DemoClass_1 > { > public: > Demo(Serializable serializer) > { > serializer_ = serializer; > } > //... > private: > Serializable serializer_; > }; > > class DemoClass_2 > { > public: > Demo(Deserializable deserializer) > { > this.deserializer = deserializer; > } > //... > private: > Deserializable deserializer_; > }; > ``` > 上面的的代码实现思路,也体现了“基于接口而非实现编程”的设计原则,结合迪米特法则,我们可以总结出一条新的设计原则,那就是“基于最小接口而非最大实现编程”。 > > 这样的好处: > > 尽管我们还是要往 DemoClass\_1 的构造函数中,传入包含序列化和反序列化的 Serialization 实现类,但是,我们依赖的 Serializable 接口只包含序列化操作,DemoClass\_1 无法使用 Serialization 类中的反序列化接口,对反序列化操作无感知,这也就符合了迪米特法则后半部分所说的“依赖有限接口”的要求。 > > ## 2、举一反三 > >> 可以参考迪米特里法则 >> > > 如果出现Serializer 和 deserialize都有很多成员函数的话,可以拆分开来,下面这种设计思路要更好些 > > ```cpp > class Serializer > { // 参看JSON的接口定义 > public: > String serialize(Object object) { //... } > String serializeMap(Map map) { //... } > String serializeList(List list) { //... } > > Object deserialize(String objectString) { //... } > Map deserializeMap(String mapString) { //... } > List deserializeList(String listString) { //... } > } > ``` > 对于刚刚这个 Serialization 类来说,只包含两个操作,确实没有太大必要拆分成两个接口。但是,**如果我们对 Serialization 类添加更多的功能,实现更多更好用的序列化、反序列化函数**,我们来重新考虑一下这个问题。 > > 因为基于之前的应用场景来说,大部分代码只需要用到序列化的功能。对于这部分使用者,没必要了解反序列化的“知识”,而修改之后的 Serialization 类,反序列化的“知识”,从一个函数变成了三个。一旦任一反序列化操作有代码改动,我们都需要检查、测试所有依赖 Serialization 类的代码是否还能正常工作。 > > 为了减少耦合和测试工作量,我们应该按照迪米特法则,将反序列化和序列化的功能隔离开来。 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏