本文的概念性内容来自深入浅出设计模式一书

项目需求

这是一个糖果机的需求图.

它有四种状态, 分别是图中的四个圆圈:

  • No Quarter: 无硬币
  • Has Quater 有硬币
  • Gumball Sold 糖果卖出
  • Out of Gumball 没有糖果了

这个图很像一个状态图. 每个圆圈就是一个状态, 每个带箭头的线就是状态的转换.

这个需求用文字描述就是: 糖果机在没投硬币的时候, 可以投硬币, 投完硬币, 搬动手柄, 糖果就会出来, 如果糖果机里没有糖果了, 那么就无法再卖糖果了.

初步设计

这个需求看起来还是蛮简单的, 我想可以这样来实现:

1. 整理好所有的状态, 一共有4个:

2. 创建一个实例变量来保存当前的状态, 并为每个状态定义一个值:

3. 整理好系统中可能发生的动作:

4. 创建一个类作为状态机, 针对每一个动作, 我们创建一个方法, 在方法里我们使用条件语句来决定在每个状态中该行为是否合理. 例如, 投入硬币后, 我们可能需要下面这个方法:

注意: 最后一个if中, 有改变状态的动作(如果之前的状态是没有硬币, 那么投入硬币后, 状态应改为有硬币).

下面来实现这个状态机:

代码量还是不小啊, 这里面主要做的就是在每个动作方法里, 判断各种状态, 如何合理就改变状态.

运行一下:

结果:

看起来一切都OK了, 直到:

需求变更

糖果机老板说, 我想买糖果变成一个游戏, 投硬币买糖果的人中的10%在搬动手柄后将会得到两个糖果而不是一个.

现在的状态开始有点乱了:

随着需求的变化, 我们设计会导致越来越多的bug...

回想一下设计原则: "把变化的部分封装起来" 和 "尽量使用组合". 我们可以把每个状态的行为放到它自己的类里面, 然后每个动作只需要实现自己状态下的动作即可. 而且也许糖果机可以使用状态对象来委托表示自己当前的状态.

重新设计

这次我们就把状态的行为封装到各个状态对象里面, 并在动作发生的时候委托到当前的状态.

1. 首先, 定义一个状态接口, 这个接口包含糖果机的每个动作

2. 针对每种状态, 实现一个具体的状态类. 这些类将负责糖果机在改状态下的行为.

3. 最后, 去掉那些条件判断代码, 把这些工作委托给状态类.

上面要实现的就是状态模式 (State Pattern).

把一个状态所有的行为放到一个类里面, 这样, 就实现了本地化并且便于修改和理解.

设计类图:

这里我们使用状态类来代替初版设计中的数值.

当然别忘了这个状态:

现在我直接使用C#实现这些状态:

状态接口:

  1. namespace StatePattern.Abstractions
  2. {
  3. public interface IState
  4. {
  5. void InjectQuarter();
  6. void EjectQuarter();
  7. void TurnCrank();
  8. void Dispense();
  9. }
  10. }

五个状态, 有硬币:

  1. using System;
  2. using StatePattern.Abstractions;
  3. using StatePattern.Machines;
  4.  
  5. namespace StatePattern.States
  6. {
  7. public class HasQuarterState : IState
  8. {
  9. private readonly GumballMachine _gumballMachine;
  10. private readonly Random _random = new Random();
  11.  
  12. public HasQuarterState(GumballMachine gumballMachine)
  13. {
  14. _gumballMachine = gumballMachine;
  15. }
  16.  
  17. public void InjectQuarter()
  18. {
  19. Console.WriteLine("You can’t insert another quarter");
  20. }
  21.  
  22. public void EjectQuarter()
  23. {
  24. Console.WriteLine("Quarter returned");
  25. _gumballMachine.State = _gumballMachine.NoQuarterState;
  26. }
  27.  
  28. public void TurnCrank()
  29. {
  30. Console.WriteLine("You turned...");
  31. var winner = _random.Next(, );
  32. if (winner == && _gumballMachine.Count > )
  33. {
  34. _gumballMachine.State = _gumballMachine.WinnerState;
  35. }
  36. else
  37. {
  38. _gumballMachine.State = _gumballMachine.SoldState;
  39. }
  40. }
  41.  
  42. public void Dispense()
  43. {
  44. Console.WriteLine("No gumball dispensed");
  45. }
  46.  
  47. public override string ToString()
  48. {
  49. return "just being inserted with a quarter";
  50. }
  51. }
  52. }

