C# 设计模式:行为型

这篇博客我们写行为型模式,应该算是最后一篇设计模式的博客了吧,相较于上一篇结构型有的模式写的比较水(组合模式、适配器模式),这篇博客大部分例子都是自己原创的(然后这篇博客的用时就超标了),算是自己的一个“救赎”吧,不过迭代器模式我没写,因为我觉得之前用了三篇博客来写迭代器以及迭代器实现的集合以及很足够了。 嗯,总是是请各位看官多多批评指正。

C# 设计模式:行为型_第1张图片


 

模板方法模式:定义一个操作中的算法骨架(抽象函数),将算法的具体实现延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的特定步骤。

模板方法比较简单(这也是为什么我把这个模式放在第一个),我们可以说模板方法即为抽象类或者接口的基本用法。即在父类中定义函数的结构(函数名,函数的形参,函数的返回值),然后将它们里面的逻辑的具体实现延迟到子类。

我们之前印象比较深刻的对算法逻辑进行拆分时在结构型的装饰模式(将功能的核心区和非核心区拆分)中,但这里表现的更简单,这里只有重写,而没有对原函数进行细微的修改的需求(原函数只是一个骨架)。由于这个模式比较简单,这里只给出标准的格式。

那么原函数的定义骨架就是这样的:

    abstract class AbstractClass
    {
        public abstract void PrimitiveOperation1();
        public abstract void PrimitiveOperation2();

        public void TemplateMethod()
        {
            PrimitiveOperation1();
            PrimitiveOperation2();
            Console.WriteLine(" ");
        }
    }

继承这个抽象类的类对两个方法有各自的实现:

    class ConcreteClassA : AbstractClass
    {
        public override void PrimitiveOperation1()
        {
            Console.WriteLine("A类的方法1实现");
        }

        public override void PrimitiveOperation2()
        {
            Console.WriteLine("A类的方法2实现");
        }
    }

    class ConcreteClassB : AbstractClass
    {
        public override void PrimitiveOperation1()
        {
            Console.WriteLine("B类的方法1实现");
        }

        public override void PrimitiveOperation2()
        {
            Console.WriteLine("B类的方法2实现");
        }
    }

那么,调用时只需要利用里式替换原则创建抽象类的引用指向实现抽象的子类的对象就行了:

    class Program
    {
        static void Main()
        {
            AbstractClass instance = new ConcreteClassA();
            instance.TemplateMethod();
        }
    }

那么就可以按骨架来各自实现不同的功能:

C# 设计模式:行为型_第2张图片


策略模式:定义策略家族,并分别封装。并且让它们可以相互替换。

从这句话我们可以看到,策略模式的定义的语句里即表明了它需要的内容:

  • 策略家族:即代码中对算法的抽象定义,它定义了这类算法的“模板”是怎么样的。
  • 封装:对算法的封装与可替换性做出定义。

我们代码中常常会遇到相同的目的需求但是需要不同的实现的情况,例如生活中超市的促销活动那样,这个“超市促销活动”就是一种“策略家族”,我们需要对策略家族进行不同的实现来达成策略的目的,而不同的促销形式就是实现这个“算法家族”的算法。我们来尝试一下很简单的策略模式的实现,模拟一下一个游戏的盈利策略,这非常好实现,我们游戏的“盈利”就是一种策略家族,那么实现盈利的策略可以有:打折、限时免费、道具抽奖等。而我们游戏营销并不是一潭死水,可以互相切换不同的策略行为。策略模式往往有以下三个模块:

策略家族:即定义抽象的策略:

    abstract class SellStartegy
    {
        public abstract void SellInterface();
    }

策略:策略家族的具体实现:

    class StartegyInstanceA : SellStartegy
    {
        public override void SellInterface()
        {
            Console.WriteLine("打折");
        }
    }

    class StartegyInstanceB : SellStartegy
    {
        public override void SellInterface()
        {
            Console.WriteLine("限时免费");
        }
    }

    class StartegyInstanceC : SellStartegy
    {
        public override void SellInterface()
        {
            Console.WriteLine("道具抽奖");
        }
    }

策略的封装:实现策略的互相切换:

    class StartegyContext
    {
        SellStartegy startegy;

        public void SetStartegy(SellStartegy startegy)
        {
            this.startegy = startegy;
        }

        public void ContextInterface()
        {
            startegy.SellInterface();
        }
    }

那么,我们在进行游戏促销的时候就可以进行简便的切换了:

    class Program
    {
        static void Main()
        {
            StartegyContext context = new StartegyContext();
            context.SetStartegy(new StartegyInstanceA());
            context.ContextInterface();

            context.SetStartegy(new StartegyInstanceB());
            context.ContextInterface();

            context.SetStartegy(new StartegyInstanceC());
            context.ContextInterface();
        }
    }

上面实现的策略模式非常简单,所谓的策略封装仅仅是保存一个策略的对象,这里可以与工厂模式相结合起来,实现在不同情况下面执行不同的策略。

策略模式的特点在于:定义一种抽象的公共函数,并且通过封装这个函数的调用来实现策略的切换。换句话说,策略模式封装了变化在策略模式实际的应用中,它可以来封装任何不同类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,策略模式就可以派上用场。

