top of page
作家相片Lingheng Tao

Unity Engine #3 Patterns


这篇文章主要介绍一下各类代码的设计模式。在 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。


单例模式的优势

  1. 控制实例数量:单例模式确保了一个类在应用程序的生命周期内只有一个实例。

  2. 全局访问点:单例实例可以全局访问,因此在应用程序任何地方都可以访问,不再需要重复创建对象或通过参数传递对象;

  3. 保存状态:单例可以在应用程序的生命周期保持状态。这对于存储跨多个系统或组件共享的数据非常有用,例如配置数据、缓存、日志记录等。

  4. 减少资源消耗:因为不再需要创建大量的对象(或销毁),所以可以减少 GC 的调用。

  5. 统一资源管理:当你有一个需要在多个地方使用的共享资源时(例如数据库连接或文件系统的访问),单例可以提供一个统一的点来管理这些资源,确保资源的使用是同步和一致的。

  6. 延迟初始化:单例可以实现延迟初始化(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 赋值时复制引用。


工厂模式


工厂模式是一种创建型设计模式,用于创建对象,而不将对象创建的逻辑暴露给客户端。它提供了一种封装机制,用于创建对象的实例,这样可以让系统更加模块化,增加代码的可维护性和灵活性。


当一个类不知道它所必须创建的对象的类的时候,或者当一个类希望由其子类来指定创建对象的类的时候,或者当类将创建对象的职责委托给多个帮助类中的某一个,局部地希望将哪一个委托者是谁作为信息隐藏的时候,我们就可以使用工厂模式。


类型


工厂模式有三种常见的分支类型。

  1. 简单工厂模式:不属于 GoF 的 23 种设计模式,它使用一个中心化的工厂类来创建所有的实例。

  2. 工厂方法模式:定义一个用于创建对象的接口,但让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。

  3. 抽象工厂模式:提供一个接口用于创建相关或依赖对象的家族,而不需要明确指定具体类。

代码


以工厂方法模式为例,假设我们有一个用于创建不同类型日志记录器的工厂。


// 抽象产品
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)。客户端代码仅与抽象工厂和产品接口交互,使得添加新类型的日志记录器时不需要修改客户端代码。

27 次查看0 則留言

Comments


bottom of page