无硬币:

  1. using System;
  2. using StatePattern.Abstractions;
  3. using StatePattern.Machines;
  4.  
  5. namespace StatePattern.States
  6. {
  7. public class NoQuarterState: IState
  8. {
  9. private readonly GumballMachine _gumballMachine;
  10.  
  11. public NoQuarterState(GumballMachine gumballMachine)
  12. {
  13. _gumballMachine = gumballMachine;
  14. }
  15.  
  16. public void InjectQuarter()
  17. {
  18. Console.WriteLine("You inserted a quarter");
  19. _gumballMachine.State = _gumballMachine.HasQuarterState;
  20. }
  21.  
  22. public void EjectQuarter()
  23. {
  24. Console.WriteLine("You havn't inserted a quarter");
  25. }
  26.  
  27. public void TurnCrank()
  28. {
  29. Console.WriteLine("You turned, but there is no quarter");
  30. }
  31.  
  32. public void Dispense()
  33. {
  34. Console.WriteLine("You need to pay first");
  35. }
  36.  
  37. public override string ToString()
  38. {
  39. return "is Waiting for quarter";
  40. }
  41. }
  42. }

卖光了:

  1. using System;
  2. using StatePattern.Abstractions;
  3. using StatePattern.Machines;
  4.  
  5. namespace StatePattern.States
  6. {
  7. public class SoldOutState: IState
  8. {
  9. private readonly GumballMachine _gumballMachine;
  10.  
  11. public SoldOutState(GumballMachine gumballMachine)
  12. {
  13. _gumballMachine = gumballMachine;
  14. }
  15.  
  16. public void InjectQuarter()
  17. {
  18. Console.WriteLine("You can’t insert a quarter, the machine is sold out");
  19. }
  20.  
  21. public void EjectQuarter()
  22. {
  23. Console.WriteLine("You can’t eject, you haven’t inserted a quarter yet");
  24. }
  25.  
  26. public void TurnCrank()
  27. {
  28. Console.WriteLine("You turned, but there are no gumballs");
  29. }
  30.  
  31. public void Dispense()
  32. {
  33. Console.WriteLine("No gumball dispensed");
  34. }
  35.  
  36. public override string ToString()
  37. {
  38. return "is sold out";
  39. }
  40. }
  41. }

刚刚卖出糖果:

  1. using System;
  2. using StatePattern.Abstractions;
  3. using StatePattern.Machines;
  4.  
  5. namespace StatePattern.States
  6. {
  7. public class SoldState : IState
  8. {
  9. private readonly GumballMachine _gumballMachine;
  10.  
  11. public SoldState(GumballMachine gumballMachine)
  12. {
  13. _gumballMachine = gumballMachine;
  14. }
  15.  
  16. public void InjectQuarter()
  17. {
  18. Console.WriteLine("Please wait, we’re already giving you a gumball");
  19. }
  20.  
  21. public void EjectQuarter()
  22. {
  23. Console.WriteLine("Sorry, you already turned the crank");
  24. }
  25.  
  26. public void TurnCrank()
  27. {
  28. Console.WriteLine("Turning twice doesn’t get you another gumball!");
  29. }
  30.  
  31. public void Dispense()
  32. {
  33. _gumballMachine.ReleaseBall();
  34. if (_gumballMachine.Count > )
  35. {
  36. _gumballMachine.State = _gumballMachine.NoQuarterState;
  37. }
  38. else
  39. {
  40. Console.WriteLine("Oops, out of gumballs!");
  41. _gumballMachine.State = _gumballMachine.SoldOutState;
  42. }
  43. }
  44.  
  45. public override string ToString()
  46. {
  47. return "just sold a gumball";
  48. }
  49. }
  50. }

