使用C# (.NET Core) 实现状态设计模式 (State Pattern)

简介: 本文的概念性内容来自深入浅出设计模式一书 项目需求 这是一个糖果机的需求图.  它有四种状态, 分别是图中的四个圆圈: No Quarter: 无硬币 Has Quater 有硬币 Gumball Sold 糖果卖出 Out of Gumball 没有糖果了 这个图很像一个状态图.

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

项目需求

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

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

  • 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(0, 10);
            if (winner == 0 && _gumballMachine.Count > 1)
            {
                _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";
        }
    }
}
View Code

卖光了:

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";
        }
    }
}
View Code

刚刚卖出糖果:

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 > 0)
            {
                _gumballMachine.State = _gumballMachine.NoQuarterState;
            }
            else
            {
                Console.WriteLine("Oops, out of gumballs!");
                _gumballMachine.State = _gumballMachine.SoldOutState;
            }
        }

        public override string ToString()
        {
            return "just sold a gumball";
        }
    }
}
View Code

中奖了:

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 == 0)
            {
                _gumballMachine.State = _gumballMachine.SoldOutState;
            }
            else
            {
                _gumballMachine.ReleaseBall();
                if (_gumballMachine.Count > 0)
                {
                    _gumballMachine.State = _gumballMachine.NoQuarterState;
                }
                else
                {
                    Console.WriteLine("Oops, out of gumballs!");
                    _gumballMachine.State = _gumballMachine.SoldOutState;
                }
            }
        }

        public override string ToString()
        {
            return "just sold 2 gumballs";
        }
    }
}
View Code

糖果机:

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 > 0)
            {
                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 != 0)
            {
                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(5);
            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

下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏:

目录
相关文章
|
8天前
|
算法 C# 数据库
【干货】一份10万字免费的C#/.NET/.NET Core面试宝典
C#/.NET/.NET Core相关技术常见面试题汇总,不仅仅为了面试而学习,更多的是查漏补缺、扩充知识面和大家共同学习进步。该知识库主要由自己平时学习实践总结、网上优秀文章资料收集(这一部分会标注来源)和社区小伙伴提供三部分组成。该份基础面试宝典完全免费,发布两年来收获了广大.NET小伙伴的好评,我会持续更新和改进,欢迎关注我的公众号【追逐时光者】第一时间获取最新更新的面试题内容。
|
8天前
|
数据可视化 网络协议 C#
C#/.NET/.NET Core优秀项目和框架2024年3月简报
公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍、功能特点、使用方式以及部分功能截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯🔔)。
|
8天前
|
SQL 数据库 C#
C# .NET面试系列十一:数据库SQL查询(附建表语句)
#### 第1题 用一条 SQL 语句 查询出每门课都大于80 分的学生姓名 建表语句: ```sql create table tableA ( name varchar(10), kecheng varchar(10), fenshu int(11) ) DEFAULT CHARSET = 'utf8'; ``` 插入数据 ```sql insert into tableA values ('张三', '语文', 81); insert into tableA values ('张三', '数学', 75); insert into tableA values ('李四',
82 2
C# .NET面试系列十一:数据库SQL查询(附建表语句)
|
8天前
|
开发框架 算法 搜索推荐
C# .NET面试系列九:常见的算法
#### 1. 求质数 ```c# // 判断一个数是否为质数的方法 public static bool IsPrime(int number) { if (number < 2) { return false; } for (int i = 2; i <= Math.Sqrt(number); i++) { if (number % i == 0) { return false; } } return true; } class Progr
68 1
|
8天前
|
并行计算 安全 Java
C# .NET面试系列四:多线程
<h2>多线程 #### 1. 根据线程安全的相关知识,分析以下代码,当调用 test 方法时 i > 10 时是否会引起死锁? 并简要说明理由。 ```c# public void test(int i) { lock(this) { if (i > 10) { i--; test(i); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
128 3
|
8天前
|
开发框架 .NET 中间件
C#/.NET快速上手学习资料集(让现在的自己不再迷茫)
C#/.NET快速上手学习资料集(让现在的自己不再迷茫)
|
8天前
|
XML 开发框架 .NET
C#/ASP.NET应用程序配置文件app.config/web.config的增、删、改操作
C#/ASP.NET应用程序配置文件app.config/web.config的增、删、改操作
20 1
|
8天前
|
开发框架 前端开发 JavaScript
采用C#.Net +JavaScript 开发的云LIS系统源码 二级医院应用案例有演示
技术架构:Asp.NET CORE 3.1 MVC + SQLserver + Redis等 开发语言:C# 6.0、JavaScript 前端框架:JQuery、EasyUI、Bootstrap 后端框架:MVC、SQLSugar等 数 据 库:SQLserver 2012
24 0
|
8天前
|
开发框架 人工智能 .NET
C#/.NET/.NET Core拾遗补漏合集(持续更新)
在这个快速发展的技术世界中,时常会有一些重要的知识点、信息或细节被忽略或遗漏。《C#/.NET/.NET Core拾遗补漏》专栏我们将探讨一些可能被忽略或遗漏的重要知识点、信息或细节,以帮助大家更全面地了解这些技术栈的特性和发展方向。
|
8天前
|
SQL 存储 关系型数据库
C# .NET面试系列十:数据库概念知识
#### 1. 为什么要一定要设置主键? 设置主键是数据库设计中的一个重要概念,有几个主要原因: 1、唯一性 ```c# 主键必须保证表中的每一行都有唯一的标识。这样可以避免数据冗余和不一致性。如果没有主键或者主键不唯一,就可能出现数据混乱或错误。 ``` 2、查询性能 ```c# 数据库系统通常会使用主键来加速数据检索。主键通常会被索引,这样可以更快速地找到特定行的数据,提高查询效率。 ``` 3、关联性 ```c# 主键常常用于建立表与表之间的关系。在关系数据库中,一个表的主键通常与其他表中的外键建立关联,这种关系对于数据的一致性和完整性非常重要。 ``` 4、数据完
137 1
C# .NET面试系列十:数据库概念知识