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

NULL 对象设计模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (28投票s)

2015年10月23日

CPOL

8分钟阅读

viewsIcon

60866

本文将介绍 C# 中的 NULL 设计模式。

引言

问题

NULL 设计模式

IF NULL 检查

NULL 检查存在 VS 默认行为

Mock 测试、TDD 和敏捷开发

使其成为单例和不可变

总结 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 和博客上分享,这样我们就可以避免对这种模式产生很多困惑。

通过项目学习设计模式

更多阅读,请观看下面的面试准备视频和分步视频系列。 

 

© . All rights reserved.