中奖了:

  1. using System;
  2. using StatePattern.Abstractions;
  3. using StatePattern.Machines;
  4.  
  5. namespace StatePattern.States
  6. {
  7. public class WinnerState: IState
  8. {
  9. private readonly GumballMachine _gumballMachine;
  10.  
  11. public WinnerState(GumballMachine gumballMachine)
  12. {
  13. _gumballMachine = gumballMachine;
  14. }
  15.  
  16. public void InjectQuarter()
  17. {
  18. Console.WriteLine("Please wait, we’re already giving you a gumball");
  19. }
  20.  
  21. public void EjectQuarter()
  22. {
  23. Console.WriteLine("Sorry, you already turned the crank");
  24. }
  25.  
  26. public void TurnCrank()
  27. {
  28. Console.WriteLine("Turning twice doesn’t get you another gumball!");
  29. }
  30.  
  31. public void Dispense()
  32. {
  33. Console.WriteLine("YOU'RE A WINNER! You get two balls for you quarter");
  34. _gumballMachine.ReleaseBall();
  35. if (_gumballMachine.Count == )
  36. {
  37. _gumballMachine.State = _gumballMachine.SoldOutState;
  38. }
  39. else
  40. {
  41. _gumballMachine.ReleaseBall();
  42. if (_gumballMachine.Count > )
  43. {
  44. _gumballMachine.State = _gumballMachine.NoQuarterState;
  45. }
  46. else
  47. {
  48. Console.WriteLine("Oops, out of gumballs!");
  49. _gumballMachine.State = _gumballMachine.SoldOutState;
  50. }
  51. }
  52. }
  53.  
  54. public override string ToString()
  55. {
  56. return "just sold 2 gumballs";
  57. }
  58. }
  59. }

糖果机:

  1. using System;
  2. using StatePattern.Abstractions;
  3. using StatePattern.States;
  4.  
  5. namespace StatePattern.Machines
  6. {
  7. public class GumballMachine
  8. {
  9. public IState SoldOutState { get; set; }
  10. public IState NoQuarterState { get; set; }
  11. public IState HasQuarterState { get; set; }
  12. public IState SoldState { get; set; }
  13. public IState WinnerState { get; set; }
  14. public IState State { get; set; }
  15. public int Count { get; set; }
  16.  
  17. public GumballMachine(int numberOfGumballs)
  18. {
  19. SoldState = new SoldState(this);
  20. NoQuarterState = new NoQuarterState(this);
  21. HasQuarterState = new HasQuarterState(this);
  22. SoldOutState = new SoldOutState(this);
  23. WinnerState = new WinnerState(this);
  24.  
  25. Count = numberOfGumballs;
  26. if (Count > )
  27. {
  28. State = NoQuarterState;
  29. }
  30. }
  31.  
  32. public void InjectQuarter()
  33. {
  34. State.InjectQuarter();
  35. }
  36.  
  37. public void EjectQuarter()
  38. {
  39. State.EjectQuarter();
  40. }
  41.  
  42. public void TurnCrank()
  43. {
  44. State.TurnCrank();
  45. State.Dispense();
  46. }
  47.  
  48. public void ReleaseBall()
  49. {
  50. Console.WriteLine("A gumball comes rolling out the slot...");
  51. if (Count != )
  52. {
  53. Count--;
  54. }
  55. }
  56.  
  57. public void Refill(int count)
  58. {
  59. Count += count;
  60. State = NoQuarterState;
  61. }
  62.  
  63. public override string ToString()
  64. {
  65. return $"Mighty Gumball, Inc.\nC#-enabled Standing Gumball Model #2018\nInventory: {Count} gumballs\nThe machine {State} ";
  66. }
  67. }
  68. }

注意糖果机里面的状态使用的是对象而不是原来的数值.

