<<代码之丑>> 05 | 大类:如何避免写出难以理解的大类?

这一讲,我们再来讲一个你一听名字就知道是怎么回事的坏味道:大类。

一听到大类,估计你的眼前已经浮现出一片无边无际的代码了。类之所以成为了大类,一种表现形式就是我们上节课讲到的长函数,一个类只要有几个长函数,那它就肯定是一眼望不到边了大类还有一种表现形式,类里面有特别多的字段和函数,也许,每个函数都不大,但架不住数量众多啊,这也足以让这个类在大类中占有一席之地。

分模块的程序

我先来问你一个问题,为什么不把所有的代码都写到一个文件里?你可能会觉得这个问题很傻,心里想:除了像练习之类的特定场景,谁会在一个正经的项目上 把代码写到一个文件里啊?没错,确实没有人这么做,但你思考过原因吗?把代码都写到一个文件里,问题在哪里呢?

事实是,把代码写到一个文件里,一方面,相同的功能模块没有办法复用;另一方面,也是更关键的,把代码都写到一个文件里,其复杂度会超出一个人能够掌握的认知范围。简言之,一个人理解的东西是有限的,没有人能同时面对所有细节。

人类面对复杂事物给出的解决方案是分而治之。所以,我们看到几乎各种程序设计语言都有自己的模块划分方案,从最初的按照文件划分,到后来,使用面向对象方案按照类进行划分,本质上,它们都是一种模块划分的方式。这样,人们面对的就不再是细节,而是模块,模块的数量显然会比细节数量少,人们的理解成本就降低了。好,你现在已经理解了,对程序进行模块划分,本质上就是在把问题进行分解,而这种做法的背后原因,就是人类的认知能力是有限的。

理解了这一点,我们再回过头来看大类这个坏味道,你就知道问题出在哪了。如果一个类里面的内容太多,它就会超过一个人的理解范畴,顾此失彼就在所难免了。

大类的产生

