设计模式之游戏–桥接模式
一.引入问题
在游戏引擎中,同一种图形可以由不同的绘图引擎,同一种绘图引擎也可以绘制不同的
图形。从继承的角度上分析的话可以划分如下层次。
通过分析我们发现这样设计有很大的问题:
(1)扩展性问题:如果新增图形类,那么就要新增各绘图引擎下的类,同理新增绘图引擎也是如此,这样到了后期类的个数会呈几何倍数递增。
(2)违反了单一职责原则:一个类XXXX绘制的图形有两个引起这个类变化的原因。
简单来说就是使用继承的方式,不管新增一个类型还是新增一个品牌,都会牵扯出另外一个维度的变化。
二.简单组合改进:
有两个图形类Sphere,Cude和一个绘图引擎类OpenGL,现在我们要实现的功能是使用绘图引擎绘制出这两个图形。一般的做法如下,就是在图形类中持有绘图引擎的引用,并调用里面的绘图方法。
具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour {
void Start () {
Sphere sphere = new Sphere();
sphere.Draw();
}
}
public class Sphere
{
public string name = "Sphere";
public OpenGL openGL = new OpenGL();
public void Draw()
{
openGL.Draw(name);
}
}
public class Cube
{
public string name = "Cube";
public OpenGL openGL = new OpenGL();
public void Draw()
{
openGL.Draw( name);
}
}
public class OpenGL
{
public void Draw(string name)
{
Debug.Log("使用OpenGL绘制出" + name);
}
}
假如现在需求发生改变,新增了一个绘图引擎DriectX,使用新增加的绘图引擎进行绘图,那么我们需要在所有的图形类中都持有DriectX的引用,并新增加一个由DriectX实现的绘图方法。如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour {
void Start () {
//Sphere sphere = new Sphere();
//sphere.Draw();
//Cube cude = new Cube();
//cude.Draw();
Sphere sphere = new Sphere();
sphere.DriecDraw();
Cube cude = new Cube();
cude.DriecDraw();
}
}
public class Sphere
{
public string name = "Sphere";
public OpenGL openGL = new OpenGL();
public DriectX driectL = new DriectX();
public void Draw()
{
openGL.Draw(name);
}
public void DriecDraw()
{
driectL.DriecDraw(name);
}
}
public class Cube
{
public string name = "Cube";
public OpenGL openGL = new OpenGL();
public DriectX driectL = new DriectX();
public void Draw()
{
openGL.Draw( name);
}
public void DriecDraw()
{
driectL.DriecDraw(name);
}
}
public class OpenGL
{
public void Draw(string name)
{
Debug.Log("使用OpenGL绘制出" + name);
}
}
public class DriectX
{
public void DriecDraw(string name)
{
Debug.Log("使用DriectX绘制出" + name);
}
}
通过上面的分析我们可以发现,假如我们继续新增图形和绘图引擎,并要求对绘图引擎进行变更,那么我们每次都要对所有的图形类进行更改,这显然不符合我们的开闭原则,如果我们仔细分析它们的依赖关系,可以发现也违反了我们的依赖倒置原则。
三. 最终解决方式:桥接模式
(1)定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
(2)适用环境:
a.一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
b.如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
c.对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的场景。
(3)UML图:
1) Abstraction(IShape 形状):定义抽象接口,拥有一个Implementor类型的对象引用
2) RefinedAbstraction(具体图形Cube,Shpere等):扩展Abstraction中的抽象接口定义
3) Implementor(IRenderEngine 绘图引擎基类):是具体实现的抽象,
4) ConcreteImplementor(OpenGL等具体的绘图引擎):实现Implementor接口,给出具体实现。
(4)具体实现:
我们可以定义绘图引擎抽象基类IRenderEngine,我们可以在IRenderEngine里定义抽象方法Draw,然后在子类中实现各自的绘图方法Draw,然后定义图形的公共基类IShape,让其持有绘图引擎抽象基类IRenderEngine的引用,在构造的时候传递具体的绘图引擎类。这样就能灵活的实现更换绘图引擎,并且不修改各图形类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
//Sphere sphere = new Sphere();
//sphere.Draw();
//Cube cude = new Cube();
//cude.Draw();
DriectX render= new DriectX();
Sphere sphere = new Sphere(render);
sphere.Draw();
Cube cude = new Cube(render);
cude.Draw();
}
}
public class IShape
{
public string name;
public IRenderEngine renderEngine;
public IShape(IRenderEngine renderEngine)
{
this.renderEngine = renderEngine;
}
public void Draw()
{
renderEngine.Draw(name);
}
}
public class Sphere:IShape
{
public Sphere(IRenderEngine re) :base(re)
{
name = "Sphere";
}
}
public class Cube:IShape
{
public Cube(IRenderEngine re) : base(re)
{
name = "Cube";
}
}
public abstract class IRenderEngine
{
public abstract void Draw(string name);
}
public class OpenGL:IRenderEngine
{
public override void Draw(string name)
{
Debug.Log("使用OpenGL绘制出" + name);
}
}
public class DriectX:IRenderEngine
{
public override void Draw(string name)
{
Debug.Log("使用DriectX绘制出" + name);
}
}
现在当我们新增一个绘图引擎SuperRender,并把当前的绘图引擎变更为它,我们只需增加一个继承IRenderEngine的绘图子类,并实现Draw方法,然后在开始的时候,把原来定义的DriectX 类改为SuperRender,就轻松的实现了变换。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
//Sphere sphere = new Sphere();
//sphere.Draw();
//Cube cude = new Cube();
//cude.Draw();
SuperRender render = new SuperRender();
Sphere sphere = new Sphere(render);
sphere.Draw();
Cube cude = new Cube(render);
cude.Draw();
}
}
public class IShape
{
public string name;
public IRenderEngine renderEngine;
public IShape(IRenderEngine renderEngine)
{
this.renderEngine = renderEngine;
}
public void Draw()
{
renderEngine.Draw(name);
}
}
public class Sphere:IShape
{
public Sphere(IRenderEngine re) :base(re)
{
name = "Sphere";
}
}
public class Cube:IShape
{
public Cube(IRenderEngine re) : base(re)
{
name = "Cube";
}
}
public abstract class IRenderEngine
{
public abstract void Draw(string name);
}
public class OpenGL:IRenderEngine
{
public override void Draw(string name)
{
Debug.Log("使用OpenGL绘制出" + name);
}
}
public class DriectX:IRenderEngine
{
public override void Draw(string name)
{
Debug.Log("使用DriectX绘制出" + name);
}
}
public class SuperRender : IRenderEngine
{
public override void Draw(string name)
{
Debug.Log("使用SuperRender绘制出" + name);
}
}
总结:
1.桥接模式使用对象之间的组合关系解耦了抽象和实现之间强联系,使得抽象和实现可以沿着各自的维度来变化。
2.处理多层继承结构,处理多维度变化的场景,我们可以将将各个维度设计成独立的继承结构,使各个维度可以独立的在抽象层进行扩展,这是桥接模式的设计思想,但桥接模式的应用一般在“两个非常强的变化维度”,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈,换而言之两个变化不会导致纵横交错的结果,并不一定要使用桥接模式。
四.其它应用实例:
角色与武器:
角色:战士,法师,刺客,枪手
武器:长刀,短刀,匕首,太刀,手枪…