烤羊肉串引来的思考——命令模式

烤羊肉串引来的思考——命令模式

继上一篇“手机软件何时统一——桥接模式”后,本文继续讲解《大话设计模式》第23章“烤羊肉串引来的思考——命令模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文由于历史久远,故事讲述可能比较过时,但设计模式的精髓不变。

本文正式开始


1 吃烤羊肉串!

时间:6月23日17点 地点:小区门口 人物:小菜、大鸟

“小菜,肚子饿了,走,我请你吃羊肉串。”

“好呀,小区门口那新疆人烤的就很不错。”

小菜和大鸟来到了小区门口。

“啊,这么多人,都围了十几个。”小菜感叹道。

“现在读大学,进公司,做白领,其实未必有人家烤羊肉串的挣得多。”

“这是两回事,人家也很辛苦呀。”

此时,老板烤的第一批羊肉好了。

“老板,我这有两串。”

“老板,我的是三串不辣的。”

“老板,你怎么给她了,我先付的钱!”

“老板,这串不太熟呀,再烤烤。”

“老板,我老早就等在这里,钱早给你了,你都不给我,我不要了。退钱!”

旁边等着拿肉串的人七嘴八舌地叫开了。场面有些混乱,由于人实在太多,烤羊肉串的老板已经分不清谁是谁,造成分发错误,收钱错误,烤肉质量不过关等等。

“小菜,我看我们还是换一家吧,这里实在太混乱了,过去不远有一家烤肉店是有店面的。”

“嗯,他这样子生意是做不好。咱们去那一家吧。”

时间:6月2日18点 地点:烤肉店 人物:小菜、大鸟、服务员

小菜和大鸟走到了那家烤肉店。

“服务员,我们要十串羊肉串、两串鸡翅、两瓶啤酒。”大鸟根本没有看菜单。

“鸡翅没有了,您点别的烧烤吧。”服务员答道。

“那就来四串牛板筋,烤肉要辣的。”大鸟很轻车熟路。

“大鸟常来这里吃吗?很熟悉嘛!”小菜问道。

“太熟悉了,这年头,单身在外混,哪有不熟悉家门口附近的吃饭的地儿。不然每天晚上的肚皮问题怎么解决?”

“你说,在外面打游击烤羊肉串和这种开门店做烤肉,哪个更赚钱?”小菜问道。

“哈,这很难讲,毕竟各有各的好,在外面打游击,好处是不用租房,不用上税,最多就是交点‘保护费’,但下雨天不行、大白天不行、太晚也不行,一般都是傍晚做几个钟头,顾客也不固定,像刚才那个,由于人多造成混乱,于是就放跑了我们这两条大鱼,其实他的生意是不稳定的。”

“大白天不行?太晚不行?”

“大白天,城管没下班呢,怎能容忍他如此安逸。超过晚上11点,夜深人静,谁还愿意站在路边吃烤肉。但开门店就不一样了,不管什么时间都可以做生意,由于环境相对好,所以固定客户就多,看似好像房租交出去了,但其实由于顾客多,而且是正经做生意,所以最终可以赚到大钱。

“大鸟研究得很透嘛。”

“其实这门店好过马路游击队,还可以对应一个很重要的设计模式呢!”

“哦,此话怎讲?”

2 烧烤摊vs.烧烤店

“你再回忆刚才在我们小区门口烤肉摊看到的情景。”

“因为要吃烤肉的人太多,都希望能最快吃到肉串,烤肉老板一个人,所以有些混乱“

“还不止这些,老板一个人,来的人一多,他就未必记得住谁交没交过钱,要几串,需不需要放辣等等。”

“是呀,大家都站在那里,没什么事,于是都盯着烤肉去了,哪一串多、哪一串少、哪一串烤得好、哪一串烤得焦看得清清楚楚,于是挑剔也就接踵而至。”

“这其实就是我们在编程中常说的什么?”

“我想想,你是想说‘紧耦合’?”

“哈,不错,不枉我的精心栽培。”

“由于客户和烤羊肉串老板的‘紧耦合’所以使得容易出错,容易混乱,也容易挑剔。“

