意图
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
结构
适用性
- 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为;
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。
效果
- 把不同状态(State)的相关行为封装成一个个独立的对象,也便于以后增加新的状态和转换;
- 代码结构更加清晰,状态(State)之间的转换意图更为明确;
- 当状态(State)只是以编码类型做区分时,状态对象可以被共享。
思考
- 状态之间的切换由谁执行?
- Context 负责调用当前状态的行为并更改当前状态;
- State负责履行自身的行为并更改Context当前状态。
- 状态之间的转换关系也可以使用表查询(配置)方式实现;
- 状态若存储有大量信息,频繁切换会产生大量的创建和销毁的开销;
示例
设计一个熔断器(CircuitBreaker)提供系统过载保护,防止应用程序不断地尝试执行可能会失败的操作。熔断器可以使用状态机来实现,内部模拟以下几种状态:
- 闭合(closed)状态
对应用程序的请求能够直接引起方法的调用。代理类维护了最近调用失败的次数,如果某次调用失败,则使失败次数加1。如果最近失败次数超过了在给定时间内允许失败的阈值,则代理类切换到断开(Open)状态。此时代理开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误。
- 断开(Open)状态
在该状态下,对应用程序的请求会立即返回错误响应。 - 半断开(Half-Open)状态
允许对应用程序的一定数量的请求可以去调用服务。如果这些请求对服务的调用成功,那么可以认为之前导致调用失败的错误已经修正,此时熔断器切换到闭合状态(并且将错误计数器重置);如果这一定数量的请求有调用失败的情况,则认为导致之前调用失败的问题仍然存在,熔断器切回到断开方式,然后开始重置计时器来给系统一定的时间来修正错误。半断开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次拖垮。
实现(C#)
熔断器的状态转换图,根据要求设计如下:
using System;
public abstract class State
{
protected readonly CircuitBreaker breaker;
protected State (CircuitBreaker breaker)
{
this.breaker = breaker;
}
public abstract void Handle(Action handler);
}
public sealed class ClosedState : State
{
private int failureCount = 0;
private DateTime failureExpiredUtc;
public ClosedState(CircuitBreaker breaker) : base(breaker)
{
this.failureExpiredUtc= DateTime.UtcNow.Add(breaker.FailureExpired);
}
public override void Handle(Action handler)
{
try
{
handler();
}
catch
{
// 超时,重新计数
if(failureExpiredUtc < DateTime.UtcNow)
{
// reset
this.failureExpiredUtc= DateTime.UtcNow.Add(breaker.FailureExpired);
this.failureCount = 1;
}
else
{
this.failureCount ++;
}
//Console.WriteLine("failureCount:{0}", this.failureCount);
// 在限定的时间内,错误次数达到一定阀值
if(this.failureCount >= this.breaker.FailureCountThreshold && this.failureExpiredUtc > DateTime.UtcNow)
{
this.breaker.MoveToOpen();
}
}
}
public override string ToString()
{
return "闭合";
}
}
public sealed class HalfOpenState : State
{
private int successCount = 0;
public HalfOpenState(CircuitBreaker breaker) : base(breaker) {}
public override void Handle(Action handler)
{
try
{
handler();
this.successCount ++;
if(this.successCount >= this.breaker.SuccessCountThreshold)
{
this.breaker.MoveToClosed();
}
}
catch
{
this.breaker.MoveToOpen();
}
}
public override string ToString()
{
return "半开";
}
}
public sealed class OpenState : State
{
private System.Threading.Timer timer;
public OpenState(CircuitBreaker breaker) : base(breaker)
{
this.timer = new System.Threading.Timer(this.MoveToHalfOpen,this, breaker.DelayTime, breaker.DelayTime);
}
public override void Handle(Action handler)
{
Console.WriteLine("无法获取远程资源.");
}
public void MoveToHalfOpen(object state)
{
this.breaker.MoveToHalfOpen();
}
public override string ToString()
{
return "断开";
}
}
public sealed class CircuitBreaker
{
public int FailureCountThreshold { get; private set; }
public int SuccessCountThreshold { get; private set; }
public TimeSpan DelayTime { get; private set; }
public TimeSpan FailureExpired { get; private set; }
public State State { get; private set; }
public CircuitBreaker(int failureCountThreshold, int successCountThreshold, int delaySeconds, int failureSeconds)
{
this.FailureCountThreshold = failureCountThreshold;
this.SuccessCountThreshold = successCountThreshold;
this.DelayTime = TimeSpan.FromSeconds(delaySeconds);
this.FailureExpired = TimeSpan.FromSeconds(failureSeconds);
this.State = new ClosedState(this);
}
public void Execute(Action exec)
{
this.State.Handle(exec);
}
public void MoveToHalfOpen()
{
this.State = new HalfOpenState(this);
}
public void MoveToOpen()
{
this.State = new OpenState(this);
}
public void MoveToClosed()
{
this.State = new ClosedState(this);
}
}
public class App
{
public static void Main(string[] args)
{
// 1. 关闭: 在10秒内失败次数达到3次,状态调整为「断开」
// 2. 断开: 15秒后,状态调整为「半开」
// 3. 半开: 连续成功5次,状态调整为「闭合」
CircuitBreaker breaker = new CircuitBreaker(3,5,15,10);
breaker.Execute(() => Console.WriteLine("成功获得远程资源,当前状态:「{0}」", breaker.State));
// 连续三次失败
breaker.Execute(() => { throw new Exception(); });
breaker.Execute(() => { throw new Exception(); });
breaker.Execute(() => { throw new Exception(); });
Console.WriteLine("连续3次失败后,当前状态:「{0}」", breaker.State);
// 等待15秒
Console.Write("等待15秒,当前状态:「{0}」... ", breaker.State);
System.Threading.Thread.Sleep(15500);
Console.WriteLine("完毕! 当前状态:「{0}」", breaker.State);
// 连续成功5次
breaker.Execute(() => Console.WriteLine("1.成功获得远程资源,当前状态:「{0}」", breaker.State));
breaker.Execute(() => Console.WriteLine("2.成功获得远程资源,当前状态:「{0}」", breaker.State));
breaker.Execute(() => Console.WriteLine("3.成功获得远程资源,当前状态:「{0}」", breaker.State));
breaker.Execute(() => Console.WriteLine("4.成功获得远程资源,当前状态:「{0}」", breaker.State));
breaker.Execute(() => Console.WriteLine("5.成功获得远程资源,当前状态:「{0}」", breaker.State));
Console.WriteLine("连续成功5次后,当前状态:「{0}」", breaker.State);
}
}
// 控制台输出:
// 成功获得远程资源,当前状态:「闭合」
// 连续3次失败后,当前状态:「断开」
// 等待15秒,当前状态:「断开」... 完毕! 当前状态:「半开」
// 1.成功获得远程资源,当前状态:「半开」
// 2.成功获得远程资源,当前状态:「半开」
// 3.成功获得远程资源,当前状态:「半开」
// 4.成功获得远程资源,当前状态:「半开」
// 5.成功获得远程资源,当前状态:「半开」
// 连续成功5次后,当前状态:「闭合」