深入浅出设计守则1
客户需求
小明的公司提供一套肯特机系统, 客户希望系统能够提供消费上限机制, 客户希望系统能够提供至少三种设定, 设定规则如下:
1天内最多消费上限 (例如1000 元, 或是无上限)7天内最多消费上限 (例如6000 元, 或是无上限)30天内最多消费上限 (例如20000 元, 或是无上限)客户设定储存之后,
24小时内不能修改设定,但是可以把消费上限金额往下调整(例如1 天消费上限金额1000 元往下调为900元), 或者是原本是无上限金额调整为有上限金额.超过24 小时之后, 可以自由调整各种消费上限金额
开始实作
于是小明按照需求设计了一个储存设定的物件
public class ChargeLimitConfig{ public ChargeLimitConfig(int limit1Day, int limit7Day, int limit30Day, DateTime lastModified) { Day1Limit = new DepositLimit(limit1Day); Day7Limit = new DepositLimit(limit7Day); Day30Limit = new DepositLimit(limit30Day); LastModified = lastModified; } public DateTime LastModified { get; } public DepositLimit Day1Limit { get; } public DepositLimit Day7Limit { get; } public DepositLimit Day30Limit { get; }}
然后针对"消费金额" 做了一个物件,
public class ChargeLimit{ public ChargeLimit(int amount) { Amount = amount; } private bool IsUnlimited => Amount == 0; private bool Islimited => Amount > 0; private int Amount { get; } public bool IsMoreThan(ChargeLimit other) { return IsIncreaseMoreFrom(other) || ChangeFromLimitedToUnlimited(other); } private bool ChangeFromLimitedToUnlimited(ChargeLimit other) { return (IsUnlimited && other.Islimited); } private bool IsIncreaseMoreFrom(ChargeLimit other) { return Islimited && other.Islimited && Amount > other.Amount; }}
特别注意的地方是,
里面程式码将上限金额为0 时, 表示为无上限
接着小明写了一个检查设定储存验证物件, 企图用Validate 方法来验证是否允许客户修改设定内容
public class ChargeLimitUpdateValidator{ private readonly ChargeLimitConfig _newConfig; private readonly ChargeLimitConfig _oldConfig; public ChargeLimitUpdateValidator(ChargeLimitConfig newConfig, ChargeLimitConfig oldConfig) { _newConfig = newConfig; _oldConfig = oldConfig; } public bool Validate() { if ((IncreaseDay1LimitAmount() || IncreaseDay7LimitAmount() || IncreaseDay30LimitAmount()) && ModifiedWithinRestrictionTimespan()) return false; return true; } private bool IncreaseDay1LimitAmount() { return _newConfig.Day1Limit.IsMoreThan(_oldConfig.Day1Limit); } private bool IncreaseDay7LimitAmount() { return _newConfig.Day7Limit.IsMoreThan(_oldConfig.Day7Limit); } private bool IncreaseDay30LimitAmount() { return _newConfig.Day30Limit.IsMoreThan(_oldConfig.Day30Limit); } private bool ModifiedWithinRestrictionTimespan() { return _newConfig.LastModified - _oldConfig.LastModified < new TimeSpan(24, 0, 0); }}
在系统中, 小明就用下面程式码来检查是否可以让客户更动设定
var updateValidator = new ChargeUpdateValidator(newConfig, oldConfig);if( updateValidator.Validate() ){ //Save newConfig to Database}
可以改进的地方
观察上述的程式码, 可以发现到
商业逻辑规则(客户设定修改储存限制), 散落在ChargeLimit 物件和ChargeLimitUpdateValidator 物件. 如果要新增规则, 就很难维护修改.
有重複的程式码
IncreaseDay1LimitAmount()IncreaseDay7LimitAmount()IncreaseDay30LimitAmount()
如果增加新的天数消费上限, 例如(15天内的消费金额),要修改的地方太多.
设计守则
找出程式码可能更动的地方, 把它们独立出来,
不要和不太改动的地方放在一起.
从上述的需求可以看出可能更动的地方
N天内的上限金额设定(1天,7天,30天)商业逻辑规则--消费金额上限修改规则24小时内的规则1 金额大小只能往下修24小时内的规则2 无上限金额可以改成有上限金额超过24小时的规则如果你有大量的资料型别的资料, 就考虑可能将资料组织起来,形成一个物件类.
故看到这些1 天,7 天,30 天这些设定的资料, 就该考虑把这些设定资料集合起来变成物件类.
集合资料的方法有很多种, 资料结构可以用阵列也可以利用集合.
在这案例, 可以考虑用Dictionary
public class ChargeLimit{ public int PeriodDays { get; set; } public int Amount { get; set; }}public class ChargeLimitsConfig{ public Dictionary<int, ChargeLimit> PeriodDayLimits { get; set; } public DateTime LastModifiedTime { get; set; }}
接下来看ChargeLimitUpdateValidator::Validate() 这个函数, 这个函数需要用数条规则来验证使用者的参数内容(ChargeLimitsConfig)是否允许被修改
public class ChargeLimitUpdateValidator{ public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig) { //这里需要实作数条规则来验证 }}
设计守则 当看到数条规则的时候, 我们应该试着考虑设计一个验证模式(Validation Pattern)
设计验证应该满足下面条件
声明(Declarative) - 我想要有一个验证器(validator)能够方便给开发人员去执行.只有一个单点故障 - 我不想要维护许多变数(variables)而只是为了检查输入是否满足规则.容易扩展(Extendable) - 最后我希望增加规则的时候, 开发人员能够容易新增.所以我们要建立验证介面, 这个验证方法需要"旧的ChargeLimit", "修改时间", "新的ChargeLimit", 以及"新的修改时间"四个参数
public interface IChargeLimitUpdateRule{ bool Validate(ChargeLimit oldLimit, DateTime lastModifyiedTime, ChargeLimit newLimit, DateTime modifyTime);}
设计守则 函数(function)的参数应该尽可能地很少, 3个或更多参数对于一个函数来说太多了
所以我们要建立物件来包装这四个参数
public class ValidateChargeLimitArgs{ public ChargeLimit OldLimit { get; set; } public DateTime LastModifiedTime { get; set; } public ChargeLimit NewLimit { get; set; } public DateTime ModifyTime { get; set; }}
经过上面的重构, IChargeLimitUpdateRule 宣告如下
public interface IChargeLimitUpdateRule{ void Handle(ValidateChargeLimitArgs args);}
设计守则 规则实作 - 只要发现不符合规则, 我们就直接丢例外就好
首先设计规则1是 -- 不允许金额增加
public class CannotIncreaseRule : IChargeLimitUpdateRule{ public void Handle(ValidateChargeLimitArgs args) { if (!args.OldLimit.IsUnlimit && !args.NewLimit.IsUnlimit){ if (args.OldChargeLimitSetting.Amount < args.NewChargeLimitSetting.Amount) { throw new ValidationException(); } } }}
接着设计规则2, 不允许有上限金额改成无上限金额
public class CannotLimitToUnlimitRule : IChargeLimitUpdateRule{ public void Handle(ValidateChargeLimitArgs args) { if (args.OldLimit.Islimit && args.NewLimit.IsUnlimit ) { throw new ValidationException(); } }}
接着设计规则3, 24小时内要套用规则1 和规则2 , 这时候...
设计守则 多用组合, 少用继承.
原因如下
某些语言不支援多继承 - 这个限制导致只能其继承一个基类. 如果想赋予一个类多个功能, 选择只有两个: 介面和组合.组合让测试更容易 - 单元测试的时候, 我们需要mock 资料. 使用继承时, 我们不得不mock 基类. 而使用组合, 则简单很多, 我们可以通过注入不同的实例(instance)来方便的完成mock .继承不利于封装 - 在继承中, 如果子类依赖父类的行为, 子类将变得脆弱. 因为一旦父类行为发生变化, 子类也将受到影响.故我们设计如下
public class In24HrRule : IChargeLimitUpdateRule{ public void Handle(ValidateChargeLimitArgs args) { if (!TimeHelper.IsIn24Hr(args.LastModifiedTime, args.ModifyTime)) { return; } var rule1 = new CannotIncreaseRule(); rule1.Handle(args); var rule2 = new CannotLimitToUnlimitRule(); rule2.Handle(args); }}
回到ChargeLimitUpdateValidator::Validate() 的地方, 开始建立规则验证实例并呼叫
public class ChargeLimitUpdateValidator{ public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig) { var rule = new In24HrRule(); rule.Handle(xxx); }}
我们需要取得所有的使用者新旧设定资料, 故我们实作方法来做这件事情
private static IEnumerable<ValidateChargeLimitArgs> GetAllChargeLimits(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig){ var q1 = from tb1 in oldConfig.PeriodDayLimits.Values join tb2 in newConfig.PeriodDayLimits.Values on tb1.PeriodDays equals tb2.PeriodDays select new ValidateChargeLimitArgs() { OldLimit = tb1, LastModifiedTime = oldConfig.LastModifiedTime, NewLimit = tb2, ModifyTime = DateTime.Now }; return q1;}
取得所有使用者新旧设定资料之后, 并且一个一个餵给规则去检查,
然后用try...catch 方式检查规则是否有丢出验证例外, 如果有例外错误, 就回传false
public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig){var allChargeLimits = GetAllChargeLimits(oldConfig, newConfig); var rule = new In24HrRule(); try { foreach (var item in allChargeLimits) { rule.Handle(item); } return true; } catch(ValidationException) { return false; }}
为了程式码更乾净一点, 我们可以把try...catch 抽取出去变成一个方法
private static bool HandleAllChargeLimitsByRule(IEnumerable<ValidateChargeLimitArgs> chargeLimits, IChainOfResponsibilityHandler<ValidateChargeLimitArgs> rule){ try { foreach (var limit in chargeLimits) { rule.Handle(limit); } return true; } catch { return false; }}
最后我们的验证器方法变成如下
public bool Validate(ChargeLimitsConfig oldConfig, ChargeLimitsConfig newConfig){ var allChargeLimits = GetAllChargeLimits(oldConfig, newConfig); var rule = new In24HrRule(); return HandleAllChargeLimitsByRule(allChargeLimits, rule);}
这样一来程式码不就看起来清晰漂亮了吗?
接下来如果想要加更多的规则, 我们更可以用责任链模式(Chain Of Responsibility Pattern)来设计规则.
责任链模式的特色 - 当这个物件没有要处理或是处理完的时候, 能够将这个请求(request) 传递给下一个物件继续处理.
责任链物件的建构元(constructor) 通常都会有一个参数(下一个处理物件是谁)
public class MyHandler : IChainOfResponsibilityHandler{ IChainOfResponsibilityHandler _nextHandler; public MyHandler(IChainOfResponsibilityHandler nextHandler) { //储存下一个处理物件 _nextHandler = nextHandler; } public void Handle(object request) { //处理完我的事情之后 ... //将这个请求(request) 传递给下一个处理物件继续处理 _nextHandler?.Handle(request); }}
所以CannotIncreaseRule 规则的程式码就会修改为如下
public class CannotIncreaseRule : IChargeLimitUpdateRule{ IChargeLimitUpdateRule _nextHandler; public CannotIncreaseRule(IChargeLimitUpdateRule nextHandler) { _nextHandler = nextHandler; } public void Handle(ValidateChargeLimitArgs args) { if (!args.OldLimit.IsUnlimit && !args.NewLimit.IsUnlimit){ if (args.OldChargeLimitSetting.Amount < args.NewChargeLimitSetting.Amount) { throw new ValidationException(); } } _nextHandler?.Handle(args); }}
另一条规则也修改如下
public class CannotLimitToUnlimitRule : IChargeLimitUpdateRule{ IChargeLimitUpdateRule _nextHandler; public CannotLimitToUnlimitRule(IChargeLimitUpdateRule nextHandler) { _nextHandler = nextHandler; } public void Handle(ValidateChargeLimitArgs args) { if (args.OldLimit.Islimit && args.NewLimit.IsUnlimit ) { throw new ValidationException(); } _nextHandler?.Handle(args); }}
然后在程式中使用这2 个规则的时候, 就可以这样使用
var rules = new CannotIncreaseRule(new CannotLimitToUnlimitRule());rules.Handle(request);
到这里, 你会发现....
设计守则 不要有重複的程式码
刚刚两个规则中你可以发现有重複的程式码(duplicate code).
设计守则 应当减少巢状式的写法
另外你发现初始化那一串物件的地方, 也是一种坏味道(巢状式的写法). 我们也能够用其他方式来封装.
所以我们把重複的程式码抽出来变成另一个类. 然后新增一个方法SetNext 来指定下一个处理物件.
public abstract class BaseRule : IChargeLimitUpdateRule{ IChargeLimitUpdateRule _nextHandler; public IChargeLimitUpdateRule SetNext(IChargeLimitUpdateRule handler) { this._nextHandler = handler; return this._nextHandler; } public virtual void Handle(ValidateChargeLimitArgs args) { _nextHandler?.Handle(args); }}
然后把刚刚两个规则改写为如下
public class CannotIncreaseRule : BaseRule, IChargeLimitUpdateRule{ public override void Handle(ValidateChargeLimitArgs args) { if (args.OldChargeLimitSetting.Amount < args.NewChargeLimitSetting.Amount) { throw new ValidationException(); } base.Handle(args); }}
另外一个规则也改写如下
public class CannotLimitToUnlimitRule : BaseRule, IChargeLimitUpdateRule{ public override void Handle(ValidateChargeLimitArgs args) { if (args.OldLimit.Islimit && args.NewLimit.IsUnlimit ) { throw new ValidationException(); } base.Handle(args); }}
接着写一个初始化一串Chain 的辅助方法
public static class ChainOfResponsibility { public static IChargeLimitUpdateRule Chain(params IChargeLimitUpdateRule[] handlers) { var first = handlers.First(); var chain = first; foreach (var handler in handlers.Skip(1)) { chain = chain.SetNext(handler); } return first; }}
同样地在程式中初始化Chain 这2 个规则以上的时候, 就可以这样使用
var rules = ChainOfResponsibility.Chain( new CannotIncreaseRule(), new CannotLimitToUnlimitRule(), new xxxRule1(), new xxxRule2());rules.Handle(request);
这样一来就可以打破巢状式的初始化写法