3.6 为变化而设计
为变化而设计即设计变化的内容和方式。
变化的内容来源于业务需求。从业多年的老程序员会告诉我们需求是不断变化的。而软件需要有一定的适应性来应对这些变化。业务并不关心需求是如何被软件和基础设施团队实现的,他们仅关心需求是否在规定的时间和成本下正确得到了实现。
而软件和基础设施团队更关注业务需求的实现方式。无论项目采用何种技术或流程来实现需求,软件和目标环境都必须能够适应需求的变化。
但这并不是全部内容。众所周知,软件版本经常随着缺陷的修复和新特性的添加而发生变化。随着新特性的实现和重构的实施,一些软件代码将会标记为过时并最终弃用。除此之外,软件供应商会制定软件的路线图并作为应用程序生命周期管理的一部分。最终一些软件版本会被淘汰,软件供应商不再为其提供后续支持。这些不再支持的版本将被迫将主版本迁移至新的受支持的版本上,在此过程中可能涉及一些中断性的更改。
3.6.1 面向接口编程
面向接口编程(Interface-Oriented Programming,IOP)有助于编写多态的代码。面向对象编程中的多态指的是不同的类可以实现相同的接口。这样,通过使用接口就可以达到修改软件以满足业务要求的目的。
以数据库连接为例。若应用程序需要连接到不同的数据源上,那么如何才能达到不论部署何种数据库都令数据库代码保持不变的效果呢?答案当然是使用接口。
可以令不同的数据库连接类实现相同的数据库连接接口。每一个连接类都针对其特定的版本实现接口中的方法,即多态性。数据库以数据库连接接口类型为参数。这样就可以将任何实现了数据库连接接口的数据库连接类型传递到数据库中。以下代码将清晰地展示这个过程。
首先创建一个简单的.NET Framework控制台应用程序,并将Program
类的内容更新如下:
上述代码中,Main()
方法创建了Program
类的实例并调用InterfaceOriented-ProgrammingExample()
方法。InterfaceOrientedProgrammingExample()
方法创建了两个不同数据库(MongoDB和SQL Server)的连接对象。使用MongoDB连接创建了数据库实例,执行了打开和关闭数据库连接的操作;使用SQL Server连接创建了新的数据库实例并赋值给相同的变量,并执行了打开和关闭数据库连接的操作。可见,只需一个包含单一构造器的Database
类就可以和任何实现了相应接口的数据库连接对象协作了。其中IConnection
接口定义如下:
该接口仅仅包含两个方法:Open()
和Close()
。MongoDB
类实现了这个接口:
以上类实现了IConnection
接口。其中的每个方法都向控制台输出了一条消息。Sql-ServerConnection
类也可以如法炮制:
SqlServerConnection
类也实现了IConnection
接口。同样,其中的每个方法都会向控制台输出一条信息。最后,Database
类的实现如下所示:
Database
类(的构造器)接收IConnection
类的参数,并将其赋值给_connection
成员变量。OpenConnection()
方法将打开数据库连接而CloseConnection()
方法将关闭数据库连接。执行上述程序将会在控制台窗口得到如下输出:
综上所述,面向接口编程优点显著。它能够扩展程序的功能而无须修改现有代码。因此,如果需要支持更多数据库类,只需为其编写相应的数据库连接类并实现IConnection
接口。
在了解接口的工作方式后,接下来将介绍如何在依赖注入和控制反转过程中使用它们。依赖注入技术有助于编写低耦合且易于测试的整洁代码,而控制反转可以在必要时替换实现了相同接口的软件的实现。
3.6.2 依赖注入和控制反转
C#中可以使用依赖注入(Dependency Injection,DI)和控制反转(Inversion of Control,IoC)来解决软件变更的问题。虽然这两个术语各自有其含义,但是它们通常是可以互换的,且表示相同的意思。
我们可以使用IoC编写框架,并通过调用模块来完成任务。IoC容器可用于保存注册后的模块。这些模块会在用户或配置需要它们时加载。
DI删除了类中的内部依赖,依赖对象将由外部调用者注入对象。IoC容器就是使用DI将依赖对象注入对象或方法的。
通过本章学习,你将理解IoC和DI,并在程序中使用这些技术。
接下来我们将不依赖任何第三方框架实现简单的DI和IoC程序。
3.6.3 DI范例
本例将创建简单的DI程序。首先,定义ILogger
接口,其中仅含一个接收一个字符串参数的方法;其次,定义TextFileLogger
类实现ILogger
接口,并将字符串输出到文本文件;最后,创建Worker
类来演示构造器注入和方法注入。以下将详细介绍代码。
以下接口中仅包含一个方法,实现该方法的类需要根据方法实现的方式来输出消息:
TextFileLogger
类实现了ILogger
接口并将消息输出到文本文件中:
Worker
类演示了构造器依赖注入和方法依赖注入的方式。请注意,其参数为接口类型。因此任何实现了该接口的类都可以在运行时进行注入:
DependencyInject
方法展示了DI的执行方式:
如上述代码所示,首先我们创建了一个TextFileLogger
类的实例,并将其注入Worker
的构造器。随后以TextFileLogger
实例为参数调用DoSomeWork
方法。这就是将代码通过构造器和方法注入类的方式。
上述代码的优点是它解除了Worker
和TextFileLogger
实例之间的依赖,因此很容易将TextFileLogger
替换为其他实现了ILogger
的日志记录类型。我们也可以将其替换为事件日志记录器或基于数据库的日志记录器。因此,使用DI是降低代码耦合度的好方法。
了解DI的工作方式之后,接下来将介绍IoC。
3.6.4 IoC范例
在本节的范例中,我们会将依赖注册到IoC容器中,并使用DI对必要的依赖进行注入。
以下代码定义了IoC容器。该容器将所有需要注入的依赖保存在字典中,并从配置元数据中获得信息:
在创建容器实例之后,就可以使用它来配置元数据、注册类型,并创建依赖的实例:
在下一节中,我们将介绍如何限制对象了解的信息,通过迪米特法则使得对象仅仅获得与其最相关的信息,避免链条调用式的代码,保持C#代码的整洁。