在基本的策略模式中(也就是我们上文的这个例子),选择所用具体实现的职责由客户端对象来承担,并且将引用传递给Context对象。所以这也是为什么需要让它与创建型的工厂模式结合的原因。

同时,策略模式也简化了单元测试,每个算法都有自己的类,这样可以通过接口单独测试。


观察者模式:定义一对多的依赖关系,让多个观察者对象监听某一个主题对象,并在主题对象发生变化时通知观察者

观察者模式又叫订阅-发布模式,熟悉事件的人对这个东西肯定不陌生。

观察者模式的需求来源于“通知”,通知在生活中很常见,但是代码中的通知则有更多的需求,如果单纯将某一个函数调用后在里面即刻调用另一个函数,我们也可以说通知这个功能实现了,但是这样的通知基于紧耦合,对代码的可维护性非常不利,而观察者的通知的特点在于:主题对象并不知道观察者,观察者之间也互相不知道,这样就实现了二者之间的解耦。

事件中的角色可以分为五类,在观察者模式中,则划分为四类:

  • 抽象主题对象:并提供增加和删除观察者的接口。
  • 实体主题对象:内部维护观察这个主题对象的引用,并且在内部状态发生改变时,给观察者发出通知。
  • 抽象观察者:观察者的抽象,为观察者定义标准接口以便于主题对象通知。
  • 具体观察者:实现抽象观察者。

我们之前写事件的时候那篇博客,讲述了我打游戏的时候家里爸妈来敲门的事情,我们做个类比,假设我在学校登录某个网络游戏平台上玩游戏,学校机房的网管或者老师都能看到我在线玩啥,一旦他们上线了,网络平台让我立马下线,那么这个例子中,老师和网管就是主题对象,而各种网络平台就是观察者

我们实现主题对象类,里面维护观察者的集合,并实现对观察者的添加,当内部发生变化时(例如老师上线),遍历调用每个观察者集合中的特定函数:

    abstract class Watcher
    {
        protected string WatcherName
        {
            get; set;
        }
        public abstract void Add(Player player);
        public abstract void OnLine();
    }
    class Teacher : Watcher
    {
        private List Players = new List();

        public Teacher()
        {
            WatcherName = "老师";
        }
        public override void Add(Player player)
        {
            Players.Add(player);
        }

        public override void OnLine()
        {
            Console.WriteLine("老师发现你们了!");
            foreach (Player player in Players)
            {
                player.Offline();
            }
        }
    }

    class SchoolMonitor : Watcher
    {
        private List Players = new List();

        public SchoolMonitor()
        {
            WatcherName = "学校主机";
        }
        public override void Add(Player player)
        {
            Players.Add(player);
        }

        public override void OnLine()
        {
            Console.WriteLine("学校主机发现你们了!");
            foreach (Player player in Players)
            {
                player.Offline();
            }
        }
    }

抽象观察者以及具体观察者:相较于要发出通知的主题对象类,观察者类只需要定义收到通知的操作就好了(注意,观察者可以不知道它要观察谁),在我们的例子中,观察者会让玩家下机:

    abstract class Player
    {
        protected string PlayerName;
        public Player(string name)
        {
            PlayerName = name;
        }

        public abstract void Offline();
    }

    class Steam : Player
    {
        public Steam(string name) : base(name)
        { }

        public override void Offline()
        {
            Console.WriteLine("Steam平台:正在玩游戏的" + PlayerName + ",有人正在监视,请下线!");
        }
    }
    class UPlay : Player
    {
        public UPlay(string name) : base(name)
        { }

        public override void Offline()
        {
            Console.WriteLine("UPlay平台:正在玩游戏的" + PlayerName + ",有人正在监视,请下线!");
        }
    }

 那么我们可以这样来订阅这个“老师上线”的事件:

    class Program
    {
        static void Main()
        {
            Player playerA = new Steam("AAA");
            Watcher watcher = new Teacher();

            Player playerB = new UPlay("BBB");
            watcher.Add(playerA);
            watcher.Add(playerB);

            //老师上机了!
            watcher.OnLine();
        }
    }

输出:

C# 设计模式:行为型_第3张图片

这样就实现了观察者模式,当然在现在大部分高级语言里都不需要这样去实现观察者了,C++有函数指针,C#和Java有委托,但也不是手动定义的观察者模式完全被取代了,他们之间也有许多区别: 

虽然观察者模式的通知相较于事件中的通知来说比较原始,相较于C#中的事件,基本的观察者模式不存在对某一个委托的形式进行匹配的硬性要求,因此我们可以对事件与事件处理器进行不同的参数设置。但是委托的耦合层级相较于观察者模式发生了变化,观察者模式中的耦合层级仍然是对象,但是委托以及事件的耦合层级则是函数。我认为这样的耦合层级的变化催生出了匿名函数和Lambda表达式。

观察者模式的最大的特点在于解耦,让主题对象与观察者都依赖于抽象,而不依赖于具体的对象。


状态模式:当对象的内在状态改变时允许改变其行为

状态模式实际上是对if-else和Switch这种分支语法的一种补正,在创建型的设计模式中,简单工厂的例子中用到了这种语法,但工厂模式为了迎合开闭原则对这种语法进行了修正。与之相对应的,在行为型设计模式中,通过状态模式进行了补正。状态模式将状态的切换转移到了具体状态类中,这样它们之间互相的依赖就少了很多了,

