无废话C#设计模式之十六:State
意图
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
场景
我们在制作一个网上书店的网站,用户在书店买了一定金额的书后可以升级为银会员、黄金会员,不同等级的会员购买书籍有不同的优惠。你可能会想到可以在User类的BuyBook方法中判断用户历史消费的金额来给用户不同的折扣,在GetUserLevel方法中根据用户历史消费的金额来输出用户的等级。带来的问题有三点:
l 不用等级的用户给予的优惠比率是经常发生变化的,一旦变化是不是就要修改User类呢?
l 网站在初期可能最高级别的用户是黄金会员,而随着用户消费金额的累计,我们可能要增加钻石、白金等会员类型,这些会员的折扣又是不同的,发生这样的变化是不是又要修改User类了呢?
l 抛开变化不说,User类承担了用户等级判断、购买折扣计算等复杂逻辑,复杂的User类代码的可维护性会不会很好呢?
由此引入State模式,通过将对象和对象的状态进行分离,把对象状态的转化以及由不同状态产生的行为交给具体的状态类去做,解决上述问题。
示例代码
using System; using System.Collections.Generic; using System.Text; namespace StateExample { classProgram { staticvoid Main(string[] args) { User user = newUser("zhuye"); user.BuyBook(2000); user.BuyBook(2000); user.BuyBook(2000); user.BuyBook(2000); } } classUser { privateUserLevel userLevel; publicUserLevel UserLevel { get { return userLevel; } set { userLevel = value; } } privatestring userName; privatedouble paidMoney; publicdouble PaidMoney { get { return paidMoney; } } public User(string userName) { this.userName = userName; this.paidMoney = 0; this.UserLevel = newNormalUser(this); } publicvoid BuyBook(double amount) { Console.WriteLine(string.Format("Hello {0}, You have paid ${1}, You Level is {2}.", userName, paidMoney, userLevel.GetType().Name)); double realamount = userLevel.CalcRealAmount(amount); Console.WriteLine("You only paid $" + realamount + " for this book."); paidMoney += realamount; userLevel.StateCheck(); } } abstractclassUserLevel { protectedUser user; public UserLevel(User user) { this.user = user; } publicabstractvoid StateCheck(); publicabstractdouble CalcRealAmount(double amount); } classDiamondUser : UserLevel { public DiamondUser(User user) : base(user) { } publicoverridedouble CalcRealAmount(double amount) { return amount * 0.7; } publicoverridevoid StateCheck() { } } classGoldUser : UserLevel { public GoldUser(User user) : base(user) { } publicoverridedouble CalcRealAmount(double amount) { return amount * 0.8; } publicoverridevoid StateCheck() { if (user.PaidMoney > 5000) user.UserLevel = newDiamondUser(user); } } classSilverUser : UserLevel { public SilverUser(User user) : base(user) { } publicoverridedouble CalcRealAmount(double amount) { return amount * 0.9; } publicoverridevoid StateCheck() { if (user.PaidMoney > 2000) user.UserLevel = newGoldUser(user); } } classNormalUser : UserLevel { public NormalUser(User user) : base(user) { } publicoverridedouble CalcRealAmount(double amount) { return amount * 0.95; } publicoverridevoid StateCheck() { if (user.PaidMoney > 1000) user.UserLevel = newSilverUser(user); } } } |
代码执行结果如下图:
代码说明
l User类型是环境角色。它的作用一是定义了客户端感兴趣的方法(购买书籍),二是拥有一个状态实例,它定义了用户的当前状态(普通会员、银会员、黄金会员还是钻石会员)。
l UserLevel类型是抽象状态角色。它的作用一是定义了和状态相关的行为的接口,二是拥有一个环境实例,用于在一定条件下修改环境角色的抽象状态。
l NormalUser、 SilverUser、GoldUser以及DiamondUser就是具体状态了。它们都实现了抽象状态角色定义的接口。
l 为User转化UserLevel的操作是在各个具体状态中进行的。在这里可以看到这种状态的转化是有序列的,这样也只会有前后两个状态产生依赖。假设现在的会员系统中是没有钻石用户的,那么GoldUser的StateCheck()方法中应该是没有什么代码的,即使以后再要加DiamondUser类,我们也只需要修改GoldUser的SateCheck()方法,以根据一定的规则来为环境转化状态。
l 在这里,我们在环境中调用了StateCheck方法,在实际应用中,你可以根据需要在抽象状态中引入模版方法,对外公开这个模版方法,并且在模版方法中调用行为方法和转化状态的方法,当然,具体的行为还是由具体状态来实现的。
何时采用
l 从代码角度来说,如果一个类有多种状态,并且在类内部通过的条件语句判断的类状态来实现不同行为时候可以把这些行为单独封装为状态类。
l 从应用角度来说,如果一个对象有多种状态,如果希望把对象状态的转化以及由不同状态产生的行为交给具体的状态类去做,那么可以考虑状态模式。
实现要点
l 在环境角色中拥有状态角色的实例。
l 在状态角色中拥有环境角色的实例用于在具体状态中修改环境角色的状态。
l 状态对象之间的依赖可以通过加载外部配置的转化规则表等方法来消除。
l 状态模式和策略模式的主要区别是,前者的行为实现方式是由条件决定的,并且应当能不在客户端干预的情况下自己迁移到合适的状态,而后者的行为实现方式是由客户端选择的,并且能随时替换。
注意事项
l 过多的状态对象可能会增加系统负担,可以考虑把各种状态角色实现为无状态对象的享元,需要保存的额外状态由环境角色进行统一管理和处理。