设计模式之行为类模式02

备忘录模式

又名快照模式,目的是在不破坏封装性的情况下将一个实例的数据进行捕捉,然后外部化保存起来.在未来的某个时刻进行数据还原.
这里有几个关键词解释一下:

  1. 不破坏封装性: 有些变量是私有变量正常情况下是不允许被访问到的.但是这个变量对于这个类的数据又至关重要,捕捉数据的同时不能更改这个变量的访问限制.
  2. 捕捉: 就是把有用的数据记录下来.
  3. 外部化: 保存到别的地方去,并不保存在自己身上.

用处: 文本编辑器的撤销功能, 浏览器的后退功能, 一些游戏的时间倒退功能.

这个模式需要用到3个类: 备忘类(Memento), 发起者(Originator) 以及 照看者(Caretaker).

我们举个时间倒退的功能的例子:

首先我们创建一个备忘录,记录一个GameObject的Transform数据以备还原:

public class Memento
{
    public GameObject go;
    public float time;
    public Vector3 position;
    public Vector3 rotation;
    public Vector3 scale;
}

然后创建一个可以被备忘的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;
    }
}

最后创建一个备忘录的照看者:

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);
    }
}

使用的时候如下:

public class Controller : MonoBehaviour
{
    private Caretaker ct;
    
    private void Start()
    {
        ct = new CareTaker();
    }
    private void Update()
    {
        var m = GetComponent<Memoable>();
        ct.AddMemento(m.CreateMemo());
    }
}

上面写的是一个非常简单的备忘录模式基础结构,如果我们有一个单位不但包含Transform的基本信息,还包含血量需要记录。
那我们可以基于上面的基础结构进行扩展:

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和实际应用场景都不需要修改。然而正常情况下给一个备忘录模式的功能都可以设计出类似的结构,那么这个备忘录模式比较好的地方在哪呢?

个人认为:在于它不破坏封装性上。上面可以记录血量的例子当中我们可以看出,这个health字段对外是只读不可写的变量。如果我们写一个第三方类来创建和恢复这个类的数据的时候我们就不得不把health字段设置为即可读又可写的字段,如此就破坏了封装性了。

而如果将数据保存在本类当中,那么就缺少了可扩展性,上面我们使用了列表的形式存储这些数据,然而有些情况下我们的需求可能只需要阴阳两种状态互相切换,那么使用列表的形式就有点不太好用了。

观察者模式

观察者模式,我们这样理解:看电影。几十个人在看一块屏幕,并对于屏幕上的显示变化进行反映。

观察者模式就是这种一对多行为的结构。

C#语言中实现观察者模式的一个东西名叫事件。百度以下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);
    }
}

当我们调用Increment或者Decrement的时候,我们通知了所有登记OnValueChanged事件的观察者,我们Value值变化了。这时候所有观察者注册的事件处理函数会被调用。

包括UnityEvent也是属于这种模式,具体的使用方式自行百度,原理也是一样的。

这样的好处:

这样可以让代码变主动为被动。也就是说,我们在判断一个值是否变化的时候,可能是这样的:

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;
        }
    }
}

这样我们在每一个Update的函数当中都在主动的询问mc.Value的值。这样做不是不行,性能消耗也不会多到哪里去,但是代码看起来就稍微丑了点.

观察者模式和中介模式有点类似,他们差别在哪里?观察模式1对多通信, 中介模式是N与N之间隔了一个中介,它们通过中介通信.

状态模式

状态模式代表了一个类型对内对外有可能处于多种状态,在这种状态下每个行为可能产生的结果都是不同的,如果这些状态多而复杂显然使用if else会把自己弄晕的.

比如世良同学在做会说话产品的换装界面,是这样的:

需求是这样的:

  1. 场景切换时有一系列动作
  2. 点击奇奇有动作反馈
  3. 点击小动物有动作反馈

换装界面中总共有5套服装,有些服装中有一些小的功能变化,这样就造成以上的每一个功能可能需要写的代码会很复杂.

最好的办法就是5套服装分开写自己的逻辑代码.

首先我们定义一个抽象的状态基类:

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();
}

然后继承这个基类:

public class HappyBirthdayCostume : MonoCostume
{
    public override void Init()
    {
        //播放生日音乐
        //蛋糕和桌子从一侧划入场景
    }
    
    public override void OnTapKiki()
    {
        //if(点击)
        //    吃蛋糕
        //else if(正在播放吃蛋糕动作)
        //    收起蛋糕
    }
    
    public override void OnTapAnimal()
    {
        //喷射生日烟花
    }
}

public class AngelCostume : MonoCostume
{
    public override void Init()
    {
        //播放小星星音乐
        //播放奇奇从天而降的动作
    }
    
    public override void OnTapKiki()
    {
        //播放奇奇扑腾天使翅膀飞起来的动作
    }
    
    public override void OnTapAnimal()
    {
        //播放奇奇打瞌睡动作
    }
}

最终写一个维持状态的类

