文章目录
目录
前言
欢迎来到设计模式的第二章🥳,接下来我将结合《Level up your code with design patterns and SOLID》介绍一下在 Unity 开发中常用的设计模式,因为涉及到的文本和代码过多,就不写到一块了,先来看看工厂模式吧!
官方示例项目的下载地址在这里。
此外,《Level up your code with design patterns and SOLID》已被翻译为中文,现已上传到 Github ,个人翻译。本人水平有限,若有错误还请指正😭,如果可以的话,请帮我点个小星星吧!🥹
当我们提到工厂,我们会联想到生产、制造、加工各个零件的场所。工厂模式指定了一个特殊对象,称为工厂,用于专门创建其他对象。在某种程度上,它封装了生成其“产品”所涉及的许多细节。直接的好处是可以简化我们的代码。
工厂模式就是将类“产生对象的流程”集合管理的模式。集合管理带来好处有:
- 能针对对象产生的流程制定规则,我们可以在创建每个实例时运行一些自定义行为。
- 减少客户端参与对象生产的过程,减少客户端与该类的耦合度。
我们还可以将工厂子类化,创建多个专门用于特定产品的工厂。这样做有助于在运行时生成敌人、障碍物或其他任何东西。
当谈及工厂模式时,我们可能会想到工厂方法模式、简单工厂模式以及抽象工厂模式,下面就来分别介绍一下它们。
工厂方法模式
工厂方法模式(Factory method pattern),GoF 对其的解释是,“定义一个可以产生对象的接口,但是让子类决定要产生哪一个类的对象。工厂方法模式让类的实例化程序延迟到子类中实施。”
工厂方法模式是先定义一个接口,之后让它的子类去决定生产哪一种对象。
Unity 官方示例项目中使用的就是这一模式,如下图:
工厂方法模式分为以下几个角色:
- 抽象工厂(Abstract Factory):一个接口,包含一个抽象的工厂方法(用于创建产品对象),如上图中的 Factory。
- 具体工厂(Concrete Factory):实现抽象工厂接口,创建具体的产品,如上图中的 ConcreteFactoryA 和 ConcreteFactoryB。
- 抽象产品(Abstract Product):定义产品的接口,如上图中的 IProduct。
- 具体产品(Concrete Product):实现抽象产品接口,是工厂创建的对象,如上图中的 ProductA 和 ProductB。
举个例子,大家生活中离不开的东西——手机,这就是一个抽象产品;小米手机、华为手机、苹果手机是具体产品;那肯定有生产手机的手机创建规范,也就是抽象工厂;而不同品牌的手机有不同的实现,将手机创建规范落到实处就成了不同的手机工厂,比如小米有小米手机工厂,华为有华为手机工厂,这些就是具体工厂。
也就是:
- 抽象产品:手机,定义了手机的共同行为或接口。
- 具体产品:如小米手机、华为手机、苹果手机,分别实现了手机的具体功能。
- 抽象工厂:这里抽象工厂可以理解为手机创建规范,声明了创建手机的方法。
- 具体工厂:具体的手机工厂(如小米手机工厂、华为手机工厂、苹果手机工厂)实现了手机创建规范,负责生产各自品牌的手机。
示例项目
点击鼠标左键生产随机的产品,按下 R 键重置场景
设计模式中就没有未重构的代码了,我们直接来看官方是怎么实现工厂方法模式的:
- 抽象工厂,Factory 类,作为所有工厂类型的基类,创建产品实例:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.Factory
{
/// <summary>
/// 作为所有工厂类型的基类。工厂创建产品实例
/// </summary>
public abstract class Factory : MonoBehaviour
{
// 获取产品实例的抽象方法
public abstract IProduct GetProduct(Vector3 position);
// 所有工厂共享的方法
public string GetLog(IProduct product)
{
string logMessage = "Factory: created product " + product.ProductName;
return logMessage;
}
}
}
- 抽象产品,IProduct 接口,产品的通用接口:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.Factory
{
/// <summary>
/// 产品的通用接口
/// </summary>
public interface IProduct
{
// 在此处添加通用属性和方法
public string ProductName { get; set; }
// 为每个具体产品自定义此方法
public void Initialize();
}
}
- 具体产品,ProductA 类和 ProductB 类,具体工厂创建的产品,具体的代码实现都一样,这里就贴上 ProductA 的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.Factory
{
public class ProductA : MonoBehaviour, IProduct
{
[SerializeField]
private string m_ProductName = "ProductA";
public string ProductName { get => m_ProductName; set => m_ProductName = value; }
private ParticleSystem m_ParticleSystem;
public void Initialize()
{
// 在此添加独特的设置逻辑
gameObject.name = m_ProductName;
m_ParticleSystem = GetComponentInChildren<ParticleSystem>();
if (m_ParticleSystem == null)
return;
m_ParticleSystem.Stop();
m_ParticleSystem.Play();
}
}
}
- 抽象工厂,ConcreteFactoryA 类 和 ConcreteFactoryB 类,用于创建具体产品,这里也只贴出 ConcreteFactoryA 的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.Factory
{
public class ConcreteFactoryA : Factory
{
// 用于创建一个预制件
[SerializeField]
private ProductA m_ProductPrefab;
public override IProduct GetProduct(Vector3 position)
{
// 创建一个预制件实例并获取产品组件
GameObject instance = Instantiate(m_ProductPrefab.gameObject, position, Quaternion.identity);
ProductA newProduct = instance.GetComponent<ProductA>();
// 每个产品包含其自己的逻辑
newProduct.Initialize();
return newProduct;
}
}
}
示例将每个产品的内部逻辑分离到各自的类中,可以使工厂代码相对简洁。每个工厂只需调用每个产品的 Initialize 方法,而无需了解其底层细节。
优缺点
优点
- 便于扩展:当我们需要添加新的产品类型时,无需修改已有代码,只需创建新的具体产品类并在工厂中注册即可,符合开闭原则。
- 降低耦合:将对象的创建逻辑与使用逻辑分离,客户端代码只需调用工厂方法,无需了解具体产品的实现细节。
- 提高代码可维护性:每个产品的具体逻辑被封装到各自的类中,工厂类只需负责实例化,代码结构更加清晰,便于维护。
缺点
- 增加代码复杂度:我们需要为每个产品创建相应的类和子类,这可能会导致代码量增加,特别是当产品种类较少时,这种模式可能显得冗余。
- 可能引入额外开销:如果产品的种类较少,使用工厂模式可能会增加不必要的复杂性,而直接创建对象可能更简单高效。
简单工厂模式
简单工厂模式不是一种 GoF 设计模式,是一种常见的编程技巧或习惯。简单工厂模式主要包括上主要角色:工厂类、抽象产品、具体产品。
简单工厂模式分为以下几个角色:
- 工厂类:负责创建产品,根据传递的不同参数创建不同的产品示例,如上图中的 Factory
- 抽象产品:定义产品的接口,如上图中的 IProduct。
- 具体产品:实现抽象产品接口,是工厂创建的对象,如上图中的 ProductA 和 ProductB。
简单工厂模式没有抽象工厂和具体工厂,取而代之的是工厂类,我们将对象的创建集中工厂类,使用简单的 if-else 或者 switch- case 语句判断来创建不同的对象,如以下代码:
public class Factory
{
public static IProduct GetProduct(string type)
{
switch (type)
{
case "A":
return new ProductA();
case "B":
return new ProductB();
default:
throw new ArgumentException("Invalid product type");
}
}
}
简单工厂类简化了客户端操作,客户端可以调用工厂方法来获取具体产品,而无需直接与具体产品类交互,降低了耦合,缺点也很明确,如果要添加新的产品就要修改代码,违反了开闭原则。
抽象工厂模式
抽象工厂模式也是一种 GoF 设计模式,提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。通过使用抽象工厂模式,可以将客户端与具体产品的创建过程解耦,使得客户端可以通过工厂接口来创建一族产品。
在工厂方法模式中,每个具体工厂只负责创建单一的产品。但是如果有多类产品呢,比如说“手机”,一个品牌的手机有高端机、中低端机之分,这些具体的产品都需要建立一个单独的工厂类,但是它们都是相互关联的,都共同属于同一个品牌,我们将其这样的一些产品称为产品族,这就可以使用到抽象工厂模式。
抽象工厂模式可以确保一系列相关的产品被一起创建,这些产品能够相互配合使用。
图片来自菜鸟教程
抽象工厂模式包含多个抽象产品接口,多个具体产品类,一个抽象工厂接口和多个具体工厂,每个具体工厂负责创建一组相关的产品。和工厂方法模式不同的是,抽象工厂模式会有多个产品接口。
- 抽象产品接口(Abstract Product):定义产品的接口,可以定义多个抽象产品接口,如上图中的 Shape 和 Color。
- 具体产品类(Concrete Product):实现抽象产品接口,产品的具体实现,如上图中的 Circle、Square、Rectangle 等。
- 抽象工厂接口(Abstract Factory):声明一组用于创建产品的方法,每个方法对应一个产品,如上图中的 AbstractFactory,当然,抽象工厂也可以是类。
- 具体工厂类(Concrete Factory):实现抽象工厂接口,负责创建一组具体产品的对象,如上图中的 ShapeFactory 和 ColorFactory。
优缺点
优点
- 保证产品族内部的一致性:抽象工厂模式确保同一产品族的对象能一起工作,避免了不兼容的问题。
- 降低客户端的依赖:客户端代码依赖于抽象工厂,而不是具体的实现类,因此可以轻松更换不同的产品系列,而无需修改客户端代码。
缺点
扩展产品族非常困难:如果需要新增一个产品族(比如在现有的“手机”产品族基础上新增“智能手表”产品族),则必须修改抽象工厂接口,并实现所有的具体工厂,违反了开闭原则。
补充
除了 Unity 官方给出的使用接口实现工厂方法模式以外,在《设计模式与游戏完美开发》中还提到过4种实现方法,下面就一起来看一下:
第一种方法:由子类产生
定义一个可以产生对象的接口,让子类决定要产生哪一个类的对象:
声明抽象工厂类:
public abstract class AbstractFactory
{
// 子类返回对应的 Product 类型的对象
public abstract AbstractProduct FactoryMethod();
}
FactoryMethod 方法负责生产 AbstractProduct 类的对象, AbstractProduct 类及其子类的实现如下:
public abstract class AbstrctProduct
{}
// 具体产品类 A
public class ConcreteProductA : AbstrctProduct
{
public ConcreteProductA()
{
Debug.Log("生成具体产品类A");
}
}
// 具体产品类 B
public class ConcreteProductB : AbstrctProduct
{
public ConcreteProductB()
{
Debug.Log("生成具体产品类 B");
}
}
之后,分别继承自 AbstractFactory 的子类生产对应的产品类对象:
// 产生 ProductA 的具体工厂
public class ConcreteFactoryProductA : AbstractFactory
{
public ConcreteFactoryProductA()
{
Debug.Log("产生工厂:ConcreteFactoryProductA");
}
public override AbstrctProduct FactoryMethod()
{
return new ConcreteProductA():
}
}
// 产生 ProductB 的具体工厂
public class ConcreteFactoryProductB : AbstractFactory
{
public ConcreteFactoryProductB()
{
Debug.Log("产生工厂:ConcreteFactoryProductB");
}
public override AbstrctProduct FactoryMethod()
{
return new ConcreteProductB():
}
}
两个子类具体工厂分别产生对应的具体产品。
第二种方法:在 FactoryMethod 增加参数
当产品类对象非常多时,容易导致具体工厂子类暴增的情况,不利于后序的维护。当有上述情况时可以改成由单一 FactoryMethod 方法配合传入参数的方法,来决定要产生的产品类对象是哪一个:
public abstract class AbstractFactory_MethodType
{
public abstract Product FactoryMethod(int Type);
}
// 重新实现 FactoryMethod,以返回 Product 类的对象
public class ConcreteFactory_MethodType : Factory_MethodType
{
public ConcreteFactory_MethodType()
{
Debug.Log("生产工厂:ConcreteFactory_MethodType");
}
public override Product FactoryMethod(int Type)
{
switch (Type)
{
case 1:
return new ConcreteProductA();
case 2:
return new ConcreteProductB();
default:
Debug.Log("Type[" + Type + "] 无法产生对象");
break;
}
return null;
}
}
注意一下,以上代码并不是简单工厂模式,由抽象工厂提供抽象方法让子类实现具体的产品创建逻辑,符合开闭原则,新增产品时不修改原有工厂代码。
还有一点,在使用 switch case 语句最后要加上 default 区段,在区段中加上警告信息,提醒有忽略的 Type 被传入,避免新增产品类是,忽略要修改这一段程序的代码。
第三种方法:AbstracrFactory 泛型类
与第一种实现方式比较起来,采用泛型类可省去继承的实现方式,改用指定“T 类型型”的方式,产生对应类的对象:
public class Factory_GenericClass<T> where T : Product, new()
{
public Factory_GenericClass()
{
Debug.Log("生产工厂: Factory_GenericClass<" + typeof(T).ToString() + ">");
}
public Product FactoryMethod()
{
return new T();
}
}
使用泛型类实现时很简洁,只有一个类需要实现。此外,可以使用:
public class Factor_GenericClass<T> where T : Product
的语句来限定 T 类型,只可以带入 Product 群组内的类。
在客户端使用时,与第一种实现方式(由于类产生)一样,要先获取能产生特定产品类的工厂对象,之后再调用工厂对象的 FactoryMethod 来产生对象。
第四种方法:FactoryMethod 泛型方法
因为泛型类不使用继承的方式实现,客户端无法获取“工厂接口”,所以当需要获取工厂接口时,则可改用泛型方法来实现工厂方法模式。
interface IAbstractFactory_GenericMethod
{
Product FactoryMethod<T>() where T: Product, new();
}
// 重新实现 factory method, 以返回 Product 类的对象
public class ConcreteFactory_GenericMethod : IAbstractFactory_GenericMethod
{
public ConcreteFactory_GenericMethod() {
Debug.Log("产生工厂:ConcreteFactory_GenericMethod");
}
public Product FactoryMethod<T>() where T: Product, new() {
return new T();
}
}
使用 C# interface 语句声明一个接口,并定义一个泛型方法 FactoryMethod<T>。客户端可以指定要产生的产品类 T,实现的类就会将 T 类的对象产生出来并返回。而 T 类在声明时,必须指定为 Product 类,且能使用 new 的方式产生。
在测试程序中,通过传入不同的 T 类型,就能产生对应的产品类对象。
使用泛型方法的方法实现,除了拥有“工厂接口”之外,还能免去使用 switch case 语句带来的缺点。另外,可以限定传入 T 的类型,必须是 Product 类,所以当有不属于 Product 群组的类被传入时,C# 在编译阶段就能发现错误。
4种实现方式的选择,一般会按实际情况,分析工厂类与其他游戏系统、客户端的互动情况来决定。不过,在不知选择哪种方式时,建议选择第二种:“利用传入参数来决定要产生的类对象”的方式,因为它能避免产生过多的工厂子类,也不必去编写较复杂的泛型语句。但唯一要忍受不便的是,其中 switch case 语句所带来的缺点,而这也是项目实现中少数可能出现 switch case 语句的地方。
总结
工厂模式的优点是,将产品族对象的产生流程整合于同一个类下实现,并提供唯一的工厂方法,让项目内的“对象产生流程”更加独立。不过,当产品族过多时,无论使用哪种方式,都会出现工厂子类数量过多或 switch case 语句过长的问题。
简单工厂、工厂方法、抽象工厂的区别
-
简单工厂模式:一个工厂方法创建所有具体产品。
-
工厂方法模式:一个工厂方法创建一个具体产品。
-
抽象工厂模式:一个工厂方法可以创建一类具体产品。
什么时候使用工厂模式?
简单工厂模式和工厂方法模式
工厂模式适合用于那些对象创建逻辑复杂、系统需要灵活扩展或保持产品族内部一致性的场景(比如敌人的产生)。通过将对象创建逻辑集中管理,工厂模式不仅能使代码更清晰、易于维护,而且还便于未来扩展和替换具体实现。在应用程序中定义新的产品类型不会改变现有的产品,也不需要修改之前的代码(仅限于工厂方法模式)。
工厂可以根据需要生成任何游戏元素。然而,请注意,创建产品通常不是它们的唯一目的。我们可以将工厂模式用作另一个更大任务的一部分(例如,在对话框中设置 UI 元素或游戏关卡的一部分)。
抽象工厂模式
抽象工厂模式适合用来实现 UI 系统架构,在 Unity 的 UI Toolkit 和 UGUI 之前,还有多种 UI 系统,比如 NGUI、IMGUI等。在面对这么多的工具,开发者应该提供一个方便的架构让这些工具可以快速转换、使用。
设计上,我们可以先将每一个界面组件都设计为一个抽象类,如显示文字的 ILabel,显示图片的 IImage......,并在每个抽象类中定义共同的操作方法;然后针对每一个界面工具继承对应的子类,如针对 NGUI 工具的 NGUILable、NGUIImage...... 等;最后,最后,再针对不同群组的界面组件也实现出能产生它们的工厂,如能产生 NGUI 组件的 NGUIFactory。
对应到抽象工厂模式的组成就是:
-
抽象产品:这些是对界面组件的抽象定义,例如用于显示文字的 IIable、显示图片的 IImage 等。在这些抽象类中,定义所有该类组件的共同行为或操作方法。
-
具体产品:针对不同的界面工具,从抽象产品派生出相应的子类,例如针对 NGUI 工具,可能会有 NGUIIable、NGUIImage 等。这些具体产品实现了抽象产品中定义的共同操作,同时又包含了各自工具的具体特性。
-
抽象工厂:定义一个抽象工厂接口或抽象类,其职责是声明创建一系列相关界面组件的方法。例如,该抽象工厂中可能包含用于创建文字组件、图片组件等的方法,保证所有创建的组件属于同一产品族。
-
具体工厂:针对不同群组的界面组件,实现具体工厂,例如 NGUIFactory。该具体工厂实现了抽象工厂接口中的各个方法,负责实例化并返回对应的具体产品(如 NGUIIable、NGUIImage 等)。
在这样的设计架构下,游戏开发者就能根据不同的需求来选择要使用的界面工具。虽然界面组件的设计摆放上需要使用对应工具,但是在程序设计上,只需要提供不同的界面工厂,就能将界面组件整个转换到不同的工具上。
当然,随着开发工具的演进,会有更多更新的界面开发工具出现。那时只要针对新的开发工具,继承实现新的界面组件及工厂类,就能马上让游戏快速转换到新的开发工具中。
改进
工厂模式的实现可以有很大的不同,考虑在构建工厂模式时进行以下调整:
-
使用字典来搜索产品
可以将产品存储为字典中的键值对,使用唯一的字符串标识符(例如名称或某个 ID)作为键,将产品的类型作为值。这样,在检索产品及其对应的工厂时会更加方便。 -
使工厂(或工厂管理器)静态化
可以将工厂或工厂管理器设计为静态类,这样使用起来更方便。不过需要注意的是,静态类不会出现在检视面板中,因此需要将产品集合也设为静态。 -
将其应用于非游戏对象和非 MonoBehaviour
不要限制在预制件或其他 Unity 特定的组件上,因为工厂模式可以与任何 C# 对象一起工作,这样设计会使代码更加通用和灵活。 -
与对象池模式结合
我们不一定每次都要实例化或创建新对象,而是可以检索层级视图中的现有对象。如果一次实例化许多对象(例如,从武器发射的投射物),那么结合对象池模式将有助于更优化地管理内存。
引用
书籍
蔡升达.(2016). 《设计模式与游戏完美开发》.清华大学出版社
Unity.(2023).《Level up your code with design patterns and SOLID》. Unity E-Book
文章
菜鸟教程.工厂模式
菜鸟教程.抽象工厂模式
nolank128.工厂方法模式
nolank128.抽象工厂模式