让对象拥有状态——C#中的状态模式
大家好,老胡又在博客和大家见面了,在聊今天的主角之前,老胡先给大家讲一个以前发生的故事。
真实的故事
当老胡还是小胡的时候,跟随团队一起开发一款游戏。这款游戏是一款末日生存类游戏,玩家可以
- 收集资源,两种,一种金子,一种铁。
- 升级自身
- 击杀敌人
- 用资源合成装备
项目开发的很顺利,我那时得到一个任务,是为游戏做一个新手教程,在这个教程里面,通过一系列步骤,引导新手玩家熟悉这个游戏。游戏设计给出的教程包含以下步骤
- 收集金子
- 收集铁
- 击杀敌人
- 升级
同时要求在不用的阶段显示不同的提示以正确引导玩家。
考虑合成装备算是高级玩家才会接触到的功能,所以暂时不打算放在新手教程里面。
当老大把任务交给我的时候,我感觉简单爆了,不就写一个新手教程么,要求又那么明确,应该要不了多少时间。于是,一个上午过后,我交出了如下代码。
我的代码
定义枚举表示教程进度
首先用一个枚举,表示教程进行的不同程度
enum TutorialState
{
GetGold,
GetIron,
KillEnemy,
LevelUp
}
定义角色类
无需多言,封装收集到的资源数、击杀敌人数量、角色等级和一些升级接口等
class Player
{
private int ironNum;
private int goldNum;
private int enemyKilled;
private int level;
public int IronNum => ironNum;
public int GoldNum => goldNum;
public int EnemyKilled => enemyKilled;
public int Level => level;
public void CollectIron(int num)
{
ironNum += num;
}
public void CollectGold(int num)
{
goldNum += num;
}
public void KillEnemy()
{
enemyKilled++;
}
public void LevelUp()
{
level++;
}
}
定义教程类
定义一个教程类,包括
- 显示帮助文字以协助玩家通过当前教程步骤
- 判断玩家是否已经完成当前教程步骤,若是,切换到下一个步骤直到完成教程
class GameTutorial
{
private TutorialState currentState;
private Player player;
public GameTutorial(Player player)
{
this.player = player;
}
public void ShowHelpDescription()
{
switch (currentState)
{
case TutorialState.GetGold:
Console.WriteLine("Please follow instruction to get gold");
break;
case TutorialState.GetIron:
Console.WriteLine("Please follow instruction to get Iron");
break;
case TutorialState.KillEnemy:
Console.WriteLine("Please follow instruction to kill enemy");
break;
case TutorialState.LevelUp:
Console.WriteLine("Please follow instruction to Up your level");
break;
default:
throw new Exception("Not Support");
}
}
public void ValidateState()
{
switch (currentState)
{
case TutorialState.GetGold:
{
if (player.GoldNum > 0)
{
Console.WriteLine("Congratulations, you finished Gold Collect Phase");
currentState = TutorialState.GetIron;
}
else
{
Console.WriteLine("You need to collect gold");
}
break;
}
case TutorialState.GetIron:
{
if (player.IronNum > 0)
{
Console.WriteLine("Congratulations, you finished Iron Collect Phase");
currentState = TutorialState.KillEnemy;
}
else
{
Console.WriteLine("You need to collect Iron");
}
break;
}
case TutorialState.KillEnemy:
{
if (player.EnemyKilled > 0)
{
Console.WriteLine("Congratulations, you finished Enemy Kill Phase");
currentState = TutorialState.LevelUp;
}
else
{
Console.WriteLine("You need to kill enemy");
}
break;
}
case TutorialState.LevelUp:
{
if (player.Level > 0)
{
Console.WriteLine("Congratulations, you finished the whole tutorial");
currentState = TutorialState.LevelUp;
}
else
{
Console.WriteLine("You need to level up");
}
break;
}
default:
throw new Exception("Not Support");
}
}
}
测试代码
static void Main(string[] args)
{
Player player = new Player();
GameTutorial tutorial = new GameTutorial(player);
tutorial.ShowHelpDescription();
tutorial.ValidateState();
//收集黄金
player.CollectGold(1);
tutorial.ValidateState();
tutorial.ShowHelpDescription();
//收集木头
player.CollectIron(1);
tutorial.ValidateState();
tutorial.ShowHelpDescription();
//杀敌
player.KillEnemy();
tutorial.ValidateState();
tutorial.ShowHelpDescription();
//升级
player.LevelUp();
tutorial.ValidateState();
}
运行结果
看起来一切都好。。编写的代码既能够根据当前步骤显示不同的提示,还可以成功的根据玩家的进度切换到下一个步骤。
于是,我自信满满的申请了code review,按照我的想法,这段代码通过code review应该是板上钉钉的事情,谁知,老大看到代码,差点没背过气去。。。稍微平复了一下心情之后,他给了我几个灵魂拷问。
- GameTutorial需要知道各个步骤的满足条件和提示,它是不是知道的太多了?这符合迪米特法则吗?
- 如果我们游戏之后新增一个教程步骤,指导玩家升级武器,是不是GameTutorial需要修改?能有办法规避这种新增的改动吗?
- 如果我们要修改现在的教程步骤之间的顺序关系,GameTutorial是不是又不能避免要被动刀?能有办法尽量减少这种修改的工作量吗?
- Switch case 在现有的情况下已经如此长,如果我们再加入新的步骤,这个方法会变成又臭又长的裹脚布吗?
当时我的表情是这样的
本来以为如此简单的一个功能,没想到还是有那么多弯弯道道,只怪自己还是太年轻啊!最后他悠悠的告诉我,去看看状态模式吧,想想这段代码可以怎么重构。
状态模式出场
定义
对象拥有内在状态,当内在状态改变时允许其改变行为,这个对象看起来像改变了其类
有点意思,看来我们可以把教程的不同步骤抽象成不同的状态,然后在各个状态内部实现切换状态和显示帮助文档的逻辑,这样做的好处是
- 符合迪米特法则,把各个步骤所对应的逻辑推迟到子类,教程类就不需要了解每个步骤的逻辑细节,同时隔离了教程类和状态类,确保状态类的修改不会影响教程类
- 符合开闭原则,如果新添加步骤,我们仅仅需要添加步骤子类并修改相邻的步骤切换逻辑,教程类无需任何改动
接着我们看看UML,
一目了然,在我们的例子里面,state就是教程子步骤,context就是教程类,内部包含教程子步骤并转发请求给教程子步骤,我们跟着来重构一下代码吧。
代码重构
创建状态基类
第一步我们需要删除之前的枚举,取而代之的是一个抽象类当作状态基类,即,各个教程步骤类的基类。注意,每个子状态要自己负责状态切换,所以我们需要教程类暴露接口以满足这个功能。
abstract class TutorialState
{
public abstract void ShowHelpDescription();
public abstract void Validate(GameTutorial tutorial);
}
重构教程类
重构教程类体现在以下方面
- 添加内部状态表面当前处于哪个步骤,在构造函数中给予初始值
- 暴露接口以让子状态能修改当前状态以完成状态切换
- 因为需要子状态能访问玩家当前数据以判断是否能切换状态,需要新加接口以避免方法链
- 修改ShowHelpDescription和ValidateState的逻辑,直接转发方法调用至当前状态
class GameTutorial
{
private TutorialState currentState;
private Player player;
public int PlayerIronNum => player.IronNum;
public int PlayerLevel => player.Level;
public int PlayerGoldNum => player.GoldNum;
public int PlayerEnemyKilled => player.EnemyKilled;
public void SetState(TutorialState state)
{
currentState = state;
}
public GameTutorial(Player player)
{
this.player = player;
currentState = TutorialStateContext.GetGold;
}
public void ShowHelpDescription()
{
currentState.ShowHelpDescription();
}
public void ValidateState()
{
currentState.Validate(this);
}
}
创建各个子状态
接着我们创建各个子状态代表不同的教程步骤
class TutorialSateGetGold : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to get gold");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerGoldNum > 0)
{
Console.WriteLine("Congratulations, you finished Gold Collect Phase");
tutorial.SetState(TutorialStateContext.GetIron);
}
else
{
Console.WriteLine("You need to collect gold");
}
}
}
class TutorialStateGetIron : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to get Iron");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerIronNum > 0)
{
Console.WriteLine("Congratulations, you finished Iron Collect Phase");
tutorial.SetState(TutorialStateContext.KillEnemy);
}
else
{
Console.WriteLine("You need to collect iron");
}
}
}
class TutorialStateKillEnemy : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to kill enemy");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerEnemyKilled > 0)
{
Console.WriteLine("Congratulations, you finished enemy kill Phase");
tutorial.SetState(TutorialStateContext.LevelUp);
}
else
{
Console.WriteLine("You need to collect kill enemy");
}
}
}
class TutorialStateLevelUp : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to level up");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerLevel > 0)
{
Console.WriteLine("Congratulations, you finished the whole tutorial");
}
}
}
添加状态容器
这是模式中没有提到的知识点,一般来说,为了避免大量的子状态对象被创建,我们会构造一个状态容器,以静态变量的方式初始化需要使用的子状态。
static class TutorialStateContext
{
public static TutorialState GetGold;
public static TutorialState GetIron;
public static TutorialState KillEnemy;
public static TutorialState LevelUp;
static TutorialStateContext()
{
GetGold = new TutorialSateGetGold();
GetIron = new TutorialStateGetIron();
KillEnemy = new TutorialStateKillEnemy();
LevelUp = new TutorialStateLevelUp();
}
}
测试代码
测试代码部分保持不变,直接运行,结果和原来一样,重构成功。
结语
这就是状态模式和它的使用场景,比较一下重构前和重构后的代码,发现代码通过重构满足了开闭原则和迪米特法则,相信重构后的代码能通过code review吧。_
不过状态模式虽然好,也有自己的缺点,因为需要一个子类对应一个子状态,那么子状态太多的时候,就会出现类爆炸的情况。还请大家多注意。
作为行为模式之一的状态模式,在日常开发中出现的频率还是挺高的,比如游戏中经常用到的状态机,就是状态模式的一种应用场景,大家在平时工作中保持善于观察的眼睛,就能学到更多的东西。
今天就讲到这里吧,谢谢大家的阅读,下次见。
让对象拥有状态——C#中的状态模式的更多相关文章
- 对象在hibernate中的状态
首先hibernate中对象的状态有三种:瞬态.游离态和持久态,三种状态转化的方法都是通过session来调用,瞬态到持久态的方法有save().saveOrUpdate().get().load() ...
- Java对象在内存中的状态
可达的/可触及的 Java对象呗创建后,如果被一个或者多个变量引用,那就是可达的,即从根节点可以触及到这个对象. 其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的. 可恢复的 Java对象 ...
- JVM中线程状态转换图
JVM中线程的状态转换图 线程在一定条件下,状态会发生变化.线程一共有以下几种状态: 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该 ...
- Hibernate实体对象的生命周期(三种状态)
瞬时状态(Transient) 通过new创建对象后,对象并没有立刻持久化,它并未与数据库中的数据有任何关联,此时Java对象的状态为瞬时状态. Session对于瞬时状态的Java对象是一无所知的, ...
- 程序中保存状态的方式之ViewState
程序中保存状态的方式有以下几种: 1.Application 2.Cookie 3.Session 4.ViewState:ViewState是保存状态的方式之一,ViewState实际就是一个Hid ...
- java对象在hibernate持久层的状态
站在持久化层的角度,一个java对象在它的生命周期中,可处于以下4个状态之一: 临时状态(transient):刚刚用new语句创建,还没有被持久化,并且不处于Session的缓存中. 持久化状态(p ...
- Android菜鸟的成长笔记(14)—— Android中的状态保存探究(上)
原文:[置顶] Android菜鸟的成长笔记(14)—— Android中的状态保存探究(上) 我们在用手机的时候可能会发现,即使应用被放到后台再返回到前台数据依然保留(比如说我们正在玩游戏,突然电话 ...
- Android菜鸟的成长笔记(15)—— Android中的状态保存探究(下)
原文:Android菜鸟的成长笔记(15)-- Android中的状态保存探究(下) 在上一篇中我们简单了解关于Android中状态保存的过程和原理,这一篇中我们来看一下在系统配置改变的情况下保存数据 ...
- .NET中的状态机库Stateless
标题:.NET中的状态机库Stateless 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/10674018.html 介绍 什么是状态机和状态模式 ...
随机推荐
- Java实现 LeetCode 441 排列硬币
441. 排列硬币 你总共有 n 枚硬币,你需要将它们摆成一个阶梯形状,第 k 行就必须正好有 k 枚硬币. 给定一个数字 n,找出可形成完整阶梯行的总行数. n 是一个非负整数,并且在32位有符号整 ...
- Java实现WUST 1002: 哈夫曼树
[问题描述] 根据给定的若干权值可以构造出一颗哈夫曼树.构造的哈夫曼树可能不唯一,但是按照下面的选取原则所构造出来的哈夫曼树应该是唯一的. (1)每次选取优先级最低的两个结点,优先级最低的作为左子树, ...
- java实现第三届蓝桥杯排日程
排日程 [编程题](满分34分) 某保密单位机要人员 A,B,C,D,E 每周需要工作5天,休息2天. 上级要求每个人每周的工作日和休息日安排必须是固定的,不能在周间变更. 此外,由于工作需要,还有如 ...
- java实现第六届蓝桥杯分机号
分机号 X老板脾气古怪,他们公司的电话分机号都是3位数,老板规定,所有号码必须是降序排列,且不能有重复的数位.比如: 751,520,321 都满足要求,而, 766,918,201 就不符合要求. ...
- java代码(1)---Java8 Lambda
Lambda 一.概述 1.什么是Lambda表达式 //1.不需要参数,返回值为5 () -> 5 //2.接收一个参数(数字类型),返回其2倍的值 x -> 2 * x //3. ...
- PAT甲级 Reversible Primes
描述 A reversible prime in any number system is a prime whose "reverse" in that number syste ...
- 关于uniapp获取当前距离屏幕顶部的距离
onPageScroll(e){ console.log(e); }
- java第三阶段作业总结
Java第三阶段总结 前言 到这里,Java课程学习进入了尾声,在这学习过程中,我学习到很多,也发现了自己的很多不足,这篇博客主要针对的是Java整门课程学习的总结. 课程收获 对整门课程的学习,我有 ...
- 实现简易版react中createElement和render方法
function createElement(type, config, children) { // 1. 创建一个对象 // 2.根据参数config修改这个对象 // 3.把children参数 ...
- Git中的core.autocrlf选项
项目的开发环境为Windows,在Linux环境下编译,使用Git进行版本控制. 在安装好Git和TortoiseGit后,从远端clone,遇到一个奇怪的问题,Shell脚本中的LF总是被替换成了C ...