深入浅出设计守则1

深入浅出设计守则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);

这样一来就可以打破巢状式的初始化写法


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章