Unity 设计模式 之 结构型模式 -【适配器模式】【桥接模式】 【组合模式】
目录
Unity 设计模式 之 结构型模式 -【适配器模式】【桥接模式】 【组合模式】
一、简单介绍
二、适配器模式 (Adapter Pattern)
1、什么时候使用适配器模式
2、使用适配器模式的好处
3、适配器模式的注意事项
三、在 Unity 中使用 适配器模式
1、定义目标接口 IModelLoader
2、模拟不同的 3D 模型加载器
2.1 OBJ 模型加载器
2.2 FBX 模型加载器
3、创建适配器
3.1 OBJ 模型适配器
3.2 FBX 模型适配器
4、在 Unity 场景中使用适配器模式
5、运行结果分析
四、桥接模式(Bridge Pattern)
1、什么时候使用桥接模式
2、使用桥接模式的好处
五、在 Unity 中使用 桥接模式
1、定义渲染接口 IRenderer
2、实现具体的渲染类
2.1 标准材质渲染器 StandardMaterialRenderer
2.2 自定义材质渲染器 CustomMaterialRenderer
3、定义抽象类 Shape
4、创建具体的形状类
4.1 立方体类 Cube
4.2 球体类 Sphere
5、在 Unity 场景中使用桥接模式
6、运行分析
六、组合模式(Composite Pattern)
1、什么时候使用组合模式
2、使用组合模式的好处
3、使用组合模式时的注意事项
七、在 Unity 中使用 组合模式
1、定义组件接口 IShape
2、实现叶子节点类
2.1 立方体 Cube
2.2 球体 Sphere
2.3 圆柱体 Cylinder
3、实现组合节点 ShapeGroup
4、在 Unity 中使用组合模式
5、运行分析
一、简单介绍
设计模式 是指在软件开发中为解决常见问题而总结出的一套 可复用的解决方案。这些模式是经过长期实践证明有效的 编程经验总结,并可以在不同的项目中复用。设计模式并不是代码片段,而是对常见问题的 抽象解决方案,它提供了代码结构和模块间交互的一种设计思路,帮助开发者解决特定的设计问题。
设计模式的特点:
- 通用性:设计模式针对的是软件开发中常见的设计问题,适用于各种软件工程项目。
- 可复用性:设计模式可以在不同项目和环境下被重复使用,提高代码的可维护性和扩展性。
- 可扩展性:设计模式有助于让代码结构更加灵活,易于扩展和修改。
- 模块化:通过设计模式,可以减少代码的耦合性,增强模块间的独立性。
- 提高沟通效率:设计模式为开发者提供了一种通用的设计语言,使得团队成员能够快速理解并讨论设计方案。
二、适配器模式 (Adapter Pattern)
适配器模式 (Adapter Pattern) 是一种结构型设计模式,作用是将一个类的接口转换为客户希望的另一个接口。适配器使得原本由于接口不兼容而不能一起工作的类可以协同工作。
在开发中,适配器模式通常用于解决现有类和新接口不兼容的问题,而无需修改现有代码。它的核心思想是创建一个适配器类,使得不同接口之间的转换变得透明。
适配器模式有两种主要实现方式:
- 对象适配器:通过组合的方式,适配器类持有一个被适配的对象,并将调用委托给该对象。
- 类适配器:通过继承的方式,适配器类继承自需要适配的类,同时实现目标接口。
1、什么时候使用适配器模式
系统需要使用现有的类,但这个类的接口与系统的接口不兼容时。适配器模式可以在不修改现有类的情况下,使其接口适应新的需求。
你想复用旧代码,并且不想修改旧代码或违反开闭原则时。适配器模式允许你使用旧代码,而不需要更改现有代码库。
需要将类整合到一个不兼容的框架中,而又不希望改动类的定义时。通过适配器模式,你可以创建一个中间层,让新框架能够使用旧类。
集成第三方库:当引入第三方库时,它们的接口可能与现有代码的需求不一致。通过适配器模式,可以实现接口之间的转换,顺利进行集成。
2、使用适配器模式的好处
提高代码的复用性:适配器模式允许复用现有的类和库,无需修改其源代码。通过适配器,你可以将现有类无缝集成到新的系统中,而无需重写它们。
解决接口不兼容问题:当你需要使用一个现有类,但它的接口不符合你当前项目的需求时,可以通过适配器模式解决这一问题。适配器可以“翻译”接口,使得不兼容的类能够在一起工作。
支持旧系统的集成:在软件开发中,项目经常需要集成遗留系统或者第三方库。适配器模式能够很好地帮助你将这些遗留系统整合到新的系统中,扩展其功能而不需要修改旧代码。
分离客户端代码与具体实现:适配器模式将客户端代码与实际实现分离,这使得客户端代码可以适应未来的变化,比如新的接口或实现方式的引入。
3、适配器模式的注意事项
-
适配器本身需要额外的代码:引入适配器可能会增加系统的复杂性,尤其是当涉及多个适配器时。要权衡适配器的引入是否真正有必要。
-
可能引入性能开销:适配器模式通过引入中间层来解决接口不兼容问题,这可能在某些场景下增加调用的性能开销,特别是在大量频繁的接口调用时。
-
适配的灵活性:适配器的实现应该尽可能保持灵活,避免过于依赖具体的实现类。确保适配器可以应对接口或实现方式的变化。
总之,适配器模式 用于解决不兼容接口之间的转换问题,提供了将现有类复用到新环境中的灵活机制。它的主要好处是提高代码的复用性和扩展性,尤其适合在需要集成旧代码或第三方库时使用。通过适配器模式,开发者可以在不修改原始类的情况下,兼容不同接口,使系统更加灵活和可扩展。
三、在 Unity 中使用 适配器模式
在 Unity 中,我们可以使用适配器模式来处理不同类型的 3D 对象渲染,例如不同的 3D 模型格式(比如 OBJ
、FBX
、GLTF
等)或不同的材质处理方式。通过适配器模式,可以将这些不同的 3D 模型格式转换成统一的接口来使用。
我们将模拟一个场景,其中需要加载不同格式的 3D 模型。通过适配器模式,我们可以创建一个统一的接口,让系统无论加载哪种格式的模型,都会以相同的方式进行操作。
步骤概述:
- 定义一个目标接口
IModelLoader
,它用于统一加载模型的接口。- 实现不同的 3D 模型加载器,比如
ObjModelLoader
和FbxModelLoader
,它们各自处理不同格式的模型。- 创建适配器,将这些不同的加载器适配到
IModelLoader
接口。- 使用适配器,在场景中加载不同格式的模型。
参考类图如下:
1、定义目标接口 IModelLoader
该接口定义了一个加载模型的方法 LoadModel()
,通过这个接口,我们可以统一管理不同格式的模型加载。
public interface IModelLoader
{
void LoadModel();
}
2、模拟不同的 3D 模型加载器
在这个示例中,我们会模拟两个不同的 3D 模型加载器,一个用于加载 OBJ
模型,另一个用于加载 FBX
模型。
2.1 OBJ 模型加载器
using UnityEngine;
public class ObjModelLoader
{
public void LoadObjModel()
{
Debug.Log("Loading OBJ model...");
// 模拟加载 OBJ 模型
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.GetComponent<Renderer>().material.color = Color.blue;
}
}
2.2 FBX 模型加载器
using UnityEngine;
public class FbxModelLoader
{
public void LoadFbxModel()
{
Debug.Log("Loading FBX model...");
// 模拟加载 FBX 模型
GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
sphere.GetComponent<Renderer>().material.color = Color.red;
}
}
3、创建适配器
我们将创建适配器类,将 ObjModelLoader
和 FbxModelLoader
适配为统一的 IModelLoader
接口。
3.1 OBJ 模型适配器
public class ObjModelAdapter : IModelLoader
{
private ObjModelLoader objModelLoader;
public ObjModelAdapter(ObjModelLoader loader)
{
objModelLoader = loader;
}
public void LoadModel()
{
objModelLoader.LoadObjModel();
}
}
3.2 FBX 模型适配器
public class FbxModelAdapter : IModelLoader
{
private FbxModelLoader fbxModelLoader;
public FbxModelAdapter(FbxModelLoader loader)
{
fbxModelLoader = loader;
}
public void LoadModel()
{
fbxModelLoader.LoadFbxModel();
}
}
4、在 Unity 场景中使用适配器模式
using UnityEngine;
public class ModelLoaderExample : MonoBehaviour
{
void Start()
{
// 使用 OBJ 模型适配器加载 OBJ 模型
ObjModelLoader objLoader = new ObjModelLoader();
IModelLoader objModelAdapter = new ObjModelAdapter(objLoader);
objModelAdapter.LoadModel(); // 加载并显示 OBJ 模型
// 使用 FBX 模型适配器加载 FBX 模型
FbxModelLoader fbxLoader = new FbxModelLoader();
IModelLoader fbxModelAdapter = new FbxModelAdapter(fbxLoader);
fbxModelAdapter.LoadModel(); // 加载并显示 FBX 模型
}
}
5、运行结果分析
OBJ 模型:当
ObjModelAdapter.LoadModel()
被调用时,适配器会使用ObjModelLoader
来加载一个立方体(模拟的 OBJ 模型),并将其颜色设置为蓝色。FBX 模型:当
FbxModelAdapter.LoadModel()
被调用时,适配器会使用FbxModelLoader
来加载一个球体(模拟的 FBX 模型),并将其颜色设置为红色。
通过适配器模式,我们将不同的 3D 模型加载逻辑统一到了 IModelLoader
接口,使得客户端可以以相同的方式处理不同格式的模型,而无需关心底层的具体实现。
通过适配器模式,我们能够将现有的类和接口兼容起来,简化系统中不同类之间的交互。在 Unity 中,适配器模式可以用于处理不同的资源格式(如模型、音频、材质等)的加载和使用。
它极大地提高了代码的复用性和可扩展性,特别是在处理与第三方库或遗留代码时。通过适配器,客户端代码可以以统一的方式使用不同实现,降低了代码的耦合度。
适配器模式特别适合在 Unity 中处理多种资源加载、外部库集成和扩展功能时使用。例如,将外部导入的模型格式或资产类型转换为系统能够理解和处理的标准格式。
四、桥接模式(Bridge Pattern)
桥接模式(Bridge Pattern) 是一种结构型设计模式,旨在将抽象部分与其实现部分分离,使它们可以独立变化。换句话说,桥接模式通过引入一个桥接接口,将类的抽象层次和实现层次分开,从而使得它们能够分别独立地扩展。
桥接模式通过组合(而不是继承)来实现这种分离,这样可以避免类爆炸式的增长(即由于多个抽象和实现的组合导致的大量子类)。
桥接模式的组成部分
- Abstraction(抽象类):定义了高层的抽象行为,并且包含对
Implementor
(实现类接口)的引用。- RefinedAbstraction(细化抽象类):扩展
Abstraction
,通过组合方式调用Implementor
的方法来实现具体行为。- Implementor(实现类接口):定义实现类的接口,该接口并不与
Abstraction
相关联。- ConcreteImplementor(具体实现类):具体实现
Implementor
接口中的方法。
1、什么时候使用桥接模式
类需要在多个维度上扩展,且这些维度之间是独立变化的。例如,系统需要支持不同的操作系统平台,同时还需要支持多种功能扩展,这时使用桥接模式可以避免多重继承带来的复杂性。
不希望使用继承或者继承层次过深:如果你使用继承,可能会产生很多子类,而桥接模式可以通过组合方式减少类的数量,并使代码更具弹性。
需要动态切换实现时:桥接模式允许你在运行时切换不同的实现类,而不需要修改抽象层次的代码,非常适合需要灵活切换实现的场景。
希望增强代码的可维护性和可测试性:桥接模式通过解耦抽象和实现,使得两者可以独立开发、测试和维护,从而提高代码的可维护性。
2、使用桥接模式的好处
分离抽象与实现:桥接模式允许你将抽象部分与具体实现部分分离开来,使得它们可以独立变化,降低了耦合度。抽象和实现可以分别扩展,互不影响。
扩展性强:由于抽象部分和实现部分可以独立地扩展,因此可以更容易地进行系统扩展,而不需要修改现有代码。比如,你可以为同一抽象引入不同的实现,而不需要修改抽象层次的代码。
减少类爆炸:通过组合代替继承,避免了因为多维度扩展(如平台、功能等)而导致的类爆炸问题。只需添加新的实现类或抽象类,就可以扩展系统。
动态切换实现:桥接模式允许你在运行时动态地切换实现,提供了极大的灵活性。例如,你可以根据不同的环境选择不同的实现类来完成任务。
五、在 Unity 中使用 桥接模式
在 Unity 中,桥接模式可以用于将 渲染方式 与 3D 对象的形状 分离,从而实现更灵活的渲染系统。例如,我们可以有不同的渲染器(比如使用 标准材质 或 自定义材质 进行渲染),同时也可以有不同的形状(如 立方体、球体 等)。桥接模式允许我们将这些渲染器和形状解耦,使它们能够独立扩展。
使用桥接模式在 Unity 中渲染 3D 对象
我们将设计一个系统,能够以不同的方式渲染不同形状的 3D 对象。使用桥接模式,将渲染逻辑和形状解耦。渲染方式可以是使用标准材质渲染,也可以是自定义材质渲染。形状包括立方体和球体。
参考类图如下:
1、定义渲染接口 IRenderer
这个接口定义了一个 Render
方法,接受一个形状名称作为参数,用于渲染对应的形状。
public interface IRenderer
{
void Render(string shape);
}
2、实现具体的渲染类
2.1 标准材质渲染器 StandardMaterialRenderer
using UnityEngine;
public class StandardMaterialRenderer : IRenderer
{
public void Render(string shape)
{
Debug.Log($"Rendering {shape} with Standard Material");
// 创建 Unity 原生物体并应用标准材质
GameObject obj = shape == "Cube" ? GameObject.CreatePrimitive(PrimitiveType.Cube) : GameObject.CreatePrimitive(PrimitiveType.Sphere);
obj.GetComponent<Renderer>().material = new Material(Shader.Find("Standard"));
}
}
2.2 自定义材质渲染器 CustomMaterialRenderer
using UnityEngine;
public class CustomMaterialRenderer : IRenderer
{
public void Render(string shape)
{
Debug.Log($"Rendering {shape} with Custom Material");
// 创建 Unity 原生物体并应用自定义材质(如带颜色的材质)
GameObject obj = shape == "Cube" ? GameObject.CreatePrimitive(PrimitiveType.Cube) : GameObject.CreatePrimitive(PrimitiveType.Sphere);
Material customMaterial = new Material(Shader.Find("Standard"));
customMaterial.color = Color.green; // 自定义颜色
obj.GetComponent<Renderer>().material = customMaterial;
}
}
3、定义抽象类 Shape
Shape
类是抽象类,持有一个 IRenderer
的引用,并定义了 Draw
方法,用于渲染形状。
public abstract class Shape
{
protected IRenderer renderer;
public Shape(IRenderer renderer)
{
this.renderer = renderer;
}
public abstract void Draw();
}
4、创建具体的形状类
4.1 立方体类 Cube
public class Cube : Shape
{
public Cube(IRenderer renderer) : base(renderer) {}
public override void Draw()
{
renderer.Render("Cube");
}
}
4.2 球体类 Sphere
public class Sphere : Shape
{
public Sphere(IRenderer renderer) : base(renderer) {}
public override void Draw()
{
renderer.Render("Sphere");
}
}
5、在 Unity 场景中使用桥接模式
using UnityEngine;
public class BridgePatternExample : MonoBehaviour
{
void Start()
{
// 使用标准材质渲染器渲染立方体
IRenderer standardRenderer = new StandardMaterialRenderer();
Shape cube = new Cube(standardRenderer);
cube.Draw(); // 输出:Rendering Cube with Standard Material
// 使用自定义材质渲染器渲染球体
IRenderer customRenderer = new CustomMaterialRenderer();
Shape sphere = new Sphere(customRenderer);
sphere.Draw(); // 输出:Rendering Sphere with Custom Material
}
}
6、运行分析
- 当
cube.Draw()
被调用时,立方体将使用标准材质渲染。 - 当
sphere.Draw()
被调用时,球体将使用自定义材质(绿色)渲染。
在这个示例中,桥接模式将渲染方式(IRenderer
)和形状(Shape
)进行了分离。通过这种方式,我们可以独立扩展渲染方式和形状。比如你可以为不同的形状添加更多渲染方式(如自发光材质渲染器等),也可以扩展更多的形状,而不需要修改现有代码。
渲染逻辑和形状实现独立变化,增强了系统的扩展性和灵活性。
不同渲染逻辑可以复用,适应不同的形状,而不同形状也可以复用相同的渲染器。
桥接模式适合应用在需要多个维度变化的场景中,例如 Unity 中的渲染系统、音效处理系统、或者动画控制系统。在这些系统中,不同的维度可以独立扩展,而不影响彼此。
六、组合模式(Composite Pattern)
组合模式(Composite Pattern) 是一种结构型设计模式,它将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端对单个对象和组合对象的使用具有一致性。
在组合模式中,叶子对象和容器对象被统一为一个对象接口,客户端可以同样地对待它们。这种模式常用于表示具有层次结构的数据结构,例如文件系统、公司组织结构、GUI 树等。
组合模式的组成部分
- Component(抽象组件):定义了叶子和容器的共同接口,通常包括对子组件的管理方法(如添加、删除子组件)。
- Leaf(叶子节点):表示没有子节点的基本组件,实现
Component
接口。- Composite(组合节点):包含子组件,负责对子组件的管理,实现
Component
接口。
1、什么时候使用组合模式
需要表示对象的部分-整体层次结构时,例如文件系统、图形界面控件树、组织架构等。组合模式适合用于管理树形结构,使得各个节点具有一致的操作接口。
客户端希望一致地对待组合对象和单个对象时。组合模式可以让客户端无需关心处理的是单个对象还是一组对象,极大简化了客户端的代码。
需要动态地管理对象结构时,使用组合模式可以轻松地添加、删除和遍历组合对象及其子对象,而不需要复杂的操作。
2、使用组合模式的好处
一致性:客户端可以一致地处理单个对象和组合对象。组合模式将复杂对象和简单对象的处理方式统一起来,使代码更简洁,不需要分别处理叶子对象和组合对象。
层次结构清晰:组合模式能够清晰地表示对象的层次结构,例如文件系统中的文件和文件夹、公司中的员工和部门等。组合对象可以包含其他组合对象或叶子对象,轻松构建复杂的树形结构。
灵活的对象管理:通过组合模式,可以轻松地向组合对象中添加或移除子组件,动态地管理对象的结构。对于多层次的结构,修改只需要在局部进行,避免全局修改。
更好地扩展性:组合模式允许新增叶子节点或组合节点,不会影响现有的类结构。只需要遵循相同的接口即可轻松扩展对象的种类。
3、使用组合模式时的注意事项
平衡组合对象的复杂度:组合模式虽然简化了客户端代码,但如果对象树过于复杂,会导致系统中的对象关系难以维护。因此在设计时要考虑对象结构的复杂性,避免过度嵌套。
避免滥用组合模式:组合模式适用于具有层次结构的对象,如果应用到不合适的场景,可能会导致设计复杂化。例如在处理单一对象时,使用组合模式可能会显得多余。
叶子节点和组合节点的区别:在实现组合模式时,叶子节点和组合节点的行为应该区分开。叶子节点通常不包含子节点,而组合节点可以包含多个子节点。在设计过程中,要清楚地定义两者的功能和用途。
性能问题:由于组合模式涉及树形结构的递归遍历,因此在处理大量子节点或深层次树形结构时,可能会产生性能问题。特别是在对整个树进行操作时,要注意递归调用的深度和频率。
七、在 Unity 中使用 组合模式
在 Unity 中,组合模式可以应用于管理 3D 对象的层次结构,例如组合复杂的游戏对象,如一个包含多个部分的机器人或车辆。这些对象的每个部分(如身体、轮子、头部等)可以作为叶子节点,而整个对象则作为组合节点。
在这个示例中,我们将使用组合模式实现一个场景,其中包含不同的 3D 形状(立方体、球体、圆柱体等),并将它们组合成一个更复杂的对象。我们可以一致地操作单个形状和组合对象。
参考类图如下:
1、定义组件接口 IShape
这个接口定义了一个 Render
方法,用于渲染 3D 对象。
public interface IShape
{
void Render();
}
2、实现叶子节点类
这些类实现了 IShape
接口,用于渲染具体的 3D 形状。
2.1 立方体 Cube
using UnityEngine;
public class Cube : IShape
{
public void Render()
{
Debug.Log("Rendering Cube");
GameObject.CreatePrimitive(PrimitiveType.Cube);
}
}
2.2 球体 Sphere
using UnityEngine;
public class Sphere : IShape
{
public void Render()
{
Debug.Log("Rendering Sphere");
GameObject.CreatePrimitive(PrimitiveType.Sphere);
}
}
2.3 圆柱体 Cylinder
using UnityEngine;
public class Cylinder : IShape
{
public void Render()
{
Debug.Log("Rendering Cylinder");
GameObject.CreatePrimitive(PrimitiveType.Cylinder);
}
}
3、实现组合节点 ShapeGroup
ShapeGroup
是组合节点,它可以包含多个 IShape
对象,并对它们进行统一的渲染。
using System.Collections.Generic;
using UnityEngine;
public class ShapeGroup : IShape
{
private List<IShape> shapes = new List<IShape>();
public void AddShape(IShape shape)
{
shapes.Add(shape);
}
public void RemoveShape(IShape shape)
{
shapes.Remove(shape);
}
public void Render()
{
Debug.Log("Rendering Shape Group");
foreach (var shape in shapes)
{
shape.Render();
}
}
}
4、在 Unity 中使用组合模式
using UnityEngine;
public class CompositePattern3DExample : MonoBehaviour
{
void Start()
{
// 创建单个形状
IShape cube = new Cube();
IShape sphere = new Sphere();
IShape cylinder = new Cylinder();
// 创建组合对象
ShapeGroup complexObject = new ShapeGroup();
complexObject.AddShape(cube);
complexObject.AddShape(sphere);
// 创建更复杂的组合对象
ShapeGroup complexGroup = new ShapeGroup();
complexGroup.AddShape(complexObject);
complexGroup.AddShape(cylinder);
// 渲染所有对象
Debug.Log("Rendering all shapes:");
complexGroup.Render();
}
}
5、运行分析
当运行代码时,Unity 会在控制台输出如下信息,并且在场景中创建相应的 3D 对象:
Rendering all shapes:
Rendering Shape Group
Rendering Cube
Rendering Sphere
Rendering Cylinder
在场景中,你会看到一个立方体、球体和圆柱体被渲染出来。通过组合模式,我们可以轻松地将单个对象和组合对象进行统一处理,无需为每个对象写单独的渲染代码。
在这个示例中,组合模式允许我们将多个 3D 形状组合在一起,构建一个更复杂的 3D 对象层次结构,并能够一致地操作它们;客户端可以一致地处理单个对象和组合对象,无需关心它们是单一形状还是组合体;可以很方便地向组合对象中添加更多的形状,扩展性非常好。
组合模式适合用在需要表示层次结构的场景中,例如场景中的 3D 物体组合、游戏中的层次化对象管理等;要注意避免过度嵌套结构,尤其是在处理非常复杂的 3D 对象时,树形结构的深度可能会影响性能。