NULL 对象设计模式






4.83/5 (28投票s)
本文将介绍 C# 中的 NULL 设计模式。
引言
闭上眼睛,深呼吸……假设没有地球,没有宇宙,你没有亲戚/朋友,你被 NULL 包围,你就是 NULL,你失去的是 NULL,你得到的是 NULL,但你仍然可以快乐地生活下去。☻☻☻☻
同样,现在假设没有对象,但你的代码仍然可以运行。欢迎来到 NULL 设计模式。
问题
理解任何设计模式的最佳方法是理解它解决了什么问题。所以,让我们先尝试理解 NULL 设计模式要解决的问题。
在面向对象编程的世界里,一个对象与其他对象协作以实现复杂功能是非常常见的做法。
例如,在下面的代码中,"Order" 类正在与 "Discount" 接口进行通信/协作以计算折扣。
class Order
{
public string ProductName { get; set; }
public double ProductCost { get; set; }
private IDiscount _Discount = null;
public double CalculateDiscount()
{
return _Discount.CalculateDiscount(ProductCost);
}
}
"Discount" 类有两种类型:"PremiumDiscount" 和 "FestivalDiscount"。
未来有可能需要添加更多此类折扣计算逻辑。因此,为了确保在添加新的折扣类时,"Order" 类不会受到干扰/更改,我们在 "Order" 和 "Discount" 类之间放置了一个接口。
interface IDiscount
{
double CalculateDiscount(double productCost);
}
public class PremiumDiscount : IDiscount
{
public double CalculateDiscount(double productCost)
{
return (productCost*0.5);
}
}
public class FestivalDiscount : IDiscount
{
public double CalculateDiscount(double productCost)
{
return (productCost * 0.2);
}
}
在 "Order" 类中,我们在构造函数中做了适当的准备,以便注入任何类型的 "Discount" 对象。换句话说,我们遵循了“控制反转”原则来实现解耦。如果你不熟悉 IOC,请阅读这篇文章 控制反转 ( IOC ),如果你对 DI 和 IOC 的区别有疑问,请阅读这篇文章 DI vs IOC。
class Order
{
// Code removed for simplification
private IDiscount _Discount = null;
public Order(IDiscount dis)
{
_Discount = dis;
}
}
因此,由于我们遵循了 IOC,我们在创建任何 "Order" 和 "Discount" 类型的排列组合方面都具有极大的灵活性。
在下面的代码中,你可以看到我们使用 "PremiumDiscount" 对象创建了 "PremiumOrder" 对象,并使用 "FestivalDiscount" 对象创建了 "FestivalOrder" 对象。
Order PremiumOrder = new Order(new PremiumDiscount());
Order FestivalOrder = new Order(new FestivalDiscount());
现在,让我们考虑一个我们希望创建不带折扣的订单的场景。目前,我们没有计算不带折扣的 "Discount" 类。
为什么我们没有呢?因为拥有一个不计算折扣的 "Discount" 类没有意义。
现在,如果我们尝试像下面这样通过传递 NULL 来解决这个问题。
Order NoDiscountOrder = new Order(null);
NoDiscountOrder.CalculateDiscount();
"Order" 对象在与 "Discount" 对象协作时会崩溃。
那么,上述问题的优雅而简洁的解决方案是什么?
NULL 设计模式
我们可以将上述问题和解决方案总结如下:
“我们希望 Order 对象继续执行而不崩溃。为此,我们需要用某种默认行为或 NULL 行为来弥补 discount 对象的缺失。”
换句话说,我们需要创建一个不进行任何折扣计算的折扣类,如下面的代码所示。
public class NullDiscount : IDiscount
{
public double CalculateDiscount(double productCost)
{
return 0;
}
}
现在,下面对 Order 对象的客户端调用将不会崩溃。
Order NoDiscountOrder = new Order(new NullDiscount());
NoDiscountOrder.CalculateDiscount();
IF NULL 检查
作为开发人员,你可能会想到为什么我们不能像下面代码所示那样只进行简单的“IF NULL”检查。
public double CalculateDiscount()
{
if (_Discount == null)
{
return 0;
}
return _Discount.CalculateDiscount(ProductCost);
}
在上述方法中,存在以下问题:
- "Order" 类现在决定如何计算折扣。这明确违反了 SOLID 的 S,即单一职责原则。折扣的计算应该由折扣类来完成,而不是 Order 类。如果你不熟悉 SOLID,请阅读这篇文章 SOLID Principles in C#。
- NULL 类象征着默认行为,而默认行为可以是任何值,不一定是零。NULL 类是领域特定的,而不是技术特定的。很有可能默认折扣可以是 0.1,如下面的代码所示。因此,创建一个类比仅仅在技术上检查 NULL 并返回零值更有意义。
public class NullDiscount : IDiscount
{
public double CalculateDiscount(double productCost)
{
return 0.1;
}
}
- 有一群开发者,包括我,相信“告诉做什么,而不是询问”的原则。所以,与其检查“Discount”对象的当前状态然后采取行动,不如直接要求“Discount”对象做什么。它拥有折扣计算的专业知识,并将提供最佳解决方案。
NULL 检查存在 VS 默认行为
关于此模式的一个误解是,它用于避免 NULL 指针异常。我个人认为 NULL 对于在应用程序中发出错误和问题的信号很重要。吞咽这些错误会导致隐藏的缺陷,这些缺陷稍后可能导致严重的潜在问题。
例如,下面是一个调用函数获取订单数据的简单代码。如果找不到订单数据,它会返回 NULL。
在客户端,我们通过进行 NULL 检查来检查数据的存在。
Order order = GetOrderbyProduct("1001");
if (order == null)
{
// Order does not exist
}
在上面的情况下,我们不应该返回一个默认的 NULL 订单对象。这里我们正在检查对象的存在,在这种情况下,NULL 检查是完全可以的。
提供一个默认的订单对象可能会导致错误未被发现,并引起其他副作用。当一个对象期望另一个对象提供默认行为时,NULL 设计模式应该用于对象协作的场景。
NULL 设计模式是为填充对象缺失而提供默认实现,而不是为了避免 NULL 指针异常。如果发现 NULL 设计模式在没有对象协作的情况下实现,那么该模式的实现方式就存在问题。
是的,NULL 设计模式的附带好处是可以防止许多 NULL 异常,但这并不是该模式的意图。
Mock 测试、TDD 和敏捷开发
此模式的另一个重要用途是在进行单元测试时,特别是回归单元测试。例如,下面是 Order 类中一个简单的“Save”方法,它首先调用折扣计算,然后进行一些验证,最后将订单数据添加到数据库。
class Order
{
// Code removed for clarity
public void Save()
{
double Discount = _Discount.CalculateDiscount(ProductCost);
if (ProductName.Length == 0)
{
throw new Exception("Product Name required");
}
// Call database code goes here
}
}
现在假设你正在敏捷开发环境中工作,我们在每个冲刺(sprint)中交付代码。假设现在“Discount”类将在下一个冲刺中编写。所以你还没有任何具体的折扣类。
但你仍然想对 Order 类进行单元测试。在这种情况下,你也可以创建一个默认的“Discount”类实现,将其注入 Order 类并执行你的单元测试。
在这种情况下,我们也可以将 NULL 类称为 Mock 类,因为我们实际上是在进行 Mock 测试。如果你不熟悉 Mock 测试,请查看这个 YouTube 视频 Mock testing in C#。
使其成为单例和不可变
为了提高性能,创建 NULL 类的单个不可变实例是有意义的。如果你不知道如何使 C# 类不可变,请遵循这篇 文章 中给出的 3 个步骤。因为 NULL 类只有默认值和默认行为,所以缓存它更有意义。在这篇文章中,我不会深入探讨 Singleton 模式,如果你想了解更多,可以阅读这篇文章 Singleton Pattern in C#。
总结 NULL 设计模式
- NULL 设计模式用默认行为填充对象的缺失,并且仅当一个对象与其他对象协作时才应使用。
- NULL 设计模式并非旨在取代 NULL 异常处理。它是 NULL 设计模式的一个附带好处,但其目的是提供默认行为。
- 不应使用 NULL 设计模式对象替换 NULL 检查,因为这可能导致应用程序中的潜在缺陷。
- NULL 设计模式在 Mock 单元测试和敏捷开发中很有用。
- NULL 类是应用 Singleton 设计模式和使类不可变的经典案例。
- 这种模式通常与装饰器模式和工厂模式结合使用。
建议更改名称
我不是一个热衷于建议更改名称的人。但是,在使用这种模式的许多项目中,我觉得 NULL 这个词被误解为 NULL 数据类型。如果你仔细观察这种模式,正确的名称应该是:“默认行为对象设计模式”。因此,如果你觉得这个名字听起来合乎逻辑,请在 Twitter 和博客上分享,这样我们就可以避免对这种模式产生很多困惑。
通过项目学习设计模式
更多阅读,请观看下面的面试准备视频和分步视频系列。
- C# 设计模式分步教程
- C# 面试问答
- ASP.NET MVC 面试题及答案
- Angular 面试题及答案
- 逐步学习Azure。
- SQL Server 分步教程
- C# is vs As 关键字
- C# throw vs throw ex
- C# 并发 vs 并行
- C# 抽象类与接口
- C# 字符串是不可变的