4.6 实现单一职责原则

我们编写的所有对象和方法都应最多拥有一项职责。对象可以有多个方法,但是这些方法的组合都是为了实现其所在对象的单一目标。方法可以调用多个方法,每个方法都有各自不同的功能,但是该方法本身所执行的工作应当只有一种。

一个全知全能的方法称为“上帝方法”(God method);相应地,一个全知全能的对象则称为“上帝对象”(God object)。这种对象和方法是难以阅读、维护或排错的;并且通常它们的缺陷都会多次重复出现。编程基础扎实的程序员都会避免编写上帝对象和上帝方法。以下范例展示了职责多于一个的方法:

该方法的功能显然多于一个,因此它破坏了单一职责原则。接下来我们将其分解为若干功能单一的小方法;同时,由于该方法的参数多于两个,因此我们还将处理该方法中过多的参数。

在开始将其分解为功能单一的小方法之前,首先要梳理一下当前方法执行的动作。该方法首先将文本写入到文件中,此后创建电子邮件信息,向其中添加附件,最终发送电子邮件。因此,我们需要拥有以下功能的方法:

  • 将文本写入文件
  • 创建电子邮件信息
  • 向电子邮件中添加附件
  • 发送邮件

上述方法中和文件写入相关的参数有4个:文件夹、文件名称、文本内容以及媒体类型。其中文件夹和文件名可以组合为一个filename参数。这是因为如果filenamefolder在代码中是两个独立的参数,那么可以将其进行字符插值而合并为一个方法参数,例如$"{folder}{filename}"

至于媒体类型则可以在构造时在结构体内部进行私有设置。通过设置结构体中必要的属性,就可以用包含三个属性的结构体作为单一参数进行传递。以下是相应代码:

上述代码中,GetFileTimeStamp方法的结果将附加到FileName之后。如需保存文件则调用SaveTextFile()方法。请注意,MimeType是在内部设置为MimeType.TextPlain的。虽然也可以简单地使用硬编码设置MimeType,例如:MimeType = "text/plain";。但是使用枚举不但有助于重用代码,而且无须记忆或上网查询特定MimeType对应的文本。以下代码创建了enum类并在enum的值上添加了相应的描述:

创建enum之后仍需找到一种方法获得枚举值上的描述信息,以便对变量进行赋值。因此我们需要创建一个扩展类来获得枚举值的描述信息。这样我们就可以设置MimeType[1]

上述代码若不使用扩展方法,则MimeType属性的值为0。若使用扩展方法,则扩展方法将返回MimeType值的描述信息"text/plain"。该扩展方法也可以依据需要在其他项目中使用。

接下来实现Smtp类,它的职责是使用Smtp协议发送邮件:

Smtp类的构造器仅接收一个Credential类型的参数。该参数用于登录邮件服务器。而邮件服务器是在构造器中指定的。调用SendMessage(MailMessage mailMessage)方法就将发送邮件。

接下来我们编写DemoWorker类,并将上述工作划分到不同的方法中:

DemoWorker类比先前发送邮件的范例更整洁。其中,保存附件并将附件随邮件一起发送的主要方法是DoWork()方法。该方法仅仅包含两行代码,第一行调用SaveTextFile()方法而第二行则调用SendEmail()方法。

SaveTextFile()方法创建了TextFileData结构体对象,设置了文件名和文本内容。随后调用TextFileData结构体中的SaveTextFile()方法。该方法负责将文本保存至指定的文件中。

SendEmail()方法则创建了Smtp对象。Smtp类(的构造器)需要一个Credential参数,而Credential类(的构造器)有两个字符串类型的参数,一个为邮件地址,另一个为密码,它们均用于登录SMTP服务器。在SMTP服务器对象创建之后,即调用Send-Message(MailMessage mailMessage)方法发送邮件。

SendMessage方法需要提供MailMessage对象。我们使用GetMailMessage()方法创建MailMessage对象并将其传入SendMessage(MailMessage mailMessage)方法中。其中,GetMailMessage()方法调用GetAttachment()方法并将附件添加到MailMessage对象中。

经过上述修改,代码变得更加紧凑易读了。而这就是高质量的代码易于修改和维护的关键原因:易读易懂。这也是要求方法短小整洁,并尽量少地使用参数的原因。

你的方法有没有破坏单一职责原则呢?如果有的话,那么应当考虑将方法分割为许多职责单一的方法。至此本章关于编写整洁函数的内容就介绍完了。接下来让我们总结并测试一下学到的知识。


[1]如需MimeType枚举获得其描述,则需要编写一个扩展方法Description(),并使用如下方式获得描述:attachment.ContentType.MediaType = MimeType.TextPlain.Description();不过原书中并没有给出Description方法的实现。因此如需相关代码,请参考:https://github.com/PacktPublishing/Clean-Code-in-C-/blob/master/CH04/EnumExtensions.cs。——译者注