“说得对,**这其实就是‘行为请求者’与‘行为实现者’的紧耦合。**我们需要记录哪个人要几串羊肉串,有没有特殊要求(放辣不放辣),付没付过钱,谁先谁后,这其实都相当于对请求做什么?”

“对请求做记录,啊,应该是做日志。”

“很好,那么如果有人需要退回请求,或者要求烤肉重烤,这其实就是?”

“就相当于撤销和重做吧。”

“OK,所以对请求排队或记录请求日志,以及支持可撤销的操作等行为时,‘行为请求者’与‘行为实现者’的紧耦合是不太适合的。你说怎么办?”

“开家门店。”

“哈,这是最终结果,不是这个意思,我们是烤肉请求者,烤肉的师傅是烤肉的实现者,对于开门店来说,我们用得着去看着烤肉的实现过程吗?现实是怎么做的呢?”

“哦,我明白你的意思了,我们不用去认识烤肉者是谁,连他的面都不用见到,我们只需要给接待我们的服务员说我们要什么就可以了。他可以记录我们的请求,然后再由他去通知烤肉师傅做。”

“而且,由于我们所做的请求,其实也就是我们点肉的订单,上面有很详细的我们的要求,所有的客户都有这一份订单,烤肉师傅可以按先后顺序操作,不会混乱,也不会遗忘了。”

“收钱的时候,也不会多收或少收。”

“优点还不止这里,比如说,”大鸟突然大声叫道,“服务员,我们那十串羊肉串太多了,改成六串就可以了。”

“好的!”服务员答道。

大鸟接着说:“你注意看他接着做了什么?”

“他好像在一个小本子上划了一下,然后去通知烤肉师傅了。”

“对呀,这其实是在做撤销行为的操作。由于有了记录,所以最终算账还是不会错的。”

“对对对,这种利用一个服务员来解耦客户和烤肉师傅的处理好处真的很多。”

“好了,这里有纸和笔,你把刚才的想法写成代码吧?”

“啊,在这?”

“这才叫让编程融入生活。来吧,不写出来,你是不能完全理解的。”

“好吧,我试试看。”

3 紧耦合设计

边吃着烤肉串,边写着代码,小菜完成了第一版。

代码结构图

烤羊肉串引来的思考——命令模式

路边烤羊肉串的实现

//烤肉串者
public class Barbecuer
{
  //烤羊肉
  public void BakeMutton()
  {
    Console.WriteLine("烤羊肉串!");
  }
  
  //烤鸡翅
  public void BakeChickenwing()
  {
    Console.WriteLine("烤鸡翅!");
  }
}

客户端调用

static void Main (string[] args)
{
  // 客户端程序与‘烤肉串者’紧耦合,尽管简单,但却极为僵化,有许许多多的隐患
  Barbecuer boy = new Barbecuer();
  boy.BakeMutton();
  boy.BakeMutton();  
  boy.BakeMutton();  
  boy.BakeChickenwing();
  boy.BakeMutton();
  boy.BakeMutton();
  boy.BakeChickenwing();
  
  Console.Read();
}

“很好,这就是路边烤肉的对应,如果用户多了,请求多了,就容易乱了。那你再尝试用门店的方式来实现它。”

“我知道一定需要增加服务员类,但怎么做有些不明白。”

“嗯,这里的确是难点,要知道,不管是烤羊肉串,还是烤鸡翅,还是其他烧烤,这些都是‘烤肉串者类’的行为,也就是他的方法,具体怎么做都是由方法内部来实现,我们不用去管它但是对于‘服务员’类来说,他其实就是根据用户的需要,发个命令,说:‘有人要十个羊肉串,有人要两个鸡翅’,这些都是命令……”

“我明白了,你的意思是,把‘烤肉串者’类当中的方法,分别写成多个命令类,那么它们就可以被‘服务员’来请求了?”

“是的,说得没错,这些命令其实差不多都是同一个样式,于是你就可以泛化出一个抽象类,让‘服务员’只管对抽象的‘命令’发号施令就可以了。具体是什么命令,即是烤什么,由客户来决定吧。”

“我大概明白了。”

4 松耦合设计

接着,小菜经过思考,把第二个版本的代码写了出来。

代码结构图

烤羊肉串引来的思考——命令模式

抽象命令类

