使用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相关技术的公众号--草根专栏:

目录
相关文章
|
1月前
|
存储 开发框架 JSON
ASP.NET Core OData 9 正式发布
【10月更文挑战第8天】Microsoft 在 2024 年 8 月 30 日宣布推出 ASP.NET Core OData 9,此版本与 .NET 8 的 OData 库保持一致,改进了数据编码以符合 OData 规范,并放弃了对旧版 .NET Framework 的支持,仅支持 .NET 8 及更高版本。新版本引入了更快的 JSON 编写器 `System.Text.UTF8JsonWriter`,优化了内存使用和序列化速度。
|
2月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
|
2月前
|
开发框架 .NET 中间件
ASP.NET Core Web 开发浅谈
本文介绍ASP.NET Core,一个轻量级、开源的跨平台框架,专为构建高性能Web应用设计。通过简单步骤,你将学会创建首个Web应用。文章还深入探讨了路由配置、依赖注入及安全性配置等常见问题,并提供了实用示例代码以助于理解与避免错误,帮助开发者更好地掌握ASP.NET Core的核心概念。
92 3
|
1月前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
2月前
|
开发框架 NoSQL .NET
利用分布式锁在ASP.NET Core中实现防抖
【9月更文挑战第5天】在 ASP.NET Core 中,可通过分布式锁实现防抖功能,仅处理连续相同请求中的首个请求,其余请求返回 204 No Content,直至锁释放。具体步骤包括:安装分布式锁库如 `StackExchange.Redis`;创建分布式锁服务接口及其实现;构建防抖中间件;并在 `Startup.cs` 中注册相关服务和中间件。这一机制有效避免了短时间内重复操作的问题。
|
3月前
|
开发框架 监控 .NET
开发者的革新利器:ASP.NET Core实战指南,构建未来Web应用的高效之道
【8月更文挑战第28天】本文探讨了如何利用ASP.NET Core构建高效、可扩展的Web应用。ASP.NET Core是一个开源、跨平台的框架,具有依赖注入、配置管理等特性。文章详细介绍了项目结构规划、依赖注入配置、中间件使用及性能优化方法,并讨论了安全性、可扩展性以及容器化的重要性。通过这些技术要点,开发者能够快速构建出符合现代Web应用需求的应用程序。
55 0
|
3月前
|
缓存 数据库连接 API
Entity Framework Core——.NET 领域的 ORM 利器,深度剖析其最佳实践之路
【8月更文挑战第28天】在软件开发领域,高效的数据访问与管理至关重要。Entity Framework Core(EF Core)作为一款强大的对象关系映射(ORM)工具,在 .NET 开发中扮演着重要角色。本文通过在线书店应用案例,展示了 EF Core 的核心特性和优势。我们定义了 `Book` 实体类及其属性,并通过 `BookStoreContext` 数据库上下文配置了数据库连接。EF Core 提供了简洁的 API,支持数据的查询、插入、更新和删除操作。
114 0
|
3月前
|
开发框架 监控 .NET
【Azure 应用程序见解】在Docker中运行的ASP.NET Core应用如何开启Application Insights的Profiler Trace呢?
【Azure 应用程序见解】在Docker中运行的ASP.NET Core应用如何开启Application Insights的Profiler Trace呢?
|
3月前
|
Linux C# C++
【Azure App Service For Container】创建ASP.NET Core Blazor项目并打包为Linux镜像发布到Azure应用服务
【Azure App Service For Container】创建ASP.NET Core Blazor项目并打包为Linux镜像发布到Azure应用服务
|
3月前
|
开发框架 .NET API
如何在 ASP.NET Core Web Api 项目中应用 NLog 写日志?
如何在 ASP.NET Core Web Api 项目中应用 NLog 写日志?
162 0