对设计及重构的一点反思
本文是我对一个项目中一个小功能点的演进及重构过程的一点反思与心得。
背景:
本项目是一个电子商务类的网站,其中有个功能是在订单状态改变到某种状态后向客户发送通知短信的功能,短信及网关功能均已封装为组建的方式,我们直接调用即可。
为更清晰明白地说明与本主题相关的功能,在此我以一个控制台的程序方式说明代码的演进过程。
重构的演进过程:
最初我们是如大多数项目一样,为在规定的时间内完成相关功能点而努力奋斗着,这个功能点的主要代码如下:
static void SendSMS_V1(DataTable dt) { if (null == dt) return; for (int i = 0; i < dt.Rows.Count; i++) { var row = dt.Rows[i]; OrderStateEnum state = (OrderStateEnum)((int)row["OrderState"]); string template = string.Empty; switch (state) { case OrderStateEnum.UnConfirmed: template = "尊敬的{0},你好!你的订单已成功下达,请尽快付款以便配送。"; break; case OrderStateEnum.Confirmed: template = "尊敬的{0},你好!你的订单(订单号:{1})已被确认,请耐心等待。"; break; case OrderStateEnum.Cancel: template = "尊敬的{0},你好!你的订单(订单号:{1})已被取消,具体原因请上网查看。"; break; case OrderStateEnum.Finish: template = "尊敬的{0},你好!你的订单(订单号:{1})已完成,网站感谢您的支持与配合,欢迎再次光临。"; break; default: break; } string content = string.Format(template, row["CustomerName"], row["OrderID"]); SendSMS.Send(row["phone"].ToString(), content); } }
在项目上线初期,这段代码工作良好。
在运营一段时间后运营部门同事陆续提出要在某些地方将产品名称给加上去,在这一改动过程中,我发现代码没有和数据相分离,再者如要增加一个订单状态后增加相应的短信提示或者取消某一个状态的短信提示,这个改动过程有点麻烦。于是初步想到将短信的内容放到配置文件中,在调用的时候读取订单状态对应的配置文件然后格式化即可。如读取的内容为空或者该文件不存在则跳过不发送。主要代码如下:
#region v2 内容和数据分离 static void SendSMS_V2(DataTable dt) { if (null == dt) return; for (int i = 0; i < dt.Rows.Count; i++) { var row = dt.Rows[i]; string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SMSTemplates", "V2", row["OrderState"].ToString() + ".txt"); var template = FileHelper.Read(path); if (!string.IsNullOrEmpty(template)) { string content = string.Format(template, row["CustomerName"], row["OrderID"], row["ProductName"]); SendSMS.Send(row["phone"].ToString(), content); } } } #endregion
尊敬的{0},你好!你的订单(订单号:{1})已被确认,请耐心等待。
随着运营进行,运营部门不断地提出将某些字段在某些地方显示, string.Format 后的参数不断地增加,每次改后都要将整个过程重新测试一通,很是让人头疼!我的想法是开发这边提供一个数据标签列表,同时在后台提供操作界面将短信管理模板让运营同事自己去修改,不用每次都找我们?有了以上想法,使用一个自定义的 formatter :
public class IndexerNamedFormatter : IFormatProvider, ICustomFormatter { public IndexerNamedFormatter() { } public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) return this; throw new TypeAccessException("不匹配的类型。"); } public string Format(string format, object arg, IFormatProvider formatProvider) { if (null == arg) throw new ArgumentNullException("参数 arg 不能为 null"); int indexer = 0; bool isIndexed = int.TryParse(format, out indexer); //如是 datarow if (arg is System.Data.DataRow) { return GetStringFromDataRow(format, arg, indexer, isIndexed); } //如是 datareader 之类的 if (arg is System.Data.IDataRecord) { GetStringFromIDataRecord(format, arg, indexer, isIndexed); } return string.Empty; ; } private static void GetStringFromIDataRecord(string format, object arg, int indexer, bool isIndexed) { var dr = (System.Data.IDataRecord)arg; string.Format("{0}", isIndexed ? dr[indexer] : dr[format]); } string GetStringFromDataRow(string format, object arg, int indexer, bool isIndexed) { var row = (System.Data.DataRow)arg; return string.Format("{0}", isIndexed ? row[indexer] : row[format]); }
#region v3 使用自定义标签 static void SendSMS_V3(DataTable dt) { if (null == dt) return; IndexerNamedFormatter formatter = new IndexerNamedFormatter(); for (int i = 0; i < dt.Rows.Count; i++) { var row = dt.Rows[i]; string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SMSTemplates", "V3", row["OrderState"].ToString() + ".txt"); var template = FileHelper.Read(path); if (!string.IsNullOrEmpty(template)) { string content = string.Format(formatter, template, row); SendSMS.Send(row["phone"].ToString(), content); } } } #endregion
尊敬的{0:CustomerName},你好!你的订单(订单号:{0:OrderID},产品:产品{0:productname})已被确认,请耐心等待。
将数据字段取出来后写成一个标签列表文档提供给运营人员后,从此关于这一块的修改要求安静了。
以上只是一个小小设计技巧,这也让我明白需求的准确把握与挖掘是何等地重要!往往客户今天说要这样,明天要那样,大概很多人都在抱怨:你们真麻烦!但在需求不断地出现时我们是不是在修改的时候也反思下是不是我们未准确把握他们所想要的功能而让功能设计出了点问题?当然我也不崇尚一开始就大谈设计,过度设计要付出大量的时间成本和可能导致实现的复杂度的增加。
一点疑问:
通过查阅文档我一直感觉 arg is System.Data.DataRow 这种实现方式很是别扭,为什么索引器不定义为一个接口 ?有谁有更好的实现方法麻烦告知,谢谢!
感谢您的阅读!文中所涉及到的代码可从此下载。