LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

为什么程序开发设计中单一职责原则好理解却用不好?

admin
2024年4月19日 17:56 本文热度 35

经典的设计原则有很多,其中包括:SOLID、KISS、YAGNI、DRY、LOD 等。

下面聊聊 SOLID 原则。SOLID 是面向对象设计和编程中的五个基本原则的首字母缩写,由罗伯特·C·马丁(Robert C. Martin)提出。这些原则旨在帮助开发人员创建易于维护和扩展的软件系统。下面是对这五个原则的详细解释:

1. 单一职责原则(Single Responsibility Principle, SRP)

单一职责原则指出一个类应该只有一个原因引起变化,即一个类应该只负责一项职责。如果一个类承担了过多的职责,那么在修改它以满足一个职责的需求时,可能会产生副作用,从而影响到其他职责的功能。遵循单一职责原则可以使代码更加清晰,降低类的复杂性,提高模块化程度。

2. 开闭原则(Open/Closed Principle, OCP)

开闭原则强调软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在设计一个模块的时候,应该使得这个模块可以在不被修改的前提下进行扩展。这样做可以减少因为修改现有代码而引入的错误,同时也使得系统更加灵活,易于添加新功能。

3. 里氏替换原则(Liskov Substitution Principle, LSP)

里氏替换原则是指子类型必须能够替换掉它们的基类型,即子类对象应该能够替换掉父类对象被使用。这意味着在软件中,子类继承父类时,应该能够保证父类的所有行为在子类中仍然有效。如果违反了这个原则,可能会导致在使用子类替换父类的情况下,程序出现错误或者异常。

4. 接口隔离原则(Interface Segregation Principle, ISP)

接口隔离原则主张接口应该小而专注,不应该强迫客户程序依赖于它们不用的方法。这个原则的目的是降低类与接口之间的耦合度,使得类可以实现它们需要的接口,而不是实现一个庞大的、包含许多不必要方法的接口。这样可以提高系统的灵活性和可维护性。

5. 依赖倒置原则(Dependency Inversion Principle, DIP)

依赖倒置原则是指高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。这个原则的核心思想是通过抽象来减少模块间的耦合,使得系统更加模块化,从而提高代码的可读性、可维护性和可扩展性。

这些设计原则,从字面上理解都不难。一看就感觉懂了,但真的用到项目中的时候,会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。从我之前的工作经历来看,很多同事因为对这些原则理解得不够透彻,导致在使用的时候过于教条主义,拿原则当真理,生搬硬套,反而适得其反。

那么如何更好的理解这些原则呢?下面我通过一个例子来说明,力求使大家能够不仅懂而且会用。

如何理解单一职责原则(SRP)?

单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single responsibility。如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。

注意,这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

无论哪种理解方式,想象一下,单一职责原则就像是给每个工作角色分配一项特定的任务。不管是哪种情况,这个原则都是一个道理:每个角色(或者说类)都应该只做一件事,而且要做好。现在,我们就聊聊在设计一个类的时候,怎么按照这个原则来操作。至于模块怎么用这个原则,你可以自己想一想,原理是类似的。

这个原则其实很简单:一个类就负责一个任务。就像我们不喜欢一个员工同时做太多不同的工作一样,一个类也不应该承担太多功能。如果一个类做了太多不相关的工作,我们就得把它分成几个小类,每个小类只负责一个具体的工作。

比如说,你有一个类,它既处理订单的事情,又处理用户的事情。订单和用户是两码事,对吧?把这两件事放在一个类里,就像让一个人同时做厨师和会计的工作,这显然是不合理的。按照单一职责原则,我们应该把这个类分成两个:一个专门处理订单的类,另一个专门处理用户的类。这样一来,每个类都只关注一件事情,工作起来就更加得心应手了。

如何判断类的职责是否足够单一?

从刚刚这个例子来看,单一职责原则看似不难应用。那是因为我举的这个例子比较极端,一眼就能看出订单和用户毫不相干。但大部分情况下,类里的方法是归为同一类功能,还是归为不相关的两类功能,并不是那么容易判定的。在真实的软件开发中,对于一个类是否职责单一的判定,是很难拿捏的。我举一个更加贴近实际的例子来给你解释一下。

在一个社交产品中,我们用下面的 UserInfo 类来记录用户的信息。你觉得,UserInfo 类的设计是否满足单一职责原则呢?

