使用C# (.NET Core) 实现状态设计模式 (State Pattern)
本文的概念性内容来自深入浅出设计模式一书
项目需求
这是一个糖果机的需求图.
它有四种状态, 分别是图中的四个圆圈:
- No Quarter: 无硬币
- Has Quater 有硬币
- Gumball Sold 糖果卖出
- Out of Gumball 没有糖果了
这个图很像一个状态图. 每个圆圈就是一个状态, 每个带箭头的线就是状态的转换.
这个需求用文字描述就是: 糖果机在没投硬币的时候, 可以投硬币, 投完硬币, 搬动手柄, 糖果就会出来, 如果糖果机里没有糖果了, 那么就无法再卖糖果了.
初步设计
这个需求看起来还是蛮简单的, 我想可以这样来实现:
1. 整理好所有的状态, 一共有4个:
2. 创建一个实例变量来保存当前的状态, 并为每个状态定义一个值:
3. 整理好系统中可能发生的动作:
4. 创建一个类作为状态机, 针对每一个动作, 我们创建一个方法, 在方法里我们使用条件语句来决定在每个状态中该行为是否合理. 例如, 投入硬币后, 我们可能需要下面这个方法:
注意: 最后一个if中, 有改变状态的动作(如果之前的状态是没有硬币, 那么投入硬币后, 状态应改为有硬币).
下面来实现这个状态机:
代码量还是不小啊, 这里面主要做的就是在每个动作方法里, 判断各种状态, 如何合理就改变状态.
运行一下:
结果:
看起来一切都OK了, 直到:
需求变更
糖果机老板说, 我想买糖果变成一个游戏, 投硬币买糖果的人中的10%在搬动手柄后将会得到两个糖果而不是一个.
现在的状态开始有点乱了:
随着需求的变化, 我们设计会导致越来越多的bug...
回想一下设计原则: "把变化的部分封装起来" 和 "尽量使用组合". 我们可以把每个状态的行为放到它自己的类里面, 然后每个动作只需要实现自己状态下的动作即可. 而且也许糖果机可以使用状态对象来委托表示自己当前的状态.
重新设计
这次我们就把状态的行为封装到各个状态对象里面, 并在动作发生的时候委托到当前的状态.
1. 首先, 定义一个状态接口, 这个接口包含糖果机的每个动作
2. 针对每种状态, 实现一个具体的状态类. 这些类将负责糖果机在改状态下的行为.
3. 最后, 去掉那些条件判断代码, 把这些工作委托给状态类.
上面要实现的就是状态模式 (State Pattern).
把一个状态所有的行为放到一个类里面, 这样, 就实现了本地化并且便于修改和理解.
设计类图:
这里我们使用状态类来代替初版设计中的数值.
当然别忘了这个状态:
现在我直接使用C#实现这些状态:
状态接口:
namespace StatePattern.Abstractions
{
public interface IState
{
void InjectQuarter();
void EjectQuarter();
void TurnCrank();
void Dispense();
}
}
五个状态, 有硬币:
using System;
using StatePattern.Abstractions;
using StatePattern.Machines; namespace StatePattern.States
{
public class HasQuarterState : IState
{
private readonly GumballMachine _gumballMachine;
private readonly Random _random = new Random(); public HasQuarterState(GumballMachine gumballMachine)
{
_gumballMachine = gumballMachine;
} public void InjectQuarter()
{
Console.WriteLine("You can’t insert another quarter");
} public void EjectQuarter()
{
Console.WriteLine("Quarter returned");
_gumballMachine.State = _gumballMachine.NoQuarterState;
} public void TurnCrank()
{
Console.WriteLine("You turned...");
var winner = _random.Next(, );
if (winner == && _gumballMachine.Count > )
{
_gumballMachine.State = _gumballMachine.WinnerState;
}
else
{
_gumballMachine.State = _gumballMachine.SoldState;
}
} public void Dispense()
{
Console.WriteLine("No gumball dispensed");
} public override string ToString()
{
return "just being inserted with a quarter";
}
}
}
无硬币:
using System;
using StatePattern.Abstractions;
using StatePattern.Machines; namespace StatePattern.States
{
public class NoQuarterState: IState
{
private readonly GumballMachine _gumballMachine; public NoQuarterState(GumballMachine gumballMachine)
{
_gumballMachine = gumballMachine;
} public void InjectQuarter()
{
Console.WriteLine("You inserted a quarter");
_gumballMachine.State = _gumballMachine.HasQuarterState;
} public void EjectQuarter()
{
Console.WriteLine("You havn't inserted a quarter");
} public void TurnCrank()
{
Console.WriteLine("You turned, but there is no quarter");
} public void Dispense()
{
Console.WriteLine("You need to pay first");
} public override string ToString()
{
return "is Waiting for quarter";
}
}
}
卖光了:
using System;
using StatePattern.Abstractions;
using StatePattern.Machines; namespace StatePattern.States
{
public class SoldOutState: IState
{
private readonly GumballMachine _gumballMachine; public SoldOutState(GumballMachine gumballMachine)
{
_gumballMachine = gumballMachine;
} public void InjectQuarter()
{
Console.WriteLine("You can’t insert a quarter, the machine is sold out");
} public void EjectQuarter()
{
Console.WriteLine("You can’t eject, you haven’t inserted a quarter yet");
} public void TurnCrank()
{
Console.WriteLine("You turned, but there are no gumballs");
} public void Dispense()
{
Console.WriteLine("No gumball dispensed");
} public override string ToString()
{
return "is sold out";
}
}
}
刚刚卖出糖果:
using System;
using StatePattern.Abstractions;
using StatePattern.Machines; namespace StatePattern.States
{
public class SoldState : IState
{
private readonly GumballMachine _gumballMachine; public SoldState(GumballMachine gumballMachine)
{
_gumballMachine = gumballMachine;
} public void InjectQuarter()
{
Console.WriteLine("Please wait, we’re already giving you a gumball");
} public void EjectQuarter()
{
Console.WriteLine("Sorry, you already turned the crank");
} public void TurnCrank()
{
Console.WriteLine("Turning twice doesn’t get you another gumball!");
} public void Dispense()
{
_gumballMachine.ReleaseBall();
if (_gumballMachine.Count > )
{
_gumballMachine.State = _gumballMachine.NoQuarterState;
}
else
{
Console.WriteLine("Oops, out of gumballs!");
_gumballMachine.State = _gumballMachine.SoldOutState;
}
} public override string ToString()
{
return "just sold a gumball";
}
}
}
中奖了:
using System;
using StatePattern.Abstractions;
using StatePattern.Machines; namespace StatePattern.States
{
public class WinnerState: IState
{
private readonly GumballMachine _gumballMachine; public WinnerState(GumballMachine gumballMachine)
{
_gumballMachine = gumballMachine;
} public void InjectQuarter()
{
Console.WriteLine("Please wait, we’re already giving you a gumball");
} public void EjectQuarter()
{
Console.WriteLine("Sorry, you already turned the crank");
} public void TurnCrank()
{
Console.WriteLine("Turning twice doesn’t get you another gumball!");
} public void Dispense()
{
Console.WriteLine("YOU'RE A WINNER! You get two balls for you quarter");
_gumballMachine.ReleaseBall();
if (_gumballMachine.Count == )
{
_gumballMachine.State = _gumballMachine.SoldOutState;
}
else
{
_gumballMachine.ReleaseBall();
if (_gumballMachine.Count > )
{
_gumballMachine.State = _gumballMachine.NoQuarterState;
}
else
{
Console.WriteLine("Oops, out of gumballs!");
_gumballMachine.State = _gumballMachine.SoldOutState;
}
}
} public override string ToString()
{
return "just sold 2 gumballs";
}
}
}
糖果机:
using System;
using StatePattern.Abstractions;
using StatePattern.States; namespace StatePattern.Machines
{
public class GumballMachine
{
public IState SoldOutState { get; set; }
public IState NoQuarterState { get; set; }
public IState HasQuarterState { get; set; }
public IState SoldState { get; set; }
public IState WinnerState { get; set; }
public IState State { get; set; }
public int Count { get; set; } public GumballMachine(int numberOfGumballs)
{
SoldState = new SoldState(this);
NoQuarterState = new NoQuarterState(this);
HasQuarterState = new HasQuarterState(this);
SoldOutState = new SoldOutState(this);
WinnerState = new WinnerState(this); Count = numberOfGumballs;
if (Count > )
{
State = NoQuarterState;
}
} public void InjectQuarter()
{
State.InjectQuarter();
} public void EjectQuarter()
{
State.EjectQuarter();
} public void TurnCrank()
{
State.TurnCrank();
State.Dispense();
} public void ReleaseBall()
{
Console.WriteLine("A gumball comes rolling out the slot...");
if (Count != )
{
Count--;
}
} public void Refill(int count)
{
Count += count;
State = NoQuarterState;
} public override string ToString()
{
return $"Mighty Gumball, Inc.\nC#-enabled Standing Gumball Model #2018\nInventory: {Count} gumballs\nThe machine {State} ";
}
}
}
注意糖果机里面的状态使用的是对象而不是原来的数值.
运行:
using System;
using StatePattern.Machines; namespace StatePattern
{
class Program
{
static void Main(string[] args)
{
var originalColor = Console.ForegroundColor; var machine = new GumballMachine();
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine(machine); Console.ForegroundColor = originalColor;
Console.WriteLine();
machine.InjectQuarter();
machine.TurnCrank();
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine(machine); Console.ForegroundColor = originalColor;
Console.WriteLine();
machine.InjectQuarter();
machine.TurnCrank();
machine.InjectQuarter();
machine.TurnCrank();
machine.InjectQuarter();
machine.TurnCrank();
machine.InjectQuarter();
machine.TurnCrank();
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine(machine); Console.ReadKey();
}
}
}
我们做了什么?
我们修改了设计的结构, 但是功能是一样的:
- 把每个状态的行为本地化到它自己的类里面了
- 移除了所有状态判断代码, 他们也很难维护.
- 对每个状态的修改关闭, 但是让糖果机仍然可以扩展 (添加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)的更多相关文章
- 乐在其中设计模式(C#) - 状态模式(State Pattern)
原文:乐在其中设计模式(C#) - 状态模式(State Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 状态模式(State Pattern) 作者:webabcd 介绍 允 ...
- 二十四种设计模式:状态模式(State Pattern)
状态模式(State Pattern) 介绍允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它所属的类. 示例有一个Message实体类,对它的操作有Insert()和Get()方法, ...
- 状态模式-State Pattern(Java实现)
状态模式-State Pattern 在状态模式(State Pattern)中,类的行为是基于它的状态改变的.当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. State接口 ...
- [设计模式] 20 状态模式 State Pattern
在GOF的<设计模式:可复用面向对象软件的基础>一书中对状态模式是这样说的:允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它的类.状态模式的重点在于状态转换,很多时候,对 ...
- C#设计模式——状态模式(State Pattern)
一.概述在面向对象软件设计时,常常碰到某一个对象由于状态的不同而有不同的行为.如果用if else或是switch case等方法处理,对象操作及对象的状态就耦合在一起,碰到复杂的情况就会造成代码结构 ...
- 【UE4 设计模式】状态模式 State Pattern
概述 描述 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类. 其别名为状态对象(Objects for States),状态模式是一种对象行为型模式. 有限状态机(FSMs) ...
- 十一个行为模式之状态模式(State Pattern)
定义: 当一个对象有多个状态,并且在每个状态下有不同的行为,可以使用状态模式来在其内部改变状态时改变其行为,而客户端不会察觉状态的改变,仍使用同样的方法或接口与对象进行交互. 结构图: Context ...
- 状态模式(State Pattern)
当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. 状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况.把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂 ...
- 使用C# (.NET Core) 实现组合设计模式 (Composite Pattern)
本文的概念性内容来自深入浅出设计模式一书. 本文需结合上一篇文章(使用C# (.NET Core) 实现迭代器设计模式)一起看. 上一篇文章我们研究了多个菜单一起使用的问题. 需求变更 就当我们感觉我 ...
随机推荐
- EasyUi中对话框。
html页面代码: <head id="Head1" runat="server"> <meta http-equiv="Conte ...
- Python——cmd调用(os.system阻塞处理)
os.system(返回值为0,1,2) 0:成功 1:失败 2:错误 os.system默认阻塞当前程序执行,在cmd命令前加入start可不阻塞当前程序执行. 例如: import os os.s ...
- 剑指offer-数据流中的中位数
题目描述 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值.如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值. ...
- 算法 排序lowB三人组 冒泡排序 选择排序 插入排序
参考博客:基于python的七种经典排序算法 [经典排序算法][集锦] 经典排序算法及python实现 首先明确,算法的实质 是 列表排序.具体就是操作的列表,将无序列表变成有序列表! 一 ...
- Hibernate(四):Hello World
下载hibernate开发包: 在本章之前需要继承hibernate开发插件到eclipse,详细操作请参考我的博文:<Hibernate(一):安装hibernate插件到eclipse环境& ...
- ZOJ-1655 Transport Goods---dijkstra变形&&最长路
题目链接: https://vjudge.net/problem/ZOJ-1655 题目大意: 有N-1个城市给首都(第N个城市)支援物资,有M条路,走每条路要耗费一定百分比的物资.问给定N-1个城市 ...
- POJ-1062 昂贵的聘礼---Dijkstra+枚举上界
题目链接: https://vjudge.net/problem/POJ-1062 题目大意: 中文题 思路: 1是终点,可以额外添加一个源点0,0到任意一节点的距离就是这个点的money,最终求的是 ...
- Python系列之 - 装饰器
装饰器主要是用来对函数的操作,我们把定义的函数比作一个蛋糕的话,那么装饰器就是盒子,如果要吃蛋糕就先打开盒子.具体到程序中就是在函数外层又套了一层,套的那一层就是一个装饰器.这么说可能有点抽象,那么我 ...
- php实现记住密码自动登录的功能
$username=trim($_POST['username']); $password=md5(trim($_POST['password'])); $ref_url=$_GET['req_url ...
- Windows使用Gitblit搭建Git服务器
安装之前需确定安装JAVA运行环境. 下载安装 首先到 Gitblit官网 下载安装包.此处使用的版本是1.8.0. 将解压得到的gitblit-1.8.0文件夹放于C:\gitServer目录下. ...