这篇文章主要介绍一下各类代码的设计模式。在 GoF 的《设计模式:可复用面向对象软件的基础》一书中,作者介绍了一共 23 种设计模式。其中,在 Unity 中得到非常广泛和大量使用的模式在这篇笔记里主要写单例模式、观察者模式和工厂模式。
单例模式 Singleton Pattern
单例模式指的是当一个类在场景中只有一个实例时,我们可以给它提供一个全局访问点。常用的情况是各类 Manager。下面是一个 GameManager Singleton 的常规单例模式写法:
public class GameManager : MonoBehaviour
{
// private singleton
private static GameManager _instance;
// public singleton for others to get;
public static GameManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<GameManager>();
if (_instance == null)
{
_instance = new GameObject("Spawned GameManager", typeof(GameManager)).GetComponent<GameManager>();
}
}
return _instance;
}
}
private void Awake()
{
// since we are using the Singleton pattern, we should always assume that we only have the current instance in this scene
if (_instance != null && _instance != this)
{
// if another instance is found, we destroy it;
Destroy(this.gameObject);
}
else
{
_instance = this;
DontDestroyOnLoad(this.gameObject);
}
}
public void SomeFunction() {
// ... some implementation ...
return;
}
}
此后,我们就可以在其它 C# 脚本中,以
GameManager.Instance.SomeFunction();
来调用 GameManager 中的 public 函数和变量了,而不再需要去单独地设置一个 SerializedField 或是查找当前场景的 GameManager instance。
单例模式的优势
控制实例数量:单例模式确保了一个类在应用程序的生命周期内只有一个实例。
全局访问点:单例实例可以全局访问,因此在应用程序任何地方都可以访问,不再需要重复创建对象或通过参数传递对象;
保存状态:单例可以在应用程序的生命周期保持状态。这对于存储跨多个系统或组件共享的数据非常有用,例如配置数据、缓存、日志记录等。
减少资源消耗:因为不再需要创建大量的对象(或销毁),所以可以减少 GC 的调用。
统一资源管理:当你有一个需要在多个地方使用的共享资源时(例如数据库连接或文件系统的访问),单例可以提供一个统一的点来管理这些资源,确保资源的使用是同步和一致的。
延迟初始化:单例可以实现延迟初始化(Lazy Initialization),即直到真正需要使用对象时才创建它。这有助于减少应用程序启动时间和资源消耗。
单例模式的问题
虽然单例模式有很多优点,但也需要注意其潜在的问题。例如,它可能引入全局状态的问题,这可能导致代码难以测试和维护。此外,如果不正确使用,单例可能导致资源同步问题,特别是在多线程环境中。
在使用单例模式时,需要权衡其带来的便利性和潜在的风险,并在适当的场景中谨慎使用。
观察者模式 Observer Pattern
观察者模式定义对象间的一种一对多的依赖关系,当一个对象/被观察者(Subject)的状态改变时,所有依赖于它的观察者(Observers)都得到通知并被自动更新。
当被观察者的改变会需要改变其它的对象,同时,被观察者并不知道有多少观察者正在观察自己时,我们就会使用观察者模式。
下面是观察者模式的常规写法。
// 观察者接口
public interface IObserver
{
void Update();
}
// 被观察者接口
public interface ISubject
{
// 注册新的 Observer
void RegisterObserver(IObserver observer);
// 移除已有的 Observer
void RemoveObserver(IObserver observer);
// 通知目前所有的 Observer
void NotifyObservers();
}
// 具体主体
public class ConcreteSubject : ISubject
{
private List<IObserver> observers = new List<IObserver>();
private float someState;
public void RegisterObserver(IObserver observer)
{
observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
observers.Remove(observer);
}
public void NotifyObservers()
{
foreach (var observer in observers)
{
observer.Update();
}
}
public void ChangeState(float state)
{
someState = state;
NotifyObservers();
}
}
// 具体观察者
public class ConcreteObserver : IObserver
{
private ConcreteSubject subject;
private float observerState;
public ConcreteObserver(ConcreteSubject subject)
{
this.subject = subject;
this.subject.RegisterObserver(this);
}
// 更新操作
public void Update()
{
observerState = subject.SomeState;
}
}
在这里,我们正好对比一下 interface / struct / class 的区别。
Interface 接口
定义:接口是一种规范或合同,它定义了一组方法和属性,但不提供实现。接口只声明成员,不包含成员的实现。
用途:用于定义对象应具有的方法和属性,但不实现这些方法。它允许不相关的类实现相同的接口并被以同样的方式处理。
特性:
不能包含数据字段。
可以包含方法、属性、索引器、事件的声明。
接口成员默认是公开的(public)。
一个类可以实现多个接口。
接口支持多重继承。
Struct 结构体
定义:结构体是一种值类型,用于封装小的、轻量级的对象。
用途:适合用于表示数据结构,如坐标点(Point),颜色(Color)等。
特性:
是值类型,在栈上分配内存。
不支持继承,但可以实现接口。
结构体成员可以是数据、方法、事件等。
结构体可以有构造函数,但不能有默认的无参构造函数。
结构体比类更轻量,适用于小型对象。
复制结构体时,会创建其副本。
Class 类
定义:类是一种引用类型,用于创建对象的模板。
用途:用于封装数据和行为,是面向对象编程的基础。
特性:
是引用类型,在堆上分配内存。
支持继承,一个类只能继承自另一个类。
类成员可以是数据、方法、构造函数、事件等。
类可以有多个构造函数。
复制类对象时,只复制引用。
类可以被声明为 abstract(抽象类)或 sealed(密封类)。
抽象类 abstract class:
定义
抽象类是一种不能被实例化的类。它通常作为基类,定义和部分实现通用的功能,但也包含至少一个抽象方法。这些抽象方法必须在派生类中被实现。
特点
抽象类不能被直接实例化。
抽象类可以包含抽象方法和具体方法。
抽象方法是没有实现的方法,只有声明。
任何继承抽象类的子类必须实现其所有抽象方法,除非子类也是抽象的。
抽象类通常用于提供一个基本框架,以便由子类进行扩展和实现。
密封类 sealed class:
定义
密封类是一种不能被继承的类。换句话说,其他类不能派生自一个密封类。这通常用于安全性和性能优化。
特点
一旦类被声明为密封的,它不能被继承。
密封类可以实例化和使用,就像普通类一样。
密封类主要用于防止进一步的继承,特别是在设计框架或库时,以确保核心功能不被修改。
在某些情况下,密封类可以提高运行时的性能,因为它们使得某些类型的优化成为可能(比如 JIT 编译器优化)。
比较 Class 与 Struct
内存分配:Struct 是值类型,分配在栈上;Class 是引用类型,分配在堆上。
继承:Class 支持继承;Struct 不支持继承;Interface 用于实现多重继承。
默认构造函数:Class 可以有自定义的无参构造函数;Struct 总是有一个默认的无参构造函数。
实例化:Class 实例化时使用 new 关键字;Struct 可以不使用 new 关键字。
赋值行为:Struct 赋值时复制其值;Class 赋值时复制引用。
工厂模式
工厂模式是一种创建型设计模式,用于创建对象,而不将对象创建的逻辑暴露给客户端。它提供了一种封装机制,用于创建对象的实例,这样可以让系统更加模块化,增加代码的可维护性和灵活性。
当一个类不知道它所必须创建的对象的类的时候,或者当一个类希望由其子类来指定创建对象的类的时候,或者当类将创建对象的职责委托给多个帮助类中的某一个,局部地希望将哪一个委托者是谁作为信息隐藏的时候,我们就可以使用工厂模式。
类型
工厂模式有三种常见的分支类型。
简单工厂模式:不属于 GoF 的 23 种设计模式,它使用一个中心化的工厂类来创建所有的实例。
工厂方法模式:定义一个用于创建对象的接口,但让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。
抽象工厂模式:提供一个接口用于创建相关或依赖对象的家族,而不需要明确指定具体类。
代码
以工厂方法模式为例,假设我们有一个用于创建不同类型日志记录器的工厂。
// 抽象产品
public interface ILogger
{
void Log(string message);
}
// 具体产品
public class FileLogger : ILogger
{
public void Log(string message)
{
// 写日志到文件
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
// 输出日志到控制台
}
}
// 抽象工厂
public abstract class LoggerFactory
{
public abstract ILogger CreateLogger();
}
// 具体工厂
public class FileLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger()
{
// 返回 FileLogger 实例
return new FileLogger();
}
}
public class ConsoleLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger()
{
// 返回 ConsoleLogger 实例
return new ConsoleLogger();
}
}
// 客户端代码
class Client
{
static void Main(string[] args)
{
LoggerFactory factory = new FileLoggerFactory();
ILogger logger = factory.CreateLogger();
logger.Log("This is a log message.");
}
}
在这个案例中,LoggerFactory 是一个抽象工厂,定义了创建对象的方法。
具体的工厂类是这边的 FileLoggerFactory 和 ConsoleLoggerFactory,它们去实现了上面定义的方法,用来创建具体的产品(比如 FileLogger 和 ConsoleLogger)。客户端代码仅与抽象工厂和产品接口交互,使得添加新类型的日志记录器时不需要修改客户端代码。
Commentaires