运行:

  1. using System;
  2. using StatePattern.Machines;
  3.  
  4. namespace StatePattern
  5. {
  6. class Program
  7. {
  8. static void Main(string[] args)
  9. {
  10. var originalColor = Console.ForegroundColor;
  11.  
  12. var machine = new GumballMachine();
  13. Console.ForegroundColor = ConsoleColor.Blue;
  14. Console.WriteLine(machine);
  15.  
  16. Console.ForegroundColor = originalColor;
  17. Console.WriteLine();
  18. machine.InjectQuarter();
  19. machine.TurnCrank();
  20. Console.ForegroundColor = ConsoleColor.Blue;
  21. Console.WriteLine(machine);
  22.  
  23. Console.ForegroundColor = originalColor;
  24. Console.WriteLine();
  25. machine.InjectQuarter();
  26. machine.TurnCrank();
  27. machine.InjectQuarter();
  28. machine.TurnCrank();
  29. machine.InjectQuarter();
  30. machine.TurnCrank();
  31. machine.InjectQuarter();
  32. machine.TurnCrank();
  33. Console.ForegroundColor = ConsoleColor.Blue;
  34. Console.WriteLine(machine);
  35.  
  36. Console.ReadKey();
  37. }
  38. }
  39. }

我们做了什么?

我们修改了设计的结构, 但是功能是一样的:

  • 把每个状态的行为本地化到它自己的类里面了
  • 移除了所有状态判断代码, 他们也很难维护.
  • 对每个状态的修改关闭, 但是让糖果机仍然可以扩展 (添加WINNER 状态)
  • 创建了一个与需求图几乎完美对应的代码库和类结构, 便于理解.

状态模式定义

状态模式允许一个对象在内部状态改变的时候可以修改它自己的行为. 对象似乎修改了它的类.

第二句可以这样理解:

从客户的观点, 如果一个你使用的对象可以完全改变它的行为, 那么这个对象看起来就像是从别的类初始化出来的一样 (变了一个类). 而实际上呢, 你使用的是组合的方式来实现变类的效果, 具体到我们的项目就是引用不同的状态对象.

类图:

Context(上下文环境)就是拥有很多内部状态的类, 糖果机.

每当request()发生在Context上的时候, 它就被委托给了当时的状态对象. 右边就是各种状态对象.

比较一下策略模式和状态模式

这两个模式表面上看起来可能有点像, 但是实际上它们的目的不同的.

状态模式下, 我们把一套行为封装在状态对象里; 任何要给时刻, Context总是委托工作给其中的一个对象. 随着时间的变化, Context的当前状态对象也发生变化, 所以Context的行为也随之变化. 客户对状态对象知道的很少.

策略模式下, 客户要指定策略对象和Context组合. 该模式允许在运行时灵活的改变策略, 通常会有一个最适合当时环境的策略.

总体来说,

策略模式是对继承的灵活替换. 使用继承来定义类的行为, 当你需要改变的时候, 这个行为还会在的, 使用策略模式可是组合不同的对象来改变行为.

状态模式则是一大堆条件判断的代替者, 把行为封装在状态对象里, 就可以简单的通过切换状态对象来改变Context的行为.

其他问题

Q: 总是由具体的状态对象来决定状态的走向吗?

A: 也不是, 可以用Context决定状态的走向.

Q: 客户直接访问状态吗?

A: 客户不直接改变状态.

Q: 如果Context有很多实例, 那么可以共享状态对象吗?

A: 可以, 这个也经常发生. 但这要求你的状态对象不可以保存它们的内部状态, 否则每个Context都需要一个单独的实例.

Q: 这么设计看起来类很多啊!

A: 是啊, 但是可以让对客户可见的类的个数很少, 这个数量才重要

Q: 可以使用抽象类来代替State接口吗?

A: 可以, 如果需要一些公共方法的话.

总结

今天的比较简单, 只有这一个概念:

状态模式允许一个对象在内部状态改变的时候可以修改它自己的行为. 对象似乎修改了它的类.

该系列的源码在: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp

使用C# (.NET Core) 实现状态设计模式 (State Pattern)的更多相关文章

  1. 乐在其中设计模式(C#) - 状态模式(State Pattern)

    原文:乐在其中设计模式(C#) - 状态模式(State Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 状态模式(State Pattern) 作者:webabcd 介绍 允 ...

  2. 二十四种设计模式:状态模式(State Pattern)

    状态模式(State Pattern) 介绍允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它所属的类. 示例有一个Message实体类,对它的操作有Insert()和Get()方法, ...

  3. 状态模式-State Pattern(Java实现)

    状态模式-State Pattern 在状态模式(State Pattern)中,类的行为是基于它的状态改变的.当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. State接口 ...

  4. [设计模式] 20 状态模式 State Pattern

    在GOF的<设计模式:可复用面向对象软件的基础>一书中对状态模式是这样说的:允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它的类.状态模式的重点在于状态转换,很多时候,对 ...

  5. C#设计模式——状态模式(State Pattern)

    一.概述在面向对象软件设计时,常常碰到某一个对象由于状态的不同而有不同的行为.如果用if else或是switch case等方法处理,对象操作及对象的状态就耦合在一起,碰到复杂的情况就会造成代码结构 ...

  6. 【UE4 设计模式】状态模式 State Pattern

    概述 描述 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类. 其别名为状态对象(Objects for States),状态模式是一种对象行为型模式. 有限状态机(FSMs) ...

  7. 十一个行为模式之状态模式(State Pattern)

    定义: 当一个对象有多个状态,并且在每个状态下有不同的行为,可以使用状态模式来在其内部改变状态时改变其行为,而客户端不会察觉状态的改变,仍使用同样的方法或接口与对象进行交互. 结构图: Context ...

  8. 状态模式(State Pattern)

    当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. 状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况.把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂 ...

  9. 使用C# (.NET Core) 实现组合设计模式 (Composite Pattern)

    本文的概念性内容来自深入浅出设计模式一书. 本文需结合上一篇文章(使用C# (.NET Core) 实现迭代器设计模式)一起看. 上一篇文章我们研究了多个菜单一起使用的问题. 需求变更 就当我们感觉我 ...

随机推荐

  1. 吝啬的国度 nyoj

    吝啬的国度 时间限制:1000 ms  |  内存限制:65535 KB 难度:3   描述 在一个吝啬的国度里有N个城市,这N个城市间只有N-1条路把这个N个城市连接起来.现在,Tom在第S号城市, ...

  2. MongoDb进阶实践之三 MongoDB查询命令详述

    一.引言           上一篇文章我们已经介绍了MongoDB数据库的最基本操作,包括数据库的创建.使用和删除数据库,文档的操作也涉及到了文档的创建.删除.更新和查询,当然也包括集合的创建.重命 ...

  3. vue项目中的常见问题

    总结了几个vue项目开发过程中遇到的常见问题,希望大家注意. 注:文末有福利! 一.样式问题 1.vue中使用less 安装less依赖 npm install less less-loader -- ...

  4. 开发一个http代理服务器

    参考链接: http://www.cnblogs.com/jivi/archive/2013/03/10/2952860.html https://www.2cto.com/kf/201405/297 ...

  5. 新概念英语(1-65)Not a Baby

    新概念英语(1-65)Not a Baby Does Jill take the key to the front door? A:What are you going to do this even ...

  6. 关于css的层叠上下文和层叠顺序问题

    关于css的层叠上下文和层叠样式问题 最近在项目中遇到了一个让我欲仙欲死的问题,我给项目中的图片设置了一个淡入效果,几opacity变化,但当我在它的上面有一个定位元素时,动画结束后,定位元素居然被遮 ...

  7. POJ-2031 Building a Space Station---MST + 空间距离

    题目链接: https://vjudge.net/problem/POJ-2031 题目大意: 就是给出三维坐标系上的一些球的球心坐标和其半径,搭建通路,使得他们能够相互连通.如果两个球有重叠的部分则 ...

  8. SQL Server 查询性能优化——创建索引原则(一)(转载)

    索引是什么?索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以.但是索引对于提高查询性能也不是万能的,也不是建立越多的索引 ...

  9. 机器学习技法:12 Neural Network

    Roadmap Motivation Neural Network Hypothesis Neural Network Learning Optimization and Regularization ...

  10. 实验吧_密码忘记了(vim编辑器+代码审计)&天网管理系统(php弱比较+反序列化)

    密码忘记了 一开始尝试了各种注入发现都无效,在网页源码中找到了admin 的地址,输入地址栏发现并没有什么有用的信息,随便输个邮箱,网页返回了一个地址 ./step2.php?email=youmai ...