设计模式の结构类模式
结构类模式更关注与代码的结构,通过多个类型的组合达成一个更复杂的结构来提供一定的可扩展、可维护性。
适配器模式
适配器这个名词已经能够说明该模式的结构了。就像我们笔记本电脑的适配器一样,将220V的交流电转换成电脑能够使用的低电压直流电。
这个情况多发于多人协作或不同团队之间。比如我写了一个贝塞尔曲线的类,A同学觉得我这个贝塞尔曲线比他现在用的好用多了。但是如果使用了我写的贝塞尔曲线类使得他要改好多处代码,这样修改起来累不说还容易出错,最好的办法就是创建一个适配器,将新的贝塞尔曲线包装起来,使得它兼容旧的代码。
public class BadassBezierCurve
{
public void AddPivot(Vector3 vec){}
public Vector3 GetVector(float t){}
}
public class SuckedBezierCurve
{
public void AddPoint(Vector3 vec){}
public Vector3 GetPointAt(float t){}
}
public class BadassBezierCurveWrapper
{
private BadassBezierCurve curve;
public void AddPoint(Vector3 vec)
{
curve.AddPivot(vec);
}
public Vector3 GetPointAt(float t)
{
curve.GetVector(t);
}
}
用了适配器模式以后对比StartModified
方法里的代码,我们在StartAdapted
的代码只有第一行和OldStart
是不一样的.
public class SomeMonoBehaviour : MonoBehaviour
{
private void OldStart()
{
var curve = GetComponent();
curve.AddPoint(Vector3.zero);
curve.AddPoint(Vector3.forward);
curve.AddPoint(Vector3.up);
Debug.Log(curve.GetPointAt(.5f));
}
private void StartAdapted()
{
var curve = GetComponent();
curve.AddPoint(Vector3.zero);
curve.AddPoint(Vector3.forward);
curve.AddPoint(Vector3.up);
Debug.Log(curve.GetPointAt(.5f));
}
private void StartModified()
{
var curve = GetComponent();
curve.AddPivot(Vector3.zero);
curve.AddPivot(Vector3.forward);
curve.AddPivot(Vector3.up);
Debug.Log(curve.GetVector(.5f));
}
}
唉, 我以为适配器模式有多神奇,能让我一行代码都不用改呢,然而适配器模式表示不服.
想想一下,玩意这个BadassBezierCurve虽然很好用,但是有一个不易重现的bug迫使你不得不用回旧的贝塞尔曲线脚本,你会怎么做?
注释掉新的代码,把旧的代码重新加回去?
private void StartModified()
{
//var curve = GetComponent();
var curve = GetComponent();
}
接口在一旁冷笑道:"Naive!".
S.O.L.I.D.五大原则的"D"是什么原则,他的要求是什么?
我们可以这样做:
public interface IBezierCurve
{
void AddPoint(Vector3 vec);
void GetPointAt(float t);
}
public class BadassBezierCurve : IBezierCurve
{
//其余不变
}
public class SuckedBezierCurve : IBezierCurve
{
//其余不变
}
//最终实现
public class SomeMonoBehaviour : MonoBehaviour
{
private void Start()
{
var curve = GetComponent();
//其余不变
}
}
那么最终不论GameObject上的是好的还是坏的贝塞尔曲线,代码一律不用改变.DIP微微一笑深藏功与名.
桥接模式
有时候我们在编程的时候发现我们依赖的多个对象会根据环境进行一定的变化,如果我们要根据这几个对象进行一一适配可能要写很多代码。
比如:
public abstract class A { }
public class a1 : A { }
public class a2 : A { }
public class a1b1 : a1 { }
public class a2b1 : a2 { }
public class a1b2 : a1 { }
public class a2b2 : a1 { }
我们能看到上面b1的功能实现了两次,分别是a1b1
与a2b1
,b2也是一样,这样导致B类型的功能无法通过类的继承传下去,因为所有的类都继承于A类型,而C#语言是不支持多类型继承的。这只是两个类型的组合而已。
这种情况就比较适合桥接模式,将A和B两种类型用桥搭在一起,这样就可以组合出所有可能的方案而不需要多写代码。当然,此处也依旧发挥了DIP的作用:类的双方应该依赖于抽象。
回到我之前写的运动会的跑步功能时,我不确定我应该用哪种方式来定义一个跑道的路线,也不确定用什么方式让奇奇沿着这个路线跑,于是我就这样创建了一个基础的跑步类,让路线和跟随代码分开,并且可以互相组合。
public class TrackRunner
{
public ICurve curve;
public ICurveFollower follower;
public void Awake()
{
curve = GetComponent();
follower = GetComponent();
follower.Init(curve);
}
private void Update()
{
follower.DoFollow();
}
}
public interface ICurve
{
Vector3 GetPoint(float t);
}
public interface ICurveFollower
{
void Init(ICurve curve);
void DoFollow();
}
然后确定使用贝塞尔曲线,但是没找到好用的插件。回到前面适配器的问题,我们现在有多个贝塞尔曲线的实现。另外在跟随路线功能上,有使用Tween的方法跟随曲线的,也有将曲线分为平均N个点连接而成的点的组合的路点模式。
于是我的代码实现如下:
public class TweenCurveFollower : ICurveFollower { }
public class WaypointCurveFollower : ICurveFollower { }
那么我们在真正使用的时候我们根据我们的需求在场景中对应的GameObject中添加对应的ICurve和ICurveFollower对象即可,代码根本不需要改变。
装饰模式
装饰模式就像给你的手机加上各种功能的外壳一样,加上硅胶外壳可以增加手机防摔性能;套上外置镜头的外壳能够大大改进你手机的摄像功能..装饰模式能够给你的已有功能的代码添加其它功能.
这听起来有点像继承,装饰模式和继承的差别在哪里?从我上面手机外壳的例子来说,我们不改变手机本质,我们只在手机的外部做文章;继承则类似在手机内部做工作,比如替换某些零件什么的,当然也可以在手机外部添加功能.
但是装饰模式的实现方式更为灵活,并且万一一个类它是封装类无法被继承,那么只能使用装饰模式了.
public abstract class PhoneBase
{
public abstract void Call(string phoneNumber);
public abstract void TextTo(string phoneNumber, string content);
}
public class PhoneDecorator : PhoneBase
{
public PhoneDecorator(PhoneBase phone)
{
this.phone = phone;
}
private PhoneBase phone;
public override void Call(string phoneNumber)
{
phone.Call(phoneNumber);
}
public override void TextTo(string phoneNumber, string content)
{
phone.TextTo(phoneNumber, content);
}
}
我们创建了一个PhoneDecorator
但是它是继承PhoneBase
类的,和上面的例子有冲突啊,一个手机硅胶后盖怎么可以打电话发短信?那是因为你看漏了一点:在PhoneDecorator
类的构造方法里面需要传递一个PhoneBase
对象进去,这两者组合以后才成为一个完整的装饰构造,也就是说这个完整的装饰构造不是硅胶套,而是套了硅胶套的手机.
现在我们身处CIA黑客办公室,这里摆着几种通过手机来监听当事人的设备,他们分别是:
//电话内容监听器
public class PhoneBug : PhoneDecorator
{
public PhoneDecorator(PhoneBase phone)
:base(phone)
{
bug = new BugDevice();
}
private BugDevice bug;
public override void Call(string phoneNumber)
{
bug.BeginRecord();
base.Call(phoneNumber);
bug.EndRecord();
}
}
//短信转发器
public class PhoneTextRedirector : PhoneDecorator
{
public PhoneTextRedirector(PhoneBase phone)
:base(phone)
{
redirector = new TextRedirector();
}
private TextRedirector redirector;
public override void TextTo(string phoneNumber, string content)
{
base.TextTo(phoneNumber, string content);
redirector("CIA_HQ", "owner: xxx to " + phoneNumber +":\n" + content);
}
}
//地理位置记录器
public class PhoneGPSTracker : PhoneDecorator
{
public PhoneGPSTracker(PhoneBase phone)
:base(phone)
{
gps = new GPSDevice();
}
private GPSDevice gps;
public override void Call(string phoneNumber)
{
base.Call(phoneNumber);
gps.RecordCurrentPosition();
}
public override void TextTo(string phoneNumber, string content)
{
base.TextTo(phoneNumber, string content);
gps.RecordCurrentPosition();
}
}
现在CIA要员进入机场监听重要人员的手机:
public class Person
{
public string Name { get; set; }
public PhoneBase Phone { get; set; }
}
public class Airport
{
public void Entrance(IEnumerable people)
{
foreach(var p in people)
{
var phone = p.Phone;
p.Phone = null;
p.Phone = SecurityCheck(phone);
}
}
private PhoneBase SecurityCheck(PhoneBase phone)
{
var r = Random.value;
if(r<.33f)
return new PhoneBug(phone);
else if( r<.66f)
return new PhoneTextRedirector(phone);
else
return new PhoneGPSTracker(phone);
}
}
注意一下SecurityCheck
方法的返回类型,也是PhoneBase
,也就是说一个人将他的手机交出去以后经过安检以后还给他的还是一部手机,只不过是被动了手脚的手机,然而可以做到当事人完全不知情.
外观模式
外观模式就是负责将一个特别复杂的子系统里的一些功能简化并整合到一个表面类中去.就好像汽车的中控台,能通过一个小小的屏幕知道汽车那些功能是开着的,那些地方有故障等等,甚至可以一键将车里的多个系统调节到一定的程度实现不同的驾驶体验.
这样做的好处是可以将一个复杂的系统通过外观模式封装起来,让使用者不需要深入系统其中也能够很好的使用这个系统.
public class Car
{
public SuspensionSystem suspension;
public DriveSystem drive;
public ElectricSystem electricDevices;
}
public class CarFacade
{
public Car car;
public void SetDriveMode(string driveMode)
{
if(driveMode == "comfort")
{
car.suspension.spring.flexibility = Flexibility.Soft;
car.suspension.tube.flexibility = Flexibility.Soft;
drive.transmission.gearChangingMode = GearChangingMode.Quick;
drive.engine.oilConsumeRate = 0.5f;
electricDevices.setParam("Start-Stop", true);
}
else if(driveMode == "sport")
{
car.suspension.spring.flexibility = Flexibility.Hard;
car.suspension.tube.flexibility = Flexibility.Hard;
drive.transmission.gearChangingMode = GearChangingMode.Slow;
drive.engine.oilConsumeRate = 1.5f;
electricDevices.setParam("Start-Stop", false);
}
else
{
car.suspension.spring.flexibility = Flexibility.Normal;
car.suspension.tube.flexibility = Flexibility.Normal;
drive.transmission.gearChangingMode = GearChangingMode.Normal;
drive.engine.oilConsumeRate = 1f;
electricDevices.setParam("Start-Stop", true);
}
}
}
以上我们通过一个简简单单的SetDriveMode
即可调整整车悬挂系统,传动系统,还有电子系统做到我们想要的驾驶模式.这就是外观模式,非常简单.
享元模式
享元模式的定义是这样的,一个类型实例有一些内容可以被重用,而且对这个类的实例数量非常高的时候我们可以用到这个模式。在我看来就是一个对象池嘛。不了解对象池的同学可以上网搜索一下简单的对象池的代码研究一下,这里就不赘述了.
代理模式
我们程序员对代理这个名字肯定比较熟悉,经常在访问一些"无法访问"的网站的时候会用到它,或者平时我们要去政府办点事,会发现流程麻烦并且复杂,这时候就有"代办"服务,你给他一些钱,他帮你全部搞定.
我们在编码过程中有时候也会遇到这种问题,在做某些事情代价比较大的时候,会使用这种代理模式.
项目雪糕工厂中有一个需求,有些道具是需要使用金币购买的.所以需要购买的物品会有一个价格标签,而且还要判定玩家买不买得起,如果买不起需要显示一个锁的图标告诉玩家.
所以这个需求最终做起来是这样的:我们创建了一个GameObject,里面包含了锁还有价格标签的底图和具体价格的文字.再写了一个这样的脚本:
public class Product : MonoBehaviour
{
public string productName;
public Text priceLabel;
private void Start()
{
var price = StoreSystem.GetPrice(productName);
priceLabel.Text = price.ToString();
}
}
项目中有30个以上的道具.我们要每个道具都加上这个功能的话会导致一个问题,如果未来策划说我要改这个功能的外观表现,那我们就要在这30多个道具预置体上改30多次,这肯定是不允许出现的.所以我们将上面的功能GameObject保存成独立的预置体.然后再写一个代理代码:
public class ProductProxy : MonoBehaviour
{
public string productName;
private void Start()
{
var pf = Resources.Load("ProductPrefab");
var go = Instantiate(pf);
go.getComponent().productName = productName;
go.transform.setParent(transform);
}
}
这样一来我们通过ProduProxy
这个纯脚本的代理生成了具体价格功能的GameObject.这样我们杜绝了在外观修改上的修改风险.
而有些代理模式的结构是这样的:
public class SomeCostlyObject
{
public int GetResult()
{
System.Threading.Thread.Sleep(2);
return new Random().NextInt();
}
}
public class SomeProxy
{
private SomeCostlyObject obj;
public void GetResult(Action onComplete)
{
//开设新线程执行obj.GetResult();
//等待返回值以后执行onComplete回调
}
}
这个代理将一个非常耗时的同步操作改为移步操作,通过这个代理我们就可以节省很多时间了.
Q:有没有发现适配器模式,桥接模式,装饰模式,代理模式他们几个都很像?他们的区别在哪?