想要理解怎么拆分一个大类,我们需要知道,这些类是怎么变成这么大的。

  • 职责不单一

    最容易产生大类的原因在于职责的不单一。我们先来看一段代码:

    java
    复制代码
    public class User { private long userId; private String name; private String nickname; private String email; private String phoneNumber; private AuthorType authorType; private ReviewStatus authorReviewStatus; private EditorType editorType; ... }

    这个 User 类拥有着一个大类的典型特征,其中包含着一大堆的字段。面对这样一个类时,我们要问的第一个问题就是,这个类里的字段都是必需的吗?

    我们来稍微仔细地看一下这个类,用户 ID(userId)、姓名(name)、昵称(nickname) 之类应该是一个用户的基本信息,后面的邮箱(email)、电话号码(phoneNumber) 也算是和用户相关联的。今天的很多应用都提供使用邮箱或电话号码登录的方式,所以,这个信息放在这里,也算是可以理解。

    再往后看,作者类型(authorType),这里表示作者是签约作者还是普通作者,签约作者可以设置作品的付费信息,而普通作者不能。后面的字段是作者审核状态(authorReviewStatus),就是说,作者成为签约作者,需要有一个申请审核的过程,这个状态就是审核的状态。

    再往后,又出现了一个编辑类型(editorType),编辑可以是主编,也可以是小编,他们的权限是不一样的。这还不是这个 User 类的全部。但是,即便只看这些内容,也足以让我们发现一些问题了。首先,普通的用户既不是作者,也不是编辑。作者和编辑这些相关的字段,对普通用户来说,都是没有意义的。其次,对于那些成为了作者的用户,编辑的信息意义也不大,因为作者是不能成为编辑的,反之亦然,编辑也不会成为作者,作者信息对成为编辑的用户也是没有意义的。

    在这个类的设计里面,总有一些信息对一部分人是没有意义,但这些信息对于另一部分人来说又是必需的。之所以会出现这样的状况,关键点就在于,这里只有“一个”用户类。普通用户、作者、编辑,这是三种不同角色,来自不同诉求的业务方关心的是不同的内 容。只是因为它们都是这个系统的用户,就把它们都放到用户类里,造成的结果就是,任何业务方的需求变动,都会让这个类反复修改。这种做法实际上是违反了单一职责原则。

    单一职责原则是衡量软件设计好坏的一把简单而有效的尺子,通常来说,很多类之所以巨大,大部分原因都是违反了单一职责原则。而想要破解“大类”的谜题,关键就是能够把不同的职责拆分开来。

    回到我们这个类上,其实,我们前面已经分析了,虽然这是一个类,但其实,它把不同角色关心的东西都放在了一起,所以,它变得如此庞大。我们只要把不同的信息拆分开来,问题也就迎刃而解了。下面就是把不同角色拆分出来的结果

    java
    复制代码
    public class User { private long userId; private String name; private String nickname; private String email; private String phoneNumber; ... }
    java
    复制代码
    public class Author { private long userId; private AuthorType authorType; private ReviewStatus authorReviewStatus; ... }
    java
    复制代码
    public class Editor { private long userId; private EditorType editorType; }

    这里,我们拆分出了 Author 和 Editor 两个类,把与作者和编辑相关的字段分别移到了这两个类里面。在这两个类里面分别有一个 userId 字段,用以识别这个角色是和哪个用户相关。这个大 User 类就这样被分解了。

  • 字段未分组

    大类的产生往往还有一个常见的原因,就是字段未分组。有时候,我们会觉得有一些字段确实都是属于某个类,结果就是,这个类还是很大。比如,我们看一下上面拆分的结果,那个新的 User 类:

    java
    复制代码
    public class User { private long userId; private String name; private String nickname; private String email; private String phoneNumber; ... }

    前面我们分析过,这些字段应该都算用户信息的一部分。但是,即便相比于原来的 User 类小了许多,这个类依然也不算是一个小类,原因就是,这个类里面的字段并不属于同一种类型的信息。比如,userId、name、nickname 几项,算是用户的基本信息,而 email、phoneNumber 这些则属于用户的联系方式。 从需求上看,基本信息是那种一旦确定就不怎么会改变的内容,而联系方式则会根据实际情况调整,比如,绑定各种社交媒体的账号。所以,如果我们把这些信息都放到一个类里面,这个类的稳定程度就要差一些。所以,我们可以根据这个理解,把 User 类的字段分个组,把不同的信息放到不同的类里面。

    java
    复制代码
    public class User { private long userId; private String name; private String nickname; private Contact contact; ... }
    java
    复制代码
    public class Contact { private String email; private String phoneNumber; ... }

    这里我们引入了一个 Contact 类(也就是联系方式),把 email 和 phoneNumber 放了进去,后面再有任何关于联系方式的调整就都可以放在这个类里面。经过这次调整,我们把不同的信息重新组合了一下,但每个类都比原来要小。

    对比一下,如果说前后两次拆分有什么不同,那就是:前面是根据职责,拆分出了不同的实体,后面是将字段做了分组,用类把不同的信息分别做了封装。或许你已经发现了,**所谓的将大类拆解成小类,本质上在做的工作是一个设计工作。**我们分解的依据其实是单一职责这个重要的设计原则。没错,很多人写代码写不好,其实是缺乏软件设计的功底,不能有效地把各种模型识别出来。所以,想要写好代码,还是要好好学学软件设计的。

关于大类的讨论差不多就接近尾声了,但我估计结合这一讲最初的讨论,有些人心中会升起一些疑问:如果我们把大类都拆成小类,类的数量就会增多,那人们理解的成本是不是也会增加呢?

其实,这也是很多人不拆分大类的借口。

在这个问题上,程序设计语言早就已经有了很好的解决方案,所以,我们会看到在各种程序设计语言中,有诸如包、命名空间之类的机制,将各种类组合在一起。在你不需要展开细节时,面对的是一个类的集合。再进一步,还有各种程序库把这些打包出来的东西再进一步打包,让我们只要面对简单的接口,而不必关心各种细节。

总结时刻

我们今天讲了大类这个坏味道,这是程序员日常感知最为深刻的坏味道之一。应对大类的解决方案,主要是将大类拆分成小类。我们需要认识到,模块拆分,本质上是帮助人们降低理解成本的一种方式。我们还介绍了两种产生大类的原因:

  • 职责不单一;
  • 字段未分组。

无论是哪种原因,想要有效地对类进行拆分,我们需要对不同内容的变动原因进行分析,而支撑我们来做这种分析的就是单一职责原则。将大类拆分成小类,本质上在做的是设计工作,所以,想要写好代码,程序员需要学好软件设计。有人觉得拆分出来的小类过多,不易管理,但其实程序设计语言早就为我们提供了各种构造类集合的方式,比如包、命名空间等,再进一步,还可以封装出各种程序库。如果今天的内容你只能记住一件事,那请记住:把类写小,越小越好

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
YYY
下载 APP