我什么时候应该使用抽象工厂模式

本文关键字:抽象 工厂 模式 什么时候 | 更新日期: 2023-09-27 18:26:04

我一直在读抽象工厂模式,但即使我理解它,我也不知道何时使用它。模式的定义说它用于抽象对象实例化,但我不太清楚为什么我需要这样做,为什么我应该关心对象实例化,以及为什么(或何时)抽象它很重要。

我什么时候应该使用抽象工厂模式

该模式的主要好处是,通过将对象创建与对象使用解耦,可以实现干净且可重用的代码。

不幸的是,我手头没有C#示例,但本周我收到的一个示例用例是设计一个需要打开套接字的对象。这是在安卓系统上,某些版本错误地实现了SSL/TLS。

为了解决这个错误,在那些版本的Android上,我们必须对SSL环境进行一些大量定制,才能成功地与后端进行通信。使用抽象工厂可以让我们编写套接字客户端,这样它就不需要知道任何混乱的细节——它只需要一个工厂就可以从中获得套接字。

一个例子:

// this is pretty gross, but what can you do
public class SocketFactorySupplier implements Supplier<SSLSocketFactory> {
  @Override public SSLSocketFactory get() {
    if (androidVersion >= 2.1 && androidVersion <= 2.6) {
      return createShiftyWorkaround();
    } else {
      return getDefaultSocketFactory();
    }
  }
  // here are ~500 lines of SSL code
}
...
public class NetworkClient {
  private final Supplier<SSLSocketFactory> supplier;
  private Socket socket;
  public NetworkClient(Supplier<SSLSocketFactory> supplier) {
    this.supplier = supplier;
  }
  public void connect() {
    socket = supplier.get().createSocket();
    socket.connect();
    // code that doesn't care about SSL at all and is simpler for it
  }
}

这显然不是真正的代码,但它展示了抽象工厂模式的主要好处:

  • NetworkClient代码更干净,因为它不关心如何构建套接字
  • 通过提供mock套接字,将客户端与网络隔离,可以很容易地对其进行测试
  • SSL逻辑可以在其他需要套接字的类中重用
  • 等等

让我们假设您正在编写一个需要与数据库对话的应用程序。您可以有一个类似工厂的Database类来使用Database.CreateCommand方法创建命令。如果需要使用不同的数据库引擎,则需要为每个引擎实现不同的Database

在运行时,您可能不知道需要哪个命令工厂,所以您创建了一个DatabaseManager类,该类具有返回特定类型数据库的DatabaseManager.GetDatabase(databaseType)函数。DatabaseType可以来自配置文件,因此可以很容易地进行更改。

在本例中,每个Database都是一个常规工厂,而DatabaseManager则是一个抽象工厂。这是一个创造其他工厂的工厂。

所以你本质上可以做这样的事情:

Dim sqlCommand as ICommand = DatabaseManager.GetDatabase("MsSQL").CreateCommand

Dim oracleCommand as ICommand = DatabaseManager.GetDatabase("Oracle").CreateCommand

抽象工厂模式在学术示例中没有多大意义。我更喜欢考虑设计模式,而不是基于它们的实现方式,而是基于它们要解决的问题。在这种情况下,抽象工厂模式适用于这样一种情况,即您有一个工厂需要根据编译时不可用的信息进行更改。要以不同的方式说明,当您希望工厂根据只有在运行时才可用的信息创建不同类型的产品时,这很有帮助。功能上的差异包含在工厂产品本身中,而不是使用它们的代码中

我见过的最好的学术例子是单元测试。当你测试工厂产品的消费者时,你通常不想要产品的全部重量级功能,而是想要基本的填充物,这样你就可以隔离消费者的逻辑。按照工厂模式,这是行不通的。对于抽象的工厂模式,这是微不足道的。没有一个总是使用的工厂,而是有两个工厂——一个生产虚拟对象,另一个生产真实产品。单元测试运行时使用虚拟工厂,正常运行时使用真实工厂。

另一个很好的例子是跨平台支持。你可能希望你的工厂生产不同的产品,这取决于你是否在OSX、Windows、Linux、IOS、Android等平台上运行。抽象工厂允许你为每个操作系统实现一个工厂,并在运行时选择合适的工厂。

这是非常简单的用例。额外的抽象层允许您将各种OO设计策略应用于工厂层次结构,最明显的是继承。我发现抽象工厂在与依赖注入相结合时特别强大,无论你是在像Unity这样的托管容器中还是手动进行(这仍然是一种很好的设计实践)。

工厂的主要优点是,它们允许调用对象保持简单,并且在向应用程序添加新功能时不必更改。

假设你有一个简单的购物车用户界面,而你的公司只有一个产品。这个非常具体的代码一开始会很好地工作。

public class MyShoppingCartUI()
{
    private List<IProduct> _productsInCart = new List<IProduct>();
    public void ClickAddProductButton()
    {
        IProduct product = new ProductOne();
        _productsInCart.Add(product);
    }
}

但是,当您的公司添加新产品时,您必须更改UI代码才能直接引用新产品。因此,您必须将UI代码更改为:

public class MyShoppingCartUI()
{
    private List<IProduct> _productsInCart = new List<IProduct>();
    public void ClickAddProductOneButton()
    {
        IProduct product = new ProductOne();
        _productsInCart.Add(product);
    }
    public void ClickAddProductTwoButton()
    {
        IProduct product = new ProductTwo();
        _productsInCart.Add(product);
    }
}

正如你所看到的,每次你有了新产品,你就必须添加更多的代码,你的UI类就会变得越来越大。为什么您的购物车用户界面应该取决于可用的产品?

这可以通过工厂来解决。下面是一个解耦的代码示例:

public class ProductFactory()
{
    public IProduct Create(string productName)
    {
        if (productName == "Product1")
            return new ProductOne();
        else if (productName == "Product2")
            return new ProductTwo();            
    }
}
public class MyShoppingCartUI()
{
    private ProductFactory _factory = new ProductFactory();
    private List<IProduct> _productsInCart = new List<IProduct>();
    public void AddItem(string productName)
    {
        IProduct product = _factory.Create(productName);
        _productsInCart.Add(product);
    }
}

这样,无论您添加多少产品,都不必更改UI代码,因为它不在乎您生产什么产品。您可以将所有控件绑定到产品名称或ID列表中,工厂将为您提供这些对象。

另一个优点是,应用程序的其余部分也可以使用Factory来获取其产品对象,因此您不必单独维护创建产品对象的代码。工厂级中只有1-2条新生产线将永远覆盖您,无论您的应用程序有多大。