65.9K
CodeProject 正在变化。 阅读更多。
Home

理解和实现 Null 对象模式的初学者教程(C#)

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.79/5 (8投票s)

2016年1月21日

CPOL

6分钟阅读

viewsIcon

11964

downloadIcon

115

在本文中,我们将尝试理解空对象模式。

引言

在本文中,我们将尝试理解空对象模式,并看看该模式如何帮助我们编写更健壮的代码。我们将实现一个简单的示例来演示该模式。

背景

世界上没有一个开发者在其开发生涯中没有遇到过“Null reference exception”(空引用异常)。这是什么异常?这个异常简单地告诉我们,我们正在尝试访问或解引用一个值为 NULL 的变量。这就引出了另一个问题:什么是 NULL 值?在大多数编程语言中,NULL 表示值的缺失。对于任何变量,如果其值为 NULL,则意味着该变量没有关联的值,因此调用者不应使用该变量执行任何操作。

从语言的角度来看,NULL 非常重要且有意义,因为在某些情况下,我们希望表示变量中值的缺失。但从代码可维护性的角度来看,这意味着每当我们从另一个模块/类/服务获取对象时,在使用该变量之前都应该始终检查它是否为 null。这可能导致代码变得更加复杂且难以维护。

现在,空对象模式旨在缓解这个问题。该模式建议,每当有相互返回对象的依赖模块时,我们都应始终返回一个对象。在这种情况下,值的缺失也应表示为一个特殊对象。这可以通过创建并返回一个实现了有效类所实现的相同接口的具体类的实例来实现。但是,当对它执行任何操作时,它将什么都不做。这样做可以使调用代码更简单,因为调用代码不必检查 null,只需在返回的对象上调用方法即可。

如果我们尝试可视化空对象模式,它看起来会像这样。

  • Client(客户端)。这个类需要一个对象实例来使用。
  • DependencyBase(依赖基类)。这是一个接口,所有具体类(可能返回给客户端)都将实现它。
  • Dependency(依赖)。这是实际的具体类(可能返回给客户端)。
  • NullObject(空对象)。这是我们的空对象类。当我们要向客户端返回“无值”或值的缺失时,将返回它。换句话说,在所有本应返回 null 的情况下,都将返回这个类。重要的是要注意,这个类将实现 DependencyBase 接口,但将为接口提供“不做任何事”的实现,因为值的缺失意味着不应进行任何操作。

在继续讨论如何实现它之前,让我们先讨论一下何时使用此模式是有意义的。如果我们尝试将此模式用于系统中的所有类,最终将增加系统的复杂性而不是降低它。

只有当某个类期望另一个类,并且使用某种工厂、服务定位器或策略来获取该类的实例时,使用此模式才有意义。这将确保每当从外部模块拉取依赖项时,我们都不必担心 NULL 值,而是外部系统将返回一个 NULL 对象,该对象可以安全地用于执行“什么都不做”类型的操作。

使用代码

现在,让我们来看一个非常简单的示例来理解这个模式。首先,让我们尝试在不使用空对象的情况下理解问题。

在这个示例中,我们将要求用户选择一种运输方式(是的,又是电子商务示例)。根据用户的选择,我们将选择合适的运输策略并将其传递给 OrderProcessor(订单处理器)类。OrderProcessor 类将使用它来安排运输。所以,让我们从查看代表运输策略的接口 IShippingStrategy 开始。

public interface IShippingStrategy
{
	void ScheduleShipping();
}

准备好这个接口后,让我们看看具体的运输类。这些单独的类是因为每个类都将调用相应运输服务提供商的 Web 服务来安排运输(在现实世界中,我们只是在控制台记录方法调用)。

public class DHLShippingStrategy : IShippingStrategy
{
	public void ScheduleShipping()
	{
		Console.WriteLine("DHL Shipping has been scheduled");
	}
}

public class FedExShippingStrategy : IShippingStrategy
{
	public void ScheduleShipping()
	{
		Console.WriteLine("FedEx Shipping has been scheduled");
	}
}

public class InHouseShippingStrategy : IShippingStrategy
{
	public void ScheduleShipping()
	{
		Console.WriteLine("In House Shipping has been scheduled");
	}
}

现在,让我们看看将使用传入的 IShippingStrategy 来安排运输的 OrderProcessor 类。

class OrderProcessor
{
	internal void ProcessOrder(IShippingStrategy shippingStrategy = null)
	{
		if (shippingStrategy != null)
		{
			shippingStrategy.ScheduleShipping();
		}
		else
		{
			Console.WriteLine("Invalid Shipping Strategy");
		}
	}
}

最后,让我们看看模拟用户选择运输方法的代码。

static void Main(string[] args)
{
	Console.WriteLine("Please select the Shipping method 1. FexEd 2. DHL 3. In House Shipping");
	string input = Console.ReadLine();

	int choice = 0;
	bool result = Int32.TryParse(input, out choice);

	if(result == true)
	{
		OrderProcessor orderProcessor = new OrderProcessor();

		IShippingStrategy shippingStrategy = null;

		switch(choice)
		{
			case 1:
				shippingStrategy = new FedExShippingStrategy();
				break;
			case 2:
				shippingStrategy = new DHLShippingStrategy();
				break;
			case 3:
				shippingStrategy = new InHouseShippingStrategy();
				break;
		}

		orderProcessor.ProcessOrder(shippingStrategy);
	}
	
	Console.ReadLine();
}

现在,当我们查看 OrderProcessor 代码时,我们谈论的关于 NULL 值的问题就非常明显了。

订单处理器正在检查传入的策略是否为 null。如果不是,则采取某些操作,否则采取不同的操作。现在这里有两个问题:

  1. 由于所有这些 null 检查,代码看起来很混乱;
  2. 如果选择了无效的运输方式,除了记录之外,还需要做一些事情。这意味着无效情况的处理逻辑也将出现在 OrderProcessor 类中,这直接违反了 Single Responsibility Principle(单一职责原则)。

欢迎空对象

为了避免这个问题,让我们在应用程序中引入一个 Null Object。我们称这个类为 InvalidShippingStrategy(无效运输策略)。

class InvalidShippingStrategy : IShippingStrategy
{
	public void ScheduleShipping()
	{
		Console.WriteLine("Invalid Shipping Strategy");
	}
}

现在有了这个空对象,我们需要修改策略创建代码以适应无效场景。

static void Main(string[] args)
{
	Console.WriteLine("Please select the Shipping method 1. FexEd 2. DHL 3. In House Shipping");
	string input = Console.ReadLine();

	int choice = 0;
	bool result = Int32.TryParse(input, out choice);

	if(result == true)
	{
		OrderProcessor orderProcessor = new OrderProcessor();

		IShippingStrategy shippingStrategy = new InvalidShippingStrategy();

		switch(choice)
		{
			case 1:
				shippingStrategy = new FedExShippingStrategy();
				break;
			case 2:
				shippingStrategy = new DHLShippingStrategy();
				break;
			case 3:
				shippingStrategy = new InHouseShippingStrategy();
				break;
		}

		orderProcessor.ProcessOrder(shippingStrategy);
	}

	Console.ReadLine();
}

最好的部分是,OrderProcessor 完全不必担心无效的运输选择。所有处理无效运输方法以内的代码都可以放在我们的空对象,即 InvalidShippingStrategy 类中。

class OrderProcessor
{
	internal void ProcessOrder(IShippingStrategy shippingStrategy)
	{
		shippingStrategy.ScheduleShipping();
	}
}

这代表了空对象模式的一个非常基础的实现。

单例模式应该有帮助

现在当我们查看我们的空对象时,无论我们创建多少个实例,它的行为都是相同的,即:它将处理“无值-什么都不做”的场景。那么,拥有这个类的多个实例真的有意义吗?可能没有。所以一个优化就是将这个类设为单例。

class InvalidShippingStrategy : IShippingStrategy
{
	private static readonly InvalidShippingStrategy instance = null;

	static InvalidShippingStrategy()
	{
		instance = new InvalidShippingStrategy();
	}

	public static InvalidShippingStrategy Instance
	{
		get
		{
			return instance;
		}
	}

	public void ScheduleShipping()
	{
		Console.WriteLine("Invalid Shipping Strategy");
	}
}

我们需要修改策略创建代码以适应无效场景,因此 program.cs 现在将执行类似的操作:

IShippingStrategy shippingStrategy = InvalidShippingStrategy.Instance;

现在我们有了一个单例的空对象,因此系统中只有一个该空对象的实例,从而获得了一些性能提升。

注意:关于此模式,需要记住的重要一点是,当看到多个类通过传递对象进行协作时,就应该使用它。例如,可以使用 strategy 模式、factory 模式、Service Locator(服务定位器)、Command(命令)模式、Repository(存储库)模式等。为所有类使用此模式并不是一个好主意,它可能导致代码更加复杂。

关注点

在本文中,我们探讨了空对象模式。我们看到了该模式如何使调用代码更加简洁,无需进行 null 检查。我们还研究了该模式的一个非常基础(且不太现实)的实现。这是从初学者的角度编写的。希望这很有启发性。

历史

  • 2016 年 1 月 21 日 - 初始版本
© . All rights reserved.