LOADING

加载过慢请开启缓存 浏览器默认开启

Unity 常用面向对象设计模式系列(3):工厂模式(Factory Method / Abstract Factory)


一、工厂模式核心动机

  1. 解耦创建与使用
    Instantiate(prefab)new Class() 等逻辑从业务模块剥离,业务代码仅通过工厂接口获取实例。

  2. 支持多版本/多平台
    同一接口下,不同工厂可返回不同实现,便于在 Android/iOS 或运营服/测试服间快速切换。

  3. 集中管理依赖
    工厂内部可统一处理资源加载、初始化顺序、对象池取用等,避免重复分散的初始化代码。

  4. 简化测试
    客户端只依赖工厂接口,测试时可替换为 MockFactory,快速验证业务流程。


二、Factory Method(工厂方法)

2.1 意图

一组同类产品 定义一个创建方法(接口),由子类决定实例化哪一个具体产品。工厂方法使得工厂类与产品类的耦合度最小。

2.2 UML 类图

PatternFactory

2.3 Unity 示例:技能工厂

2.3.1 定义产品接口与实现

public interface ISkill {
    void Execute(Transform caster);
}

public class FireballSkill : MonoBehaviour, ISkill {
    public float speed = 10f;
    public GameObject effectPrefab;
    public void Execute(Transform caster) {
        var go = Instantiate(effectPrefab, caster.position, caster.rotation);
        go.GetComponent<Rigidbody>().velocity = caster.forward * speed;
    }
}

public class IceBlastSkill : MonoBehaviour, ISkill {
    public float slowDuration = 2f;
    public void Execute(Transform caster) {
        // 冰冻范围逻辑
    }
}

2.3.2 实现工厂方法

public interface ISkillFactory {
    ISkill CreateSkill();
}

public class FireballFactory : MonoBehaviour, ISkillFactory {
    public FireballSkill prefab;
    public ISkill CreateSkill() {
        return Instantiate(prefab);
    }
}

public class IceBlastFactory : MonoBehaviour, ISkillFactory {
    public IceBlastSkill prefab;
    public ISkill CreateSkill() {
        return Instantiate(prefab);
    }
}

2.3.3 客户端使用

public class PlayerCaster : MonoBehaviour {
    [SerializeField] ISkillFactory skillFactory;
    void Update() {
        if (Input.GetButtonDown("Fire1")) {
            var skill = skillFactory.CreateSkill();
            skill.Execute(transform);
        }
    }
}

优点:新增一种技能类型,只需新增 NewSkillNewSkillFactory,旧代码无需修改。
缺点:每种技能都要写一个工厂类,若技能数目庞大,工厂类会激增。


三、Abstract Factory(抽象工厂)

3.1 意图

提供一个接口,用于创建一 族相关或相互依赖 的一组产品,而无需指定它们的具体类。

3.2 UML 类图

AbstractFactoryPattern

3.3 Unity 示例:UI 抽象工厂

3.3.1 定义 UI 产品接口

public interface IButton {
    void SetText(string text);
    void OnClick(Action callback);
}

public interface IWindow {
    void Show();
    void Hide();
}

3.3.2 ScriptableObject 驱动的抽象工厂

public abstract class UIFactory : ScriptableObject {
    public abstract IButton CreateButton();
    public abstract IWindow CreateWindow();
}

[CreateAssetMenu("UI/MainMenuFactory")]
public class MainMenuFactory : UIFactory {
    public GameObject buttonPrefab;
    public GameObject windowPrefab;
    public override IButton CreateButton() {
        return Instantiate(buttonPrefab).GetComponent<IButton>();
    }
    public override IWindow CreateWindow() {
        return Instantiate(windowPrefab).GetComponent<IWindow>();
    }
}

[CreateAssetMenu("UI/HUDFactory")]
public class HUDFactory : UIFactory {
    public GameObject buttonPrefab;
    public GameObject windowPrefab;
    public override IButton CreateButton() {
        return Instantiate(buttonPrefab).GetComponent<IButton>();
    }
    public override IWindow CreateWindow() {
        return Instantiate(windowPrefab).GetComponent<IWindow>();
    }
}

3.3.3 客户端使用

public class UIManager : MonoBehaviour {
    [SerializeField] UIFactory uiFactory;

    private List<IWindow> windows = new List<IWindow>();

    void Start() {
        var playBtn = uiFactory.CreateButton();
        playBtn.SetText("PLAY");
        playBtn.OnClick(OnPlayClicked);

        var mainWin = uiFactory.CreateWindow();
        windows.Add(mainWin);
        mainWin.Show();
    }

    void OnPlayClicked() {
        foreach (var w in windows) w.Hide();
        // 切换到游戏 HUD
        uiFactory = /* assign HUDFactory */;
        // …
    }
}

优点:同一工厂实例可保证按钮与窗口风格一致;
缺点:新增新 UI 族(如设置界面)需创建新 ScriptableObject 资产。


四、与依赖注入(DI)结合

使用 Zenject 在容器中注册抽象工厂,客户端仅依赖 ISkillFactoryUIFactory

public class GameInstaller : MonoInstaller {
    public UIFactory mainMenuFactory;
    public UIFactory hudFactory;
    public override void InstallBindings() {
        Container.Bind<ISkillFactory>().To<FireballFactory>().FromComponentInHierarchy().AsTransient();
        Container.Bind<UIFactory>().FromInstance(mainMenuFactory).AsSingle();
    }
}
  • 优势

    • 工厂实例通过容器注入,切换实现零侵入;
    • 支持运行时热切换、A/B 测试。

    五、注意事项

    场景 陷阱 建议
    工厂类泛滥 每种产品写一个工厂导致类数量爆炸 使用 ScriptableObject 资产驱动工厂,静态配置减少脚本数量
    资源依赖裸 Instantiate 直接在工厂中 Instantiate 难以 Mock,测试时需加载 Prefab 让工厂依赖 GameObjectProvider,便于替换实现
    业务与工厂逻辑混合 工厂不仅创建,还包含业务初始化 保持工厂纯粹,只做创建,额外初始化放到专用初始化器/构造器
    多平台/多版本切换 工厂逻辑分散,不易统一管理 利用配置表或 DI 容器,根据平台或 AB 测试标志统一绑定工厂实现

    六、小结

    1. 职责单一:工厂只负责实例化,不做任何后续逻辑;
    2. 接口抽象:客户端依赖 IFactory 和产品抽象,无需关心具体类型;
    3. 资源驱动:结合 ScriptableObject,将工厂配置化到资源中,方便迭代与版本管理;
    4. 容器管理:使用 DI 容器统一注册/切换,增强可测试性与扩展性;
    5. 稳定接口:工厂接口一旦定义,尽量不修改方法签名,避免破坏客户端调用。