//抽象命令
public abstract class Command
{
  //抽象命令类,只需要确定‘烤肉串者’是谁
  protected Barbecuer receiver;

  public Command (Barbecuer receiver)
  {
    this.receiver = receiver;
  }
  
  //执行命令
  abstract public void ExcuteCommand();
}

具体命令类

// 烤羊肉串命令
class BakeMuttonCommand : Command
{
  // 具体命令类,执行命令时,执行具体的行为
  public BakeMuttonCommand(Barbecuer receiver)
  : base (receiver)
  { }
  
  public override void ExcuteCommand()
  {
    receiver.BakeMutton();
  }
}

// 烤鸡翅命令
class BakeChickenWingCommand : Command
{
  public BakeChickenWingCommand(Barbecuer receiver)
  : base (receiver)
  { }
  
  public override void ExcuteCommand()
  {
    receiver.BakeChickenwing();
  }
}

服务员类

//服务员
public class Waiter
{
  private Command command;
  
  // 服务员类,不用管用户想要什么烤肉,反正都是‘命令’,只管记录订单,然后通知‘烤肉串者’执行即可
  // 设置订单  
  public void SetOrder(Command command)
  {
    this.command = command;
  }
  
  // 通知执行
  public void Notify()
  {
    command.ExcuteCommand();
  }
}

烤肉串者类与之前相同,略。

客户端实现

static void Main(string[] args)
{ 
  // 开店前的准备  
  Barbecuer boy = new Barbecuer() ;
  
  // 烧烤店事先就找好了烤肉厨师、服务员和烤肉菜单,就等客户上门
  Command bakeMuttonCommandl = new BakeMuttonCommand(boy);
  Command bakeMuttonCommand2 = new BakeMuttonCommand(boy);
  Command bakeChickenWingCommand1 = new BakeChickenWingCommand(boy);
  Waiter girl = new Waiter();
  
  // 开门营业
  // 服务员根据用户要求,通知厨房开始制作
  girl.SetOrder(bakeMuttonCommandl);
  girl.Notify();
  girl.SetOrder(bakeMuttonCommand2);  
  girl.Notify();  
  girl.SetOrder(bakeChickenWingCommand1);
  girl.Notify();
  
  Console.Read ();
}

“大鸟,我这样写如何?”

“很好很好,基本都把代码实现了。但有几个问题,第一,真实的情况其实并不是用户点一个菜,服务员就通知厨房去做一个,那样不科学,应该是点完烧烤后,服务员一次通知制作:第二,如果此时鸡翅没了,不应该是客户来判断是否还有,客户哪知道有没有呀,应该是服务员或烤肉串者来否决这个请求:第三,客户到底点了哪些烧烤或饮料,这是需要记录日志的,以备收费,也包括后期的统计:第四,客户完全有可能因为点的肉串太多而考虑取消一些还没有制作的肉串。这些问题都需要得到解决。”

“你说的这些好像现在都不难办到了。你看着……”

5 松耦合后

小菜开始了第三版的代码编写。

服务员类

// 服务员
public class Waiter
{
  // 增加存放具体命令的容器
  private IList<Command> orders = new List<Command>();
  
  // 设置订单
  public void SetOrder(Command command)
  {
    // 在客户提出请求时,对没货的烧烤进行回绝
    if (command.ToString().Equals(typeof(BakeChickenWingCommand).FullName))
    {
      Console.WriteLine("服务员:鸡翅没有了,请点别的烧烤。") ;
    }
    else
    {
      orders.Add(command);
      
      // 记录客户所点的烧烤的日志,以备算账收钱
      Console.WriteLine("增加订单:" + command.ToString() + "时间:" +DateTime.Now.ToString());
    }
  }
  
  // 取消订单
  public void CancelOrder(Command command)
  {
    orders.Remove(command);
    Console.WriteLine("取消订单:" + command.ToString() + "时间:"+DateTime.Now.ToString());
  }
  
  // 通知全部执行
  public void Notify()
  {
    // 根据用户点好的烧烤订单通知厨房制作
    foreach (Command cmd in orders)
    {
      cmd.ExcuteCommand();
    }
  }
}

客户端代码实现