public class CostumeChanger : MonoBehaviour
{
    public MonoCostume CurrentCostume { get; private set; }
    public void SetState(MonoCostume costume)
    {
        if(CurrentCostume!=null)
            Destroy(CurrentCostume);
        CurrentCostume = costume;
        CurrentCostume.Init();
        
        //设置奇奇点击监听,设置小动物点击监听
        //当点击奇奇调用CurrentCostume.OnTapKiki();
        //当点击小动物调用CurrentCostume.OnTapAnimal();
    }
}

这样可以将状态的各种处理分在不同的类中执行,避免巨大而又繁杂的单类存在.在不同的时候我们执行CostumeChanger.SetState(yourState)即可更换状态.

策略模式

策略模式和状态模式有点像,都是因为一个行为在不同的情况下有不同的结果,所以我们要把不同的执行代码分成不同的类物理隔离开来减少维护的成本.策略模式和状态模式不同的是状态模式要在一个类中维护这个类的状态,随时切换状态已经状态的查看.而策略模式并不要这么做.

我们使用一个购物的例子好了. 有一个商城有多种付款形式,每种付款形式最终所付的钱都不同这时候我们需要分开不同的类来计算每种付款方式最终需要收多少钱:

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;
    }
}

最终在算钱的时候我们是这样做的:

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("清风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, 支付宝随机减免了5毛钱
        store.Checkout(ebpm, products); // 20~24点之间为20.24, 其他时段为 23
    }
}

上面我们用策略模式将算钱和扣钱的部分分开来写,因为每种支付形式所优惠的程度,还有支付的方法都不同.使用策略模式可以很好的将购买物品最终结算的时候的逻辑区分开来.

模版方法模式

模版方法在平常开发中我们一般会经常用到,比如一个行为它需要N步骤执行,每个步骤都执行一个方法,但是步骤的内容可能会变化。但是总体流程是不变的。

比如一个自动的回合制游戏,在一个角色回合的时候他需要做以下几件事:

  1. 回合开始
  2. 寻找目标
  3. 移动开始
  4. 执行移动
  5. 移动结束
  6. 攻击开始
  7. 执行攻击
  8. 攻击结束

这个流程是完全不会变的。但是每个角色在每个流程之间的具体行为可能有点不同。我们看代码:

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() {}
}

这时候我们有弓箭兵和战士两种职业,弓箭兵在攻击时发射箭矢,而战士并不生成;另外战士在攻击之前有10%的概率会出发奋勇一击技能,使之攻击力临时增加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;
        }
    }
}

我们可以看到不论是士兵还是弓箭手,虽然他们在攻击方式上有些不同,但是在真正执行回合的内容上是完全相同的,这种模式非常朴实单,只使用了一个类的继承,一点都不炫技。这就是模版方法模式。

访问者模式

访问者模式有点小复杂,访问者模式其实一个数据列表中有多种元素,他们各自有点不同.我们要对于这个数据列表当中所有的元素,然而对这些不同的元素要有不同的操作.
比如我们公司里面有很多种类型的员工

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()
    {
        //保证项目的进度和质量
    }
}

然后我们公司有各种考评措施,有年度考核,也有组内考核等等.

public interface IExam
{
    void Examine(Employee employee);
}

public class AnnualExam : IExam
{
    //不同类型的员工有不同的审核标准
    public void Examine(Employee e)
    {
        if(e is Programmer)
        {
            
        }
        else if(e is ArtDesigner)
        {
            
        }
        else if(e is ProjectManager)
        {
            
        }
    }
}

可以看出又是一大堆if,这会非常烦,但是如果员工的类型是可以确定的个数的话,我们可以这样做, 这样我们至少可以把每个员工的考核区分开来:

public interface IExam
{
    void Examine(Programmer p);
    void Examine(ArtDesigner a);
    void Examine(ProjectManager p);
}

public abstract class Employee
{
    //前面的一样就不重复了
    public void AcceptExam(Exam e);
}

public class AnnualExam : IExam
{
    public void Examine(Programmer p)
    {
        //程序员专门的考评
    }
    
    public void Examine(ArtDesginer a)
    {
        //美术专门的考评
    }
    
    public void Examine(ProjectManager p)
    {
        //项目经理专门的考评
    }
}

public class ProgrammerExam : IExam
{
    public void Examine(Programmer p)
    {
        //程序员专门的考评
    }
    
    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);
        }
    }
}

它的优点是对于每个元素的操作是是提供了丰富的拓展性的,但是对于元素的种类是比较限制的.如果某一天又增加了一个新的员工类型,我们的每一个访问者都要新增加一种方法来执行操作.这样对代码的修改来说比较多.

最终总结

其实看遍了我们这些设计模式中有很多是关于如何通过一些形式将单个类的代码复杂性降低,或者多个类型之间的耦合性降低.设计模式最终的目标是将代码的可阅读性和可维护性增高,当然在增高的过程中难免会提高代码结构的复杂度.

这些模式都有他们的经典用法,我们在编码的过程中不一定要完全遵循这些模式的规范来做,我们只要做到上面所说的目标,是否使用了传说中的经典模式一点都不重要.

当然学习了这些模式的使用方式能够让我们在未来的编码过程中多注重一下代码的设计,让我们的代码不但对自己友好,也要对其他人友好.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容