状态模式控制一个对象状态转换的条件表达式过于复杂的情况,将状态的判断逻辑转移到表示不同状态的一系列类中,这样就简化了判断逻辑。它的内部有如下结构:

  • 状态封装类Context:内部封装一个具体状态类对象,通过它类定义当前应该是哪个对象。
  • 抽象状态类State:定义一个接口,标记了用于状态行为的方法。
  • 具体状态类ConcreteState:每个子类实现状态行为的切换,并向状态封装类Context指明下一个具体状态。

我们测试一个例子,模拟一下我一天的学习状态,我们从早上开始,不断的申请查看自己的学习状态,跟上面一样,状态模式有如下结构:

状态封装类:封装了当前的学习状态,然后在学习代码(StudyING函数)中调用具体状态类的切换方法(ShowStudy):

    class Study
    {
        public StudyState state;
        public void SetState(StudyState state)
        {
            this.state = state;
        }

        public void StudyING()
        {
            if (state == null)
            {
                throw new NullReferenceException();
            }
            state.ShowState(this);
        }
    }

抽象状态类:定义了需要让状态封装类调用的状态切换函数(ShowStudy) :

    abstract class StudyState
    {
        public abstract void ShowState(Study toStudy);
    }

具体状态类:我们这里模拟了人一天的时刻,当我们持续的查看状态时,状态会按一天的时刻将状态封装类中的对象切换到下一个具体状态类:

    class Morning : StudyState
    {
        public override void ShowState(Study toStudy)
        {
            Console.WriteLine("早上要好好整代码");
            toStudy.SetState(new Noon());
        }
    }
    class Noon : StudyState
    {
        public override void ShowState(Study toStudy)
        {
            Console.WriteLine("中午了,先恰饭!恰完饭再整");
            toStudy.SetState(new Afternoon());
        }
    }
    class Afternoon : StudyState
    {
        public override void ShowState(Study toStudy)
        {
            Console.WriteLine("下午冲冲冲!");
            toStudy.SetState(new Evening());
        }
    }
    class Evening:StudyState
    {
        public override void ShowState(Study toStudy)
        {
            Console.WriteLine("恰点面包接着写!");
            toStudy.SetState(new Night());
        }
    }
    class Night : StudyState
    {
        public override void ShowState(Study toStudy)
        {
            Console.WriteLine("该休息了。。。。");
        }
    }

然后我们在主函数中,只需要定义初始状态,然后调用StudyING函数,状态自己就切换过去了: 

    class Program
    {
        static void Main()
        {
            Study TodayStudyState = new Study();
            TodayStudyState.SetState(new Morning());

            Console.WriteLine("问:早上的学习状态怎么样?");
            TodayStudyState.StudyING();

            Console.WriteLine("问:正午的学习状态怎么样?");
            TodayStudyState.StudyING();

            Console.WriteLine("问:下午的学习状态怎么样?");
            TodayStudyState.StudyING();

            Console.WriteLine("问:傍晚的学习状态怎么样?");
            TodayStudyState.StudyING();

            Console.WriteLine("问:夜晚的学习状态怎么样?");
            TodayStudyState.StudyING();
        }
    }

我们这样就可以通过状态模式非常简单的查看状态了:

 C# 设计模式:行为型_第4张图片

状态模式常常应用一下的情况:

当对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。


备忘录模式:在不破坏对象封装性的前提下捕获对象的内部状态,并在对象之外保存这种状态。

在《大话设计模式》中,用了一个游戏死亡前保存的功能来描述备忘录模式,备忘录模式,实际上就是将对象数据在外部保存,当然,实现这样的功能本身不难,但是备忘录模式也有独特的需求:

  • 备忘录发起人(Originator):需要保存的对象本身,负责创建和备忘录Memento,它内部可以写逻辑来决定需要往备忘录或者从备忘录获得什么信息。
  • 备忘录(memento):负责存储发起人Originator的内部状态,它的对象引用保存在备忘录管理者Caretaker中。
  • 备忘录管理者(Caretaker):管理备忘录的类,它封装备忘录的引用。(注意,备忘录发起人是不知道备忘录的引用的,只有管理者才保有备忘录的引用)

备忘录模式适用于功能复杂的但需要维护属性或者历史的对象的情况。它的主要需求就是:将对象复原,并且将内部的其他信息对外部屏蔽起来。

在《大话设计模式》中的例子已经很棒了,我们这里直接沿用他的例子,为了保存引用类型,我们改用序列化来实现对象的保存:

备忘录发起人(需要保存的对象):它的内部只需要实现创建一个备忘录给备忘录管理者和获得备忘录的数据,其他的函数主要是测试用途,并且,我们单独实现了一个引用类型的参数Pet,也就是宠物:

    [Serializable]
    class Pet
    {
        public int PetHP;
        public string petName;
        public Pet(string petName)
        {
            this.petName = petName;
        }
    }
    [Serializable]
    class GameRole
    {
        private int vit;
        private int atk;
        private int def;
        private Pet pet;
        public GameRole(string PetName)
        {
            pet = new Pet(PetName);
        }

        public RoleStateMemento SaveState()
        {
            return new RoleStateMemento(this);
        }
        public void RecoveryState(RoleStateMemento memento)
        {
            vit = memento.Role.vit;
            atk = memento.Role.atk;
            def = memento.Role.def;
            pet = memento.Role.pet;
        }
        public void StateDisplay()
        {
            Console.WriteLine("角色当前状态为:");
            Console.WriteLine("体力:" + vit);
            Console.WriteLine("攻击力:" + atk);
            Console.WriteLine("防御力:" + def);
            Console.WriteLine("宠物" + pet.petName + "血量:" + pet.PetHP);
            Console.WriteLine(" ");
        }

        public void GetInitState()
        {
            vit = 100;
            atk = 100;
            def = 100;
            pet.PetHP = 100;
        }
        public void Fight()
        {
            vit = 0;
            atk = 0;
            def = 0;
            pet.PetHP = 0;
        }
    }

备忘录Memento:它内部只需要保存一个备忘录发起人对象,为了实现数据的完全迁移,在备忘录发起人的属性里面使用了序列化: 

    class RoleStateMemento
    {
        private GameRole role;
        public GameRole Role
        {
            get
            {
                return role;
            }
            set
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    BinaryFormatter binary = new BinaryFormatter();
                    binary.Serialize(stream, value);
                    stream.Position = 0;
                    role = binary.Deserialize(stream) as GameRole;
                }
            }
        }
        public RoleStateMemento(GameRole role)
        {
            Role = role;
        }
    }

备忘录的管理者Caretaker:它内部非常简单,它内部仅仅封装住备忘录里Memento的引用就好了:

    class RoleStateCaretaker
    {
        private RoleStateMemento memento;
        public RoleStateMemento Memento
        {
            get
            {
                return memento;
            }
            set
            {
                memento = value;
            }
        }
    }

我们在主函数里可以这样调用它:

    class Program
    {
        static void Main()
        {
            GameRole Dragon = new GameRole("冰霜巨龙");
            Dragon.GetInitState();
            Dragon.StateDisplay();

            RoleStateCaretaker stateAdmin = new RoleStateCaretaker();
            stateAdmin.Memento = Dragon.SaveState();
            
            Console.WriteLine("发生了战斗!");
            Dragon.Fight();
            Dragon.StateDisplay();

            Console.WriteLine("玩家读档了!");
            Dragon.RecoveryState(stateAdmin.Memento);
            Dragon.StateDisplay();
        }
    }

这样就可以模拟游戏中玩家被打然后读档了:

 C# 设计模式:行为型_第5张图片

以上就是备忘录模式的简单用法,但是实际上来说,它常用于软件中的命令撤销功能,命令模式可以使用备忘录模式来实现对软件状态的存储。进而执行撤销。


命令模式:将请求封装为一个对象,从而用不同的请求对客户进行参数化,进而对请求排队或记录请求日志,以及请求的撤销

命令模式应该是这篇博客最重点的一个模式了,它通过将命令进行参数化,然后储存起来,这样就可以对命令进行排序、记录、撤销了。

命令的参数化听起来很高级,但实际上是将命令看成完成一个个功能类的对象,然后通过集合将这些对象进行存储,这杨就实现了命令的参数化。并且,命令模式把请求一个操作的对象与执行操作的对象分离,这也是命令模式最重要的一点。

命令模式可以归纳成以下几个部分:

  • 命令的处理类:执行一些操作(功能)的类。
  • 命令的抽象类:里面封装一个命令的处理类对象以及调用命令的方法名。
  • 命令的功能具体类:每一个命令的具体类对应命令处理类的一个操作(函数)
  • 命令的发出者:内部维护一个命令抽象类的集合。

我们写一个例子,假设现在一个用户登录Steam,并且需要执行例如开箱子,开始游戏,购买游戏等操作,把这个例子改写为命令模式可以知道以下的模块:

  • Steam:命令的处理类。
  • Steam操作:命令的抽象类。
  • 开箱子、开游戏、买游戏:命令的具体功能类。
  • 用户:命令的发出者。

我们将这个例子转化为代码,也是如下几个模块:

命令的处理类Steam:注意,它内部是不需要知道谁向他发出命令的,它只需要执行它应该执行的命令就行了:

    class Steam
    {
        public int BoxCount;
        public string UserID;
        public Steam(string UserID)
        {
            this.UserID = UserID;
            BoxCount = 0;
        }
        public void GameStart()
        {
            Console.WriteLine("当前用户" + UserID + "执行:打开游戏!");
        }
        public void BuyGames()
        {
            Console.WriteLine("当前用户" + UserID + "执行:购买游戏");
        }
        public void OpenBox()
        {
            BoxCount++;
            Console.WriteLine("当前用户" + UserID + "执行:开箱子,您目前已经开了" + BoxCount + "个箱子了,花费" + (BoxCount * 16) + "元");
        }
    }

命令的抽象类UserCommand,表明了命令是向Steam的对象发出的,所以它需要知道受到命令的对象是谁。并标明统一的传达命令的函数:

    abstract class UserCommand
    {
        protected Steam steamInstance;
        public UserCommand(Steam steam)
        {
            this.steamInstance = steam;
        }

        public abstract void SendCommandToSteam();
    }

命令的具体类:传达命令的函数调用了的命令处理类Steam的方法,包含了开箱子开游戏等等,这样就实现了命令的传递:

    class OpenTheGame: UserCommand
    {
        public OpenTheGame(Steam steam):base(steam)
        { }

        public override void SendCommandToSteam()
        {
            steamInstance.GameStart();
        }
    }
    class BuySomeGames: UserCommand
    {
        public BuySomeGames(Steam steam):base(steam)
        { }

        public override void SendCommandToSteam()
        {
            steamInstance.BuyGames();
        }
    }
    class OpenBox: UserCommand
    {
        public OpenBox(Steam steam):base(steam)
        { }

        public override void SendCommandToSteam()
        {
            steamInstance.OpenBox();
        }
    }

命令的发出者:里面维护一个命令抽象类的数组,并且可以增删命令对象:

    class User
    {
        public List commands = new List();
        public void AddCommand(UserCommand command)
        {
            commands.Add(command);
        }
        public void CancelCommand(UserCommand command)
        {
            commands.Remove(command);
        }
        public void DoCommand()
        {
            foreach (UserCommand command in commands)
            {
                command.SendCommandToSteam();
            }
        }
    }

那么在调用的时候,命令模式就可以让不同的命令发出得心应手并且互不干扰了:

    class Program
    {
        static void Main()
        {
            Steam steamInstance = new Steam("MeiGaZaMu");
            User user = new User();

            user.AddCommand(new BuySomeGames(steamInstance));
            user.AddCommand(new OpenBox(steamInstance));
            user.AddCommand(new OpenBox(steamInstance));
            user.AddCommand(new OpenBox(steamInstance));

            user.DoCommand();
        }
    }

输出:

C# 设计模式:行为型_第6张图片

这样就实现了命令的参数化,并且支持开闭原则,我们可以清晰的看到命令的发出,并且:命令的处理者并不知道命令的抽象类是谁,命令的发出者也并不知道命令的处理者是谁。


职责链模式:使多个对象都有机会处理请求,从而避免了请求发送者和接收者的耦合关系。将对象连成一条链,并沿着链传递该请求,直到有对象处理它为止

职责链模式在生活中特别常见,我记得以前听牛群冯巩的相声《小偷公司》中结尾包袱就说,小偷公司要转移基地,然后将请求报告向每个部门层层上报,每一层都画了一个圈,最后到了总经理那里,圈画成了奥运会的标志,总经理还以为要去奥运会偷东西。

我们把这个包袱剖析一下,里面实际上就是一个职责链模式,多个对象(不同层级的部门)都有机会处理请求(请求公司搬家),对象连成了一个链条(层层上报),直到有对象(总经理)处理请求为止(同意到奥运会去偷)。

职责链模式,就有如下的结构:

  • 抽象职责链类:定义了一个职责链中的对象的下一个对象的位置,如果当前对象不能处理一个事务,就将事务转移到职责链中的下一个对象中。
  • 具体职责链类:具体处理请求的对象。

职责链模式中需要将事务划分成不同的层级,这样不同层级的对象也方便它们处理,我们模拟一下学校里的各级领导处理学生事务的功能:

为了模拟层级,我们定义两个枚举,一个是事务的枚举,一个是领导层级的枚举;:

    public enum SchoolLevel
    {
        //班主任
        HeadTeacher = 1,
        //院长
        President = 2,
        //校长
        HeadMaster = 3
    }
    public enum SchoolEvent
    {
        //请假
        AskForLeave=1,
        //请求换班
        AskForChange=2,
        //请求毕业
        AskForGraduate=3
    }

抽象职责链类:负责包装具体职责链中的下一个对象的引用:

    abstract class SchoolEventHandler
    {
        protected SchoolEventHandler successor;
        public void SetSuccessor(SchoolEventHandler successor)
        {
            this.successor = successor;
        }
        public abstract void HandleRequest(SchoolEvent request);
    }

具体职责链类:我们实现三个具体职责链类,它们内部都有根据层级对应的枚举字段,当事务传入时,通过一些逻辑自行判断是否能够处理(枚举是否相等),如果不能处理就转移到下一个对象上去:

    class HeadTeacher : SchoolEventHandler
    {
        private SchoolLevel level = SchoolLevel.HeadTeacher;
        public override void HandleRequest(SchoolEvent request)
        {
            if ((int)request == (int)level)
            {
                Console.WriteLine("班主任:我处理了你的请求!");
            }
            else if (successor != null)
            {
                Console.WriteLine("班主任:我处理不了你的请求~~");
                successor.HandleRequest(request);
            }
        }
    }
    class President : SchoolEventHandler
    {
        private SchoolLevel level = SchoolLevel.President;
        public override void HandleRequest(SchoolEvent request)
        {
            if ((int)request == (int)level)
            {
                Console.WriteLine("院长:我处理了你的请求!");
            }
            else if (successor != null)
            {
                Console.WriteLine("院长:我处理不了你的请求~~");
                successor.HandleRequest(request);
            }
        }
    }
    class HeadMaster : SchoolEventHandler
    {
        private SchoolLevel level = SchoolLevel.HeadMaster;
        public override void HandleRequest(SchoolEvent request)
        {
            if ((int)request == (int)level)
            {
                Console.WriteLine("校长:我处理了你的请求!");
            }
            else if (successor != null)
            {
                Console.WriteLine("校长:我处理不了你的请求~~");
                successor.HandleRequest(request);
            }
        }
    }

 这样就实现了职责链,但是在主函数中,我们仍然需要自己定义职责链的层级,然后就可以放心的将事务传给它们处理:

    class Program
    {
        static void Main()
        {
            SchoolEventHandler HeadTeacher = new HeadTeacher();
            SchoolEventHandler President = new President();
            SchoolEventHandler HeadMaster = new HeadMaster();
            //手动定义职责链的下一个职责
            HeadTeacher.SetSuccessor(President);
            President.SetSuccessor(HeadMaster);

            Console.WriteLine("同学:我想换班!");
            HeadTeacher.HandleRequest(SchoolEvent.AskForChange);

            Console.WriteLine("同学:我想毕业!");
            HeadTeacher.HandleRequest(SchoolEvent.AskForGraduate);
        }
    }

这样我们就能看到和小偷公司里面一样的剧情了:

C# 设计模式:行为型_第7张图片


 

中介者模式:用一个中介对象来封装一系列的对象交互,使各对象不需要显示的相互引用,进而使耦合松散

在我们代码开发中,将系统分割成很多对象可以增加其复用性(单一职责原则),但是这样也造成了代码中互相连接的需求,反而降低了可用性,所以为了既实现两个类不必通信就可以不互相引用的迪米特原则,又需要将每个类连接起来,设计模式中存在中介者模式来完成这一点。

中介者模式很像生活中的物流集散中心,很多人带着货物来到集散中心然后互相交流把货物卖出去,这个集散中心就是一个中介者,但是并不是说每个商家不能互相存名片来交流(就像存对象的引用一样),但是互相存名片来交流很容易弄混,这样货物的收发就复杂起来了(代码的耦合就高起来了),而且集散中心能更好的货比三家。

我们写一个支付宝的例子,对于我们平时互相转账,可以使用现金来互相转账,但是通过支付APP,这样的操作变得方便很多,两个人不需要面对面就能互相传递现金:

中介者模式相较于其他的模式较为简单,这里只需要实现抽象的中介者类、用户类(用于它们子类的里式替换),具体中介者类,具体用户类就好,带入上面的例子,抽象中介者就是支付APP,具体中介者就是支付宝,用户就是抽象的用户类,我们这些一个个用户就是具体用户类

抽象的中介者:只需要定义中介者需要的方法就行,我们定义两个,转账和用户确认:

    abstract class TransferAPP
    {
        public abstract void TransferMoney(int money, User person);
        public abstract void TransferUserComfirm(User A, User B);
    }

抽象用户:抽象用户里包含了用户具体哪种支付软件,以及用户名(我们可以脑补成账户):

    abstract class User
    {
        public string UserName;
        protected TransferAPP app;
        public User(string UserName, TransferAPP app)
        {
            this.UserName = UserName;
            this.app = app;
        }
        public abstract void SendMoney(int Money, User user);
        public abstract void GetMoney(int Money, User user);
    }

具体中介者(支付宝):中介者需要知道是谁转账给谁,所以需要两个用户的引用,这里我们实现的比较简单,仅仅只是A转账就转给B,B转账就转给A:

    class ALIpay : TransferAPP
    {
        private User userA;
        private User userB;
        public override void TransferUserComfirm(User A,User B)
        {
            userA = A;
            userB = B;
        }
        
        public override void TransferMoney(int money, User person)
        {
            if (person == userA)
            {
                userB.GetMoney(money, userA);
            }
            else if (person == userB)
            {
                userA.GetMoney(money, userB);
            }
        }
    }

具体的用户:这里我们定义两个类用户,一个A一个B,每个用户都只有一个实例 (其实这里只用一种用户也行,实现一下一个类的两个对象互相传信息),由于用户通过支付宝转账是用户自己主动的行为,而支付宝只是提供转账的功能,所以我们可以判定是用户中的转账函数调用中介者中的转账功能才能完成转账:

    class UserA:User
    {
        public UserA(string UserName, TransferAPP app) : base(UserName,app)
        { }

        public override void GetMoney(int Money,User user)
        {
            Console.WriteLine("用户A:到账来自" + user.UserName + "的" + Money + "元");
        }

        public override void SendMoney(int Money, User user)
        {
            app.TransferMoney(Money, this);
        }
    }
    class UserB : User
    {
        public UserB(string UserName, TransferAPP app) : base(UserName,app)
        { }

        public override void GetMoney(int Money, User user)
        {
            Console.WriteLine("用户B:到账来自" + user.UserName + "的" + Money + "元");
        }

        public override void SendMoney(int Money, User user)
        {
            app.TransferMoney(Money, this);
        }
    }

然后在主函数中就有如下调用,设置支付宝类中的两个账户,然后进行转账:

    class Program
    {
        static void Main()
        {
            TransferAPP app = new ALIpay();

            User userA = new UserA("老A", app);
            User userB = new UserB("老B", app);

            app.TransferUserComfirm(userA, userB);

            Console.WriteLine("从A账户向B账户转账:");
            userA.SendMoney(100, userB);
        }
    }

 C# 设计模式:行为型_第8张图片

这样就实现了中介者模式的使用,我们可以看到中介者模式的牛逼之处,就是可以将很多类的交互都放入中介者类中控制,正如《大话设计模式》里面说的那样,中介者模式可以让整个代码更为宏观,在既不违背单一职责原则和迪米特原则的情况下让类与类之间轻松的交互。

但是中介者模式缺点也是很明显的:所有的耦合都挤在了中介者类里面,这意味着中介者类很可能成了一个代码深坑,一旦它的职责过多,那么某一天不得不修改它的时候也可能会引发灾难。


解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

解释器模式常常用于特定类型的问题,这种需求在使用中很常见,例如判断Email或者匹配电话号码,语气为每个需求都写一个算法函数,不如使用通用的搜索算法来解释执行一个正则表达式,而解释器模式则是为正则表达式定义一种文法,并将使用这种文法来解决问题。

当有一个语言需要解释执行,并且可将这种语言中的句子表示为一个抽象语法时,就可以使用解释器模式。

我写了一个下午的解释器例子,但是发现效果很差(还不如脑子里第一时间想出来的傻瓜方法),思来想去还是不要滥用设计模式为好,所以这里暂时仅仅给出解释器模式的标准格式:

解释器的标准格式包含三个模块:抽象解释器,终端解释器,非终端解释器,并且,在客户端使用解释器的集合来对一个特殊的语句进行处理。

抽象解释器,定义了解释器处理语句的通用函数:

    abstract class AbstractExpression
    {
        public abstract void Interpret(Context context);
    }

终端解释器与非终端解释器,定义了解释器中对不同语法的具体操控: 

    class TerminalExpression : AbstractExpression
    {
        public override void Interpret(Context context)
        {
            Console.WriteLine("终端解释器");
        }
    }

    class NonterminalExpression : AbstractExpression
    {
        public override void Interpret(Context context)
        {
            Console.WriteLine("非终端解释器");
        }
    }

语句信息:包装我们需要处理的语句的输入与输出:

    class Context
    {
        public string Input;
        public string Output;
    }

 我们在主函数中,需要使用集合来使用不同的解释器对象来对一个特定的语法(在例子中是Context对象)进行操控:

    class Program
    {
        static void Main()
        {
            Context context = new Context();
            List expressions = new List();
            expressions.Add(new TerminalExpression());
            expressions.Add(new NonterminalExpression());
            expressions.Add(new TerminalExpression());
            expressions.Add(new TerminalExpression());

            foreach (AbstractExpression expression in expressions)
            {
                expression.Interpret(context);
            }
        }
    }

 解释器为每一个文法的规则都至少定义了一个类,所以包含许多规则的文法很可能难以维护,所以它并不适用于太过复杂的问法。如果文法非常复杂时,使用其他的技术例如语法分析程序或者编译生成器反而更合适。


 访问者模式:如果表示一个作用于某对象结构中的各元素的操作的对象称为访问者,那么访问者模式可以在不改变原色的类的前提下定义作用于这些元素的新操作

我在看书的时候,说访问者模式是最复杂的(虽然我觉得解释器最复杂),但是访问者模式的结构其实很好懂,访问者嘛,说白了就是访问与被访问的关系,在我们的代码中,我们一个个操控数据的类就是访问者,那么被操控的数据背后的数据结构就是被访问者。访问者有如下的几个组成部分:

  • 抽象的数据基类:内部封装一个向访问者传递对象的函数
  • 数据子类:实现向访问者传递数据的功能
  • 抽象的访问者基类:定义对不同数据子类的操控函数
  • 访问者子类:实现对不同数据的操控函数
  • 数据的操控类:内部封装数据类的集合以便于访问者统一访问元素

访问者模式的代码结构,第一眼和桥接模式长得差不多,都是两个抽象各自实现的样子。但是桥接模式中标记自己的抽象行为靠的是保存抽象行为的对象来调用功能,但是访问者模式中的两边的抽象都互不保存引用,通过数据中特定的类调用访问者中特定的函数来实现数据的访问。同时,又利用数据存在抽象基类的特点,使用数据类型的操控类来实现对访问者的需求统一操控。

最近临近双十一,我也已经很久很久没有打彩虹六号了,所以在这里意淫一下自己在彩虹六号里面买东西的感觉,带入我们的访问者模式里,以上的几个模块就可以变成以下的样子:

  • 彩六的商城接口:抽象的数据基类
  • 彩六商城的各种物品(挂饰、皮肤、阿尔法包):数据子类
  • 彩六玩家:抽象的访问者基类
  • 我,一个普通的彩六玩家:访问者子类
  • 彩六物品结算窗口(假设彩六买东西是统一结算的):数据的操控类

那么我们可以写出如下代码:

抽象的数据基类:我们在应用中加入了物品订单这个内容,但除了订单外就只标定了一个函数:

    abstract class RainbowSix_Store
    {
        public int GoodsOrder;
        public abstract void Buy(R6sPlayer player);

        public RainbowSix_Store(int order)
        {
            GoodsOrder = order;
        }
    }

抽象的访问者基类:负责定义彩六玩家的访问行为:

    abstract class R6sPlayer
    {
        public abstract void BuyWeapon_Skin(Weapon_Skin skin);
        public abstract void BuyCharacter_Skin(Character_Skin skin);
        public abstract void BuyAlphaPack(AlphaPack skin);
        public abstract void BuyPendant(Pendant skin);
    }

访问者子类:我们先只定义一个普通玩家类,那么买到了专门的物品就会显示物品的订单信息:

    class NormalPlayer : R6sPlayer
    {
        public override void BuyAlphaPack(AlphaPack skin)
        {
            Console.WriteLine("您已购买了一件阿尔法包,订单号" + skin.GoodsOrder);
        }

        public override void BuyCharacter_Skin(Character_Skin skin)
        {
            Console.WriteLine("您已购买一件角色皮肤,订单号" + skin.GoodsOrder);
        }

        public override void BuyPendant(Pendant skin)
        {
            Console.WriteLine("您已购买一件挂饰,订单号" + skin.GoodsOrder);
        }

        public override void BuyWeapon_Skin(Weapon_Skin skin)
        {
            Console.WriteLine("您已购买一件武器皮肤,订单号" + skin.GoodsOrder);
        }
    }

数据子类:包括了我们上面说的买阿尔法包啊等等等功能,由于是我们访问这些数据,所以由这些数据将自己的对象实例主动发送给我们访问者类: 

    class Weapon_Skin : RainbowSix_Store
    {
        public Weapon_Skin(int a) : base(a)
        { }

        public override void Buy(R6sPlayer player)
        {
            player.BuyWeapon_Skin(this);
        }
    }
    class Character_Skin:RainbowSix_Store
    {
        public Character_Skin(int a) : base(a)
        { }

        public override void Buy(R6sPlayer player)
        {
            player.BuyCharacter_Skin(this);
        }
    }
    class AlphaPack:RainbowSix_Store
    {
        public AlphaPack(int a) : base(a)
        { }

        public override void Buy(R6sPlayer player)
        {
            player.BuyAlphaPack(this);
        }
    }

    class Pendant:RainbowSix_Store
    {
        public Pendant(int a) : base(a)
        { }

        public override void Buy(R6sPlayer player)
        {
            player.BuyPendant(this);
        }
    }

数据的处理类,负责统一的处理数据,里面包装一个数据类的集合

    class StoreController
    {
        private List storeItems = new List();

        public void Add(RainbowSix_Store item)
        {
            storeItems.Add(item);
        }
        public void Remove(RainbowSix_Store item)
        {
            storeItems.Remove(item);
        }
        public void Accept(R6sPlayer player)
        {
            foreach (RainbowSix_Store item in storeItems)
            {
                item.Buy(player);
            }
        }
    }

然后我们在主函数中简单的定义一个玩家对象,然后使用商店操控对象购买东西就好了:

    class Program
    {
        static void Main()
        {
            R6sPlayer player = new NormalPlayer();

            StoreController controller = new StoreController();
            controller.Add(new Weapon_Skin(111));
            controller.Add(new Character_Skin(222));
            controller.Add(new Pendant(123));
            controller.Add(new AlphaPack(444));

            controller.Accept(player);
        }
    }

 C# 设计模式:行为型_第9张图片

访问者模式的目的在于,将数据结构与它的处理分离,使得二者的耦合分开,所以操作类可以相对自由的演化或者更改操作需求。所以,我们的访问者模式添加一个新的操作算法非常容易,迎合了开闭原则。例如彩虹六号中有季票玩家,买东西打八折,我们这里就可以这样来修改“算法”,让季票玩家享受八折优惠。

只需要添加一个新的访问者就好了:

    class SeasonPassPlayer : R6sPlayer
    {
        public override void BuyAlphaPack(AlphaPack skin)
        {
            Console.WriteLine("VIP用户:您已购买了一件阿尔法包,订单号" + skin.GoodsOrder);
        }

        public override void BuyCharacter_Skin(Character_Skin skin)
        {
            Console.WriteLine("VIP用户:您已购买一件角色皮肤,订单号" + skin.GoodsOrder);
        }

        public override void BuyPendant(Pendant skin)
        {
            Console.WriteLine("VIP用户:您已购买一件挂饰,订单号" + skin.GoodsOrder);
        }

        public override void BuyWeapon_Skin(Weapon_Skin skin)
        {
            Console.WriteLine("VIP用户:您已购买一件武器皮肤,订单号" + skin.GoodsOrder);
        }
    }

那么我们在主函数的调用中,只需要改变访问者的类型就行了,其他一切不用变:

    class Program
    {
        static void Main()
        {
            R6sPlayer player = new SeasonPassPlayer();

            StoreController controller = new StoreController();
            controller.Add(new Weapon_Skin(111));
            controller.Add(new Character_Skin(222));
            controller.Add(new Pendant(123));
            controller.Add(new AlphaPack(444));

            controller.Accept(player);
        }
    }

 C# 设计模式:行为型_第10张图片

访问者模式适用于数据结构较为稳定,但是对数据的处理的需求较多的情况。这也说明,访问者模式适用于数据结构不变,数据处理多变的情况(你不能要求一个设计模式是万能的)

为了对具体的数据进行处理。我们可以看到在访问者基类中,对每个数据的具体访问都定义了专门的方法。但是,这样也造成了如果数据结构一旦发生变化,那么访问者类的变化将是巨大的。所以这也是访问者模式的致命痛点。也顺便告诉我们在编码的时候一定要整理好需求

 

你可能感兴趣的:(C#基础)