備忘錄模式
又名快照模式,目的是在不破壞封裝性的情況下將一個(gè)實(shí)例的數(shù)據(jù)進(jìn)行捕捉,然后外部化保存起來.在未來的某個(gè)時(shí)刻進(jìn)行數(shù)據(jù)還原.
這里有幾個(gè)關(guān)鍵詞解釋一下:
- 不破壞封裝性: 有些變量是私有變量正常情況下是不允許被訪問到的.但是這個(gè)變量對(duì)于這個(gè)類的數(shù)據(jù)又至關(guān)重要,捕捉數(shù)據(jù)的同時(shí)不能更改這個(gè)變量的訪問限制.
- 捕捉: 就是把有用的數(shù)據(jù)記錄下來.
- 外部化: 保存到別的地方去,并不保存在自己身上.
用處: 文本編輯器的撤銷功能, 瀏覽器的后退功能, 一些游戲的時(shí)間倒退功能.
這個(gè)模式需要用到3個(gè)類: 備忘類(Memento), 發(fā)起者(Originator) 以及 照看者(Caretaker).
我們舉個(gè)時(shí)間倒退的功能的例子:
首先我們創(chuàng)建一個(gè)備忘錄,記錄一個(gè)GameObject的Transform數(shù)據(jù)以備還原:
public class Memento
{
public GameObject go;
public float time;
public Vector3 position;
public Vector3 rotation;
public Vector3 scale;
}
然后創(chuàng)建一個(gè)可以被備忘的MonoBehaviour
public class Memoable : MonoBehaviour
{
public virtual Memento CreateMemo()
{
var m = new Memento()
{
go = gameObject,
time = Time.realTimeSinceStartup,
position = transform.position,
rotation = transform.rotation,
scale = transform.localScale
};
return m;
}
public virtual void RestoreFromMemo(Memento m)
{
if(m.go != gameObject)
return;
transform.position = m.position;
transform.rotation = m.roation;
transform.localScale = m.scale;
}
}
最后創(chuàng)建一個(gè)備忘錄的照看者:
public class Caretaker
{
private List<Memento> mementos = new List<Memento>();
public void AddMemento(Memento m)
{
mementos.Add(m);
}
public IEnumerable<Memento> GetMementos(GameObject g)
{
return mementos.Where(m=>m.go == g);
}
}
使用的時(shí)候如下:
public class Controller : MonoBehaviour
{
private Caretaker ct;
private void Start()
{
ct = new CareTaker();
}
private void Update()
{
var m = GetComponent<Memoable>();
ct.AddMemento(m.CreateMemo());
}
}
上面寫的是一個(gè)非常簡(jiǎn)單的備忘錄模式基礎(chǔ)結(jié)構(gòu)俗或,如果我們有一個(gè)單位不但包含Transform的基本信息,還包含血量需要記錄。
那我們可以基于上面的基礎(chǔ)結(jié)構(gòu)進(jìn)行擴(kuò)展:
public class MementoHealth : Memento
{
public int health;
}
public class MemoableHealth : MemoableHealth
{
private int health;
public int Health { get{ return health; } }
public override Memento CreateMemo()
{
var m = new MementoHealth()
{
go = gameObject,
time = Time.realTimeSinceStartup,
position = transform.position,
rotation = transform.rotation,
scale = transform.localScale,
htealth = health
};
}
public override void RestoreFromMemo(Memento m)
{
var mh = (MementoHealth)h;
if(mh==null||m.go != gameObject)
return;
transform.position = m.position;
transform.rotation = m.roation;
transform.localScale = m.localScale;
health = m.health;
}
}
Caretaker和實(shí)際應(yīng)用場(chǎng)景都不需要修改锥忿。然而正常情況下給一個(gè)備忘錄模式的功能都可以設(shè)計(jì)出類似的結(jié)構(gòu)考余,那么這個(gè)備忘錄模式比較好的地方在哪呢辅肾?
個(gè)人認(rèn)為:在于它不破壞封裝性上雏掠。上面可以記錄血量的例子當(dāng)中我們可以看出术健,這個(gè)health字段對(duì)外是只讀不可寫的變量络断。如果我們寫一個(gè)第三方類來創(chuàng)建和恢復(fù)這個(gè)類的數(shù)據(jù)的時(shí)候我們就不得不把health字段設(shè)置為即可讀又可寫的字段裁替,如此就破壞了封裝性了。
而如果將數(shù)據(jù)保存在本類當(dāng)中貌笨,那么就缺少了可擴(kuò)展性弱判,上面我們使用了列表的形式存儲(chǔ)這些數(shù)據(jù),然而有些情況下我們的需求可能只需要陰陽兩種狀態(tài)互相切換锥惋,那么使用列表的形式就有點(diǎn)不太好用了昌腰。
觀察者模式
觀察者模式开伏,我們這樣理解:看電影。幾十個(gè)人在看一塊屏幕剥哑,并對(duì)于屏幕上的顯示變化進(jìn)行反映硅则。
觀察者模式就是這種一對(duì)多行為的結(jié)構(gòu)。
C#語言中實(shí)現(xiàn)觀察者模式的一個(gè)東西名叫事件株婴。百度以下C#事件的使用方式我們可以看到以下的代碼:
public class MyClass
{
public event EventHandler OnValueChanged;
public int Value { get; private set; }
public void Increment()
{
Value++;
if(OnValueChanged!=null)
OnValueChanged();
}
public void Decrement()
{
Value--;
if(OnValueChanged!=null)
OnValueChanged();
}
}
public class MyTest : MonoBehaviour
{
private void Start()
{
var mc = new MyClass();
mc.OnValueChanged += OnValueChanged;
mc.Increment();
mc.Decrement();
mc.Increment();
mc.Increment();
mc.Increment();
mc.Decrement();
}
private void OnValueChanged(Object sender, EventArgs e)
{
Debug.Log("Value Changed: "+ ((MyClass)sender).Value);
}
}
當(dāng)我們調(diào)用Increment或者Decrement的時(shí)候怎虫,我們通知了所有登記OnValueChanged事件的觀察者,我們Value值變化了困介。這時(shí)候所有觀察者注冊(cè)的事件處理函數(shù)會(huì)被調(diào)用大审。
包括UnityEvent也是屬于這種模式,具體的使用方式自行百度座哩,原理也是一樣的徒扶。
這樣的好處:
這樣可以讓代碼變主動(dòng)為被動(dòng)。也就是說根穷,我們?cè)谂袛嘁粋€(gè)值是否變化的時(shí)候姜骡,可能是這樣的:
public class MyTest
{
private MyClass mc;
private float oldVal;
private void Start()
{
mc = new MyClass();
oldVal = mc.Value;
}
private void Update()
{
if(mc.Value != oldValue)
{
Debug.Log("Value Changed: " + mc.Value);
oldValue = mc.Value;
}
}
}
這樣我們?cè)诿恳粋€(gè)Update的函數(shù)當(dāng)中都在主動(dòng)的詢問mc.Value的值。這樣做不是不行,性能消耗也不會(huì)多到哪里去,但是代碼看起來就稍微丑了點(diǎn).
觀察者模式和中介模式有點(diǎn)類似,他們差別在哪里?觀察模式1對(duì)多通信, 中介模式是N與N之間隔了一個(gè)中介,它們通過中介通信.
狀態(tài)模式
狀態(tài)模式代表了一個(gè)類型對(duì)內(nèi)對(duì)外有可能處于多種狀態(tài),在這種狀態(tài)下每個(gè)行為可能產(chǎn)生的結(jié)果都是不同的,如果這些狀態(tài)多而復(fù)雜顯然使用if else會(huì)把自己弄暈的.
比如世良同學(xué)在做會(huì)說話產(chǎn)品的換裝界面,是這樣的:
需求是這樣的:
- 場(chǎng)景切換時(shí)有一系列動(dòng)作
- 點(diǎn)擊奇奇有動(dòng)作反饋
- 點(diǎn)擊小動(dòng)物有動(dòng)作反饋
換裝界面中總共有5套服裝,有些服裝中有一些小的功能變化,這樣就造成以上的每一個(gè)功能可能需要寫的代碼會(huì)很復(fù)雜.
最好的辦法就是5套服裝分開寫自己的邏輯代碼.
首先我們定義一個(gè)抽象的狀態(tài)基類:
public abstract class MonoCostume : MonoBehaviour
{
public GameObject kiki;
public GameObject animal;
public GameObject Kiki { get { return kiki; } }
public GameObject Animal { get { return animal;} }
public abstract void Init();
public abstract void OnTapKiki();
public abstract void OnTapAnimal();
}
然后繼承這個(gè)基類:
public class HappyBirthdayCostume : MonoCostume
{
public override void Init()
{
//播放生日音樂
//蛋糕和桌子從一側(cè)劃入場(chǎng)景
}
public override void OnTapKiki()
{
//if(點(diǎn)擊)
// 吃蛋糕
//else if(正在播放吃蛋糕動(dòng)作)
// 收起蛋糕
}
public override void OnTapAnimal()
{
//噴射生日煙花
}
}
public class AngelCostume : MonoCostume
{
public override void Init()
{
//播放小星星音樂
//播放奇奇從天而降的動(dòng)作
}
public override void OnTapKiki()
{
//播放奇奇撲騰天使翅膀飛起來的動(dòng)作
}
public override void OnTapAnimal()
{
//播放奇奇打瞌睡動(dòng)作
}
}
最終寫一個(gè)維持狀態(tài)的類
public class CostumeChanger : MonoBehaviour
{
public MonoCostume CurrentCostume { get; private set; }
public void SetState(MonoCostume costume)
{
if(CurrentCostume!=null)
Destroy(CurrentCostume);
CurrentCostume = costume;
CurrentCostume.Init();
//設(shè)置奇奇點(diǎn)擊監(jiān)聽,設(shè)置小動(dòng)物點(diǎn)擊監(jiān)聽
//當(dāng)點(diǎn)擊奇奇調(diào)用CurrentCostume.OnTapKiki();
//當(dāng)點(diǎn)擊小動(dòng)物調(diào)用CurrentCostume.OnTapAnimal();
}
}
這樣可以將狀態(tài)的各種處理分在不同的類中執(zhí)行,避免巨大而又繁雜的單類存在.在不同的時(shí)候我們執(zhí)行CostumeChanger.SetState(yourState)
即可更換狀態(tài).
策略模式
策略模式和狀態(tài)模式有點(diǎn)像,都是因?yàn)橐粋€(gè)行為在不同的情況下有不同的結(jié)果,所以我們要把不同的執(zhí)行代碼分成不同的類物理隔離開來減少維護(hù)的成本.策略模式和狀態(tài)模式不同的是狀態(tài)模式要在一個(gè)類中維護(hù)這個(gè)類的狀態(tài),隨時(shí)切換狀態(tài)已經(jīng)狀態(tài)的查看.而策略模式并不要這么做.
我們使用一個(gè)購物的例子好了. 有一個(gè)商城有多種付款形式,每種付款形式最終所付的錢都不同這時(shí)候我們需要分開不同的類來計(jì)算每種付款方式最終需要收多少錢:
public interface IPayMethod
{
float GetFinalPrice(float originPrice);
void Charge(float money);
}
public class CreditCardPayMethod : IPayMethod
{
public CreditCardPayMethod(string cardNumber)
{
this.cardNumber = cardNumber;
}
private string cardNumber;
public float GetFinalPrice(float originPrice)
{
return originPrice * 1.01f;
}
public void Charge(float money)
{
credCardSvc.Charge(this.cardNumber, money);
}
}
public class AliPayPayMethod : IPayMethod
{
public AliPayPayMethod(string userName, string password)
{
Login(userName, password);
}
public float GetFinalPrice(float originPrice)
{
var discount = AlipaySerice.GetDiscount("XX商城", originPrice);
return originPrice - discount;
}
public void Charge(float money)
{
balance -= money;
}
}
public class ExampleBankPayMethod : IPayMethod
{
public float GetFinalPrice(float originPrice)
{
var time = DateTime.Now.Hour;
if(time>=20 && time<=23)
return originPrice * .88f;
return originPrice;
}
public void Charge(float money)
{
balance -= money;
}
}
最終在算錢的時(shí)候我們是這樣做的:
public class OnlineStore
{
public void Checkout(IPayMethod payMethod, IEnumerable<IProduct> products)
{
var price = products.Sum(p=>p.Price);
price = payMethod.GetFinalPrice(originPrice);
payMethod.Charge(price);
}
}
public class Program
{
public void Main()
{
var store = new OnlineStore();
var products = new List<IProduct>();
products.Add(new Apple("紅富士", 1.5));//5
products.Add(new PaperRoll("清風(fēng)a", 1));//12
products.Add(new AABattery("南孚", 2));//6
ccpm = new CreditCardPayMethod("6222xxxxxxxxxxxx");
alipm = new CreditCardPayMethod("Jack Ma", "Alibaba");
ebpm = new ExampleBankPayMethod("9999xxxxxxxxxxxx");
store.Checkout(ccpm, products); // 23.23
store.Checkout(alipm, products);// 22.5, 支付寶隨機(jī)減免了5毛錢
store.Checkout(ebpm, products); // 20~24點(diǎn)之間為20.24, 其他時(shí)段為 23
}
}
上面我們用策略模式將算錢和扣錢的部分分開來寫,因?yàn)槊糠N支付形式所優(yōu)惠的程度,還有支付的方法都不同.使用策略模式可以很好的將購買物品最終結(jié)算的時(shí)候的邏輯區(qū)分開來.
模版方法模式
模版方法在平常開發(fā)中我們一般會(huì)經(jīng)常用到屿良,比如一個(gè)行為它需要N步驟執(zhí)行圈澈,每個(gè)步驟都執(zhí)行一個(gè)方法,但是步驟的內(nèi)容可能會(huì)變化尘惧。但是總體流程是不變的康栈。
比如一個(gè)自動(dòng)的回合制游戲,在一個(gè)角色回合的時(shí)候他需要做以下幾件事:
- 回合開始
- 尋找目標(biāo)
- 移動(dòng)開始
- 執(zhí)行移動(dòng)
- 移動(dòng)結(jié)束
- 攻擊開始
- 執(zhí)行攻擊
- 攻擊結(jié)束
這個(gè)流程是完全不會(huì)變的喷橙。但是每個(gè)角色在每個(gè)流程之間的具體行為可能有點(diǎn)不同啥么。我們看代碼:
public class Character
{
public void MakeAction()
{
RoundStart();//1
target = FindTarget();//2
if(target != null)
{
if(Distance(target)>attackRange)
{
bool doMove = BeforeMove();//3
if(doMove)
{
MoveToTarget();//4
AfterMove();//5
}
}
if(Distance(target)<=attackRange)
{
bool doAttack = BeforeAttack();//6
if(doAttack)
{
AttackTarget();//7
AfterAttack();//8
}
}
}
}
protected virtual void Roundstart() {}
protected virtual GameObject FindTarget() {}
protected virtual bool BeforeMove() {}
protected virtual void MoveToTarget() {}
protected virtual void AfterMove() {}
protected virtual bool BeforeAttack() {}
protected virtual void AttackTarget() {}
protected virtual void AfterAttack() {}
}
這時(shí)候我們有弓箭兵和戰(zhàn)士?jī)煞N職業(yè),弓箭兵在攻擊時(shí)發(fā)射箭矢贰逾,而戰(zhàn)士并不生成悬荣;另外戰(zhàn)士在攻擊之前有10%的概率會(huì)出發(fā)奮勇一擊技能,使之攻擊力臨時(shí)增加50
public class Archer : Character
{
preotected override AttackTarget()
{
var arrow = Instantiate<GameObject>(arrowPf);
var p = arrow.GetComponent<Projectile>();
p.shooter = this;
p.target = target;
}
}
public class Soldier : Character
{
private bool buffed;
protected override BeforeAttack()
{
var r = Random.value <= .1f;
if(r)
{
attackPoint +=50;
buffed = true;
}
}
protected override AfterAttack()
{
if(buffed)
{
attackPoint-=50;
buffed = false;
}
}
}
我們可以看到不論是士兵還是弓箭手疙剑,雖然他們?cè)诠舴绞缴嫌行┎煌扔兀窃谡嬲龍?zhí)行回合的內(nèi)容上是完全相同的,這種模式非常樸實(shí)單核芽,只使用了一個(gè)類的繼承囚戚,一點(diǎn)都不炫技。這就是模版方法模式轧简。
訪問者模式
訪問者模式有點(diǎn)小復(fù)雜,訪問者模式其實(shí)一個(gè)數(shù)據(jù)列表中有多種元素,他們各自有點(diǎn)不同.我們要對(duì)于這個(gè)數(shù)據(jù)列表當(dāng)中所有的元素,然而對(duì)這些不同的元素要有不同的操作.
比如我們公司里面有很多種類型的員工
public abstract class Employee
{
public string Name { get; set; }
public string Gender { get; set; }
public int Age { get; set; }
public abstract void Work();
}
public class Programmer : Employee
{
public void Work()
{
//寫代碼
}
}
public class ArtDesigner : Employee
{
public void Work()
{
//畫畫
}
}
public class ProjectManager : Employee
{
public void Work()
{
//保證項(xiàng)目的進(jìn)度和質(zhì)量
}
}
然后我們公司有各種考評(píng)措施,有年度考核,也有組內(nèi)考核等等.
public interface IExam
{
void Examine(Employee employee);
}
public class AnnualExam : IExam
{
//不同類型的員工有不同的審核標(biāo)準(zhǔn)
public void Examine(Employee e)
{
if(e is Programmer)
{
}
else if(e is ArtDesigner)
{
}
else if(e is ProjectManager)
{
}
}
}
可以看出又是一大堆if,這會(huì)非常煩,但是如果員工的類型是可以確定的個(gè)數(shù)的話,我們可以這樣做, 這樣我們至少可以把每個(gè)員工的考核區(qū)分開來:
public interface IExam
{
void Examine(Programmer p);
void Examine(ArtDesigner a);
void Examine(ProjectManager p);
}
public abstract class Employee
{
//前面的一樣就不重復(fù)了
public void AcceptExam(Exam e);
}
public class AnnualExam : IExam
{
public void Examine(Programmer p)
{
//程序員專門的考評(píng)
}
public void Examine(ArtDesginer a)
{
//美術(shù)專門的考評(píng)
}
public void Examine(ProjectManager p)
{
//項(xiàng)目經(jīng)理專門的考評(píng)
}
}
public class ProgrammerExam : IExam
{
public void Examine(Programmer p)
{
//程序員專門的考評(píng)
}
public void Examine(ArtDesginer a)
{
//不管
}
public void Examine(ProjectManager p)
{
//不管
}
}
最終的使用代碼是這樣的:
public class Program
{
public void Main()
{
var com = new Company();
com.Employees.Add(new Programmer("張三"));
com.Employees.Add(new ArtDesigner("李四"));
com.Employees.Add(new ProjectManager("王五"));
var exam1 = new AnnualExam();
var exam2 = new ProgrammerExam();
foreach(var e in com.Employee)
{
e.Accept(exam1);
e.Accept(exam2);
}
}
}
它的優(yōu)點(diǎn)是對(duì)于每個(gè)元素的操作是是提供了豐富的拓展性的,但是對(duì)于元素的種類是比較限制的.如果某一天又增加了一個(gè)新的員工類型,我們的每一個(gè)訪問者都要新增加一種方法來執(zhí)行操作.這樣對(duì)代碼的修改來說比較多.
最終總結(jié)
其實(shí)看遍了我們這些設(shè)計(jì)模式中有很多是關(guān)于如何通過一些形式將單個(gè)類的代碼復(fù)雜性降低,或者多個(gè)類型之間的耦合性降低.設(shè)計(jì)模式最終的目標(biāo)是將代碼的可閱讀性和可維護(hù)性增高,當(dāng)然在增高的過程中難免會(huì)提高代碼結(jié)構(gòu)的復(fù)雜度.
這些模式都有他們的經(jīng)典用法,我們?cè)诰幋a的過程中不一定要完全遵循這些模式的規(guī)范來做,我們只要做到上面所說的目標(biāo),是否使用了傳說中的經(jīng)典模式一點(diǎn)都不重要.
當(dāng)然學(xué)習(xí)了這些模式的使用方式能夠讓我們?cè)谖磥淼木幋a過程中多注重一下代碼的設(shè)計(jì),讓我們的代碼不但對(duì)自己友好,也要對(duì)其他人友好.