static void Main (string[] args)
{
  //开店前的准备
  Barbecuer boy = new Barbecuer();
  Command bakeMuttonCommand1 = new BakeMuttonCommand(boy);
  Command bakeMuttonCommand2 = new BakeMuttonCommand(boy);
  Command bakeChickenWingCommand1 = new BakeChickenWingCommand(boy);
  Waiter girl = new Waiter();
  
  // 开门营业顾客点菜
  girl.SetOrder(bakeMuttonCommand1);
  girl.SetOrder(bakeMuttonCommand2);
  girl.SetOrder(bakeChickenWingCommand1);
  
  // 点菜完闭,通知厨房
  // 订单下好后,一次通知
  girl.Notify();
  
  Console.Read();
}

执行结果:

增加订单: CommandModel._1.BakeMuttonCommand 时间:2021/5/8 11:23:18
增加订单: CommandModel._1.BakeMuttonCommand 时间:2021/5/8 11:23:18
服务员:鸡翅没有了,请点别的烧烤。
烤羊肉串!
烤羊肉串!

“哈,这就比较完整了。”大鸟满意地点点头。

“你还没有说这是什么设计模式呢。”

“哈,你猜也应该猜得出,你的那个抽象类叫什么?”

“命令,哦,这就是大名鼎鼎的命令模式呀。”

6 命令模式

命令模式(Command),将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。[DP]

命令模式(Command)结构图

烤羊肉串引来的思考——命令模式

Command类,用来声明执行操作的接口。

abstract class Command
{
  protected Receiver receiver;
  public Command (Receiver receiver)
  {
    this.receiver = receiver;
  }
  public abstract void Execute();
}

ConcreteCommand类,将一个接收者对象绑定于一个动作,调用接收者相应的操作,以实现Execute。

class ConcreteCommand : Command
{
  public ConcreteCommand(Receiver receiver) : base (receiver)
  { }
  
  public override void Execute()
  {
    receiver.Action();
  }
}

Invoker类,要求该命令执行这个请求。

class Invoker
{
  private Command command;
  
  public void SetCommand(Command command)
  (
    this.command = command;
  )
  public void ExecuteCommand()
  {
    command.Execute();
  }
}

Receiver类,知道如何实施与执行一个与请求相关的操作,任何类都可能作为一个接收者。

```C#
class Receiver
{
  public void Action()
  {
    Console.WriteLine("执行请求!");
  }
}

客户端代码,创建一个具体命令对象并设定它的接收者。

static void Main (string [] args)
{
  Receiver r = new Receiver();
  Command c = new ConcreteCommand(r);
  Invoker i = new Invoker();
  i.SetCommand(c);
  i.ExecuteCommand();
  Console.Read();
}

7 命令模式作用

“来来来,小菜你来总结一下命令模式的优点。”

“我觉得第一,它能较容易地设计一个命令队列;第二,在需要的情况下,可以较容易地将命令记入日志:第三,允许接收请求的一方决定是否要否决请求。

“还有就是第四,可以容易地实现对请求的撤销和重做;第五,由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很容易。其实还有最关键的优点就是命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。[DP]”大鸟接着总结说。

“但是否是碰到类似情况就一定要实现命令模式呢?”

“这就不一定了,比如命令模式支持撤销/恢复操作功能,但你还不清楚是否需要这个功能时,你要不要实现命令模式?”

“要,万一以后需要就不好办了。”

“其实应该是不要实现。敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现这个模式并不困难,只有在真正需要如撤销/恢复操作等功能时,把原来的代码重构为命令模式才有意义。[R2P]

“明白。这一顿我请客了。”小菜很开心,大声叫了一句,“服务员,埋单。”

“先生,你们一共吃了28元。”服务员递过来一个收费单。

小菜正准备付钱。

“慢!”大鸟按住小菜的手,“不对呀,我们没有吃10串羊肉串,后来改成6串了。应该是24元。”

服务员去查了查账本,回来很抱歉地说,“真是对不起,我们算错了,应该是24元。”

“小菜,你看到了吧,如果不是服务员做了记录,也就是记日志,单就烤肉串的人,哪记得住烤了多少串,后果就是大家都说不清楚了。”

“还是大鸟精明呀。”


每天学一点,不贪多。

下一篇我们接着读“第24章 加薪非要老总批?——职责链模式”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论