public class UserInfo {  private long userId;  private String username;  private String email;  private String telephone;  private long createTime;  private long lastLoginTime;  private String avatarUrl;  private String provinceOfAddress; // 省  private String cityOfAddress; // 市  private String regionOfAddress; // 区  private String detailedAddress; // 详细地址 // ...省略其他属性和方法...}

关于UserInfo这个类,大家看法可能不同。有人觉得,既然UserInfo里装的都是关于用户的各种信息,那么它就符合那个所谓的单一职责原则,意思就是一个类只干一种活儿。但另一些人认为,因为UserInfo里地址信息占了很大一部分,所以可以把这部分信息单独拿出来,搞个新的UserAddress类,让UserInfo只保留其他用户信息。这样一来,每个类负责的活儿就更专一了。

那哪种说法更靠谱呢?其实,这得看我们用这个社交软件的具体情况。如果这个软件就是用来展示用户的基本信息,那现在的UserInfo设计就挺好。但如果这个软件后来要加个购物功能,用户的地址信息就得在物流中用到,那我们最好还是把地址信息单独搞出来,弄成个专门的用户物流信息类。

再往深了想,如果这个公司越做越大,又开发了一堆其他应用,还想让所有应用都能用同一个账号登录,那我们就得再对UserInfo动动手脚,把跟登录认证相关的信息,比如邮箱、手机号这些,再抽出来,单独搞个类。

所以说,一个类要不要继续拆,得看我们用它来干嘛,以及将来可能要干嘛。有时候,一个类现在看起来挺合适的,但换个环境或者将来需求变了,就可能不够用了,得继续拆。而且,从不同的角度看同一个类,也可能有不同的想法。比如,从“用户”这个整体来看,UserInfo里的东西都跟用户相关,看起来挺专一的。但如果我们从更细的角度看,比如“用户展示信息”、“地址信息”、“登录认证信息”,那我们可能就得继续拆分UserInfo。

总的来说,判断一个类是不是专一,这事儿挺主观的,没有绝对的标准。在实际编程时,我们也不用太着急,一开始就想得太完美。可以先弄个简单的类,满足现在的需要。等以后业务发展了,如果这个类变得越来越复杂,代码一大堆,那时候再考虑把它拆成几个小类。这个过程,其实就是我们常说的不断改进和调整。

听到这里,你可能会说,这个原则如此含糊不清、模棱两可,到底该如何拿捏才好啊?

这里还有一些小技巧,能够很好地帮你,从侧面上判定一个类的职责是否够单一。而且,个人觉得,下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:

  1. 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;

  2. 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;

  3. 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;

  4. 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;

  5. 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

不过,你可能还会有这样的疑问:在上面的判定原则中,我提到类中的代码行数、函数或者属性过多,就有可能不满足单一职责原则。那多少行代码才算是行数过多呢?多少个函数、属性才称得上过多呢?

比较初级的工程师经常会问这类问题。实际上,这个问题并不好定量地回答,就像你问大厨“放盐少许”中的“少许”是多少,大厨也很难告诉你一个特别具体的量值。

如果继续深究一下的话,你可能还会说,一些菜谱确实给出了,做某某菜需要放多少克盐,放多少克油的具体量值啊。我想说的是,那是给家庭主妇用的,那不是给专业的大厨看的。类比一下做饭,如果你是没有太多项目经验的编程初学者,实际上,我也可以给你一个凑活能用、比较宽泛的、可量化的标准,那就是一个类的代码行数最好不能超过 200 行,函数个数及属性个数都最好不要超过 10 个。

实际上, 从另一个角度来看,当一个类的代码,读起来让你头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函数、属性过多了。实际上,代码写多了,在开发中慢慢“品尝”,自然就知道什么是“放盐少许”了,这就是所谓的“专业第六感”。

类的职责是否设计得越单一越好?

为了满足单一职责原则,是不是把类拆得越细就越好呢?答案是否定的。我们还是通过一个例子来解释一下。Serialization 类实现了一个简单协议的序列化和反序列功能,具体代码如下:

/*** Protocol format: identifier-string;{gson string}* For example: UEUEUE;{"a":"A","b":"B"}*/public class Serialization {    private static final String IDENTIFIER_STRING = "UEUEUE;";    private Gson gson;    public Serialization() {      this.gson = new Gson();    }
   
   public String serialize(Mapobject) {      StringBuilder textBuilder = new StringBuilder();      textBuilder.append(IDENTIFIER_STRING);      textBuilder.append(gson.toJson(object));      return textBuilder.toString();    }
   public Mapdeserialize(String text) {      if (!text.startsWith(IDENTIFIER_STRING)) {      return Collections.emptyMap();      }      String gsonStr = text.substring(IDENTIFIER_STRING.length());      return gson.fromJson(gsonStr, Map.class);    }}

如果我们想让类的职责更加单一,我们对 Serialization 类进一步拆分,拆分成一个只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer 类。拆分后的具体代码如下所示:

public class Serializer {  private static final String IDENTIFIER_STRING = "UEUEUE;";  private Gson gson;  public Serializer() {     this.gson = new Gson();  }
 public String serialize(Mapobject) {      StringBuilder textBuilder = new StringBuilder();      textBuilder.append(IDENTIFIER_STRING);      textBuilder.append(gson.toJson(object));      return textBuilder.toString();  }}
public class Deserializer {  private static final String IDENTIFIER_STRING = "UEUEUE;";  private Gson gson;  public Deserializer() {     this.gson = new Gson();  }
 public Mapdeserialize(String text) {    if (!text.startsWith(IDENTIFIER_STRING)) {       return Collections.emptyMap();    }    String gsonStr = text.substring(IDENTIFIER_STRING.length());    return gson.fromJson(gsonStr, Map.class);  }}

虽然经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差了。

实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。

我们来一块总结回顾一下。

1. 如何理解单一职责原则(SRP)?

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

2. 如何判断类的职责是否足够单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

  • 类中的代码行数、函数或者属性过多;

  • 类依赖的其他类过多,或者依赖类的其他类过多;

  • 私有方法过多;

  • 比较难给类起一个合适的名字;

  • 类中大量的方法都是集中操作类中的某几个属性。

3. 类的职责是否设计得越单一越好?

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。


该文章在 2024/4/19 18:01:13 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved