敏捷开发松结对编程系列:L型代码结构案例StatusFiltersDropdownList(中)

时间:2023-03-09 17:19:14
敏捷开发松结对编程系列:L型代码结构案例StatusFiltersDropdownList(中)

这是松结对编程的第22篇(专栏目录)。

接前文

业务代码

比较长,基本上就是看被注释隔开的三大段,先显示状态群筛选链接,然后是单个状态筛选,然后是显示下拉框的当前选中项,最后显示下拉框。
        public static MvcHtmlString StatusFiltersDropdownList(WebViewPage page)
{
var allStatuses = Status.AllStatuses().ToList();
const string key = "statusIds";
var currentStatusIds = page.ParameterOf(key); //Status groups filters on the top.
var linksLeft = new Dictionary<string, IEnumerable<Status>>
{
{"所有状态", allStatuses},
{"所有正常", allStatuses.Where(i => i.IsNormal)},
{"所有开放", allStatuses.Where(i => !i.IsClosed)},
{"所有故事板", allStatuses.Where(i => i.IsDisplayedOnKanban)},
};
var linksListLeft = AddToList(page, key, currentStatusIds, linksLeft); var linksRight1 = new Dictionary<string, IEnumerable<Status>>
{
{"0", null},
{"所有已放弃或推迟", allStatuses.Where(i => !i.IsNormal)},
{"所有关闭", allStatuses.Where(i => i.IsClosed)},
{"所有非故事板", allStatuses.Where(i => !i.IsDisplayedOnKanban)}
};
var linksListRight1 = AddToList(page, key, currentStatusIds, linksRight1); //Single status filters on the bottom.
linksListLeft.Add(new MvcHtmlString("<hr/>"));
linksListLeft.AddRange(allStatuses.Where(i => i.Value >= 0)
.Select(status => status.Link(outerLink: page.MergeParameter(key, "_" + status.ID + "_"), title: status.Value.ToString(),
displayAsBoldText: "_" + status.ID + "_" == currentStatusIds)));
linksListLeft.Add(new MvcHtmlString("<hr/>"));
linksListLeft.AddRange(allStatuses.Where(i => i.Value < 0)
.Select(status => status.Link(outerLink: page.MergeParameter(key, "_" + status.ID + "_"), title: status.Value.ToString(),
displayAsBoldText: "_" + status.ID + "_" == currentStatusIds))); //Current value text
var currentValueText = "";
MvcHtmlString currentValue = null;
currentValueText = linksLeft.SingleOrDefault(i => i.Value != null && currentStatusIds == i.Value.Aggregate<Status, string>(null, (current, status) => current + "_" + status.ID + "_"))
.Key ?? currentValueText;
currentValueText = linksRight1.SingleOrDefault(i => i.Value != null && currentStatusIds == i.Value.Aggregate<Status, string>(null, (current, status) => current + "_" + status.ID + "_"))
.Key ?? currentValueText;
if (!string.IsNullOrEmpty(currentValueText))
{
currentValue = new MvcHtmlString("<b>" + currentValueText + "</b>");
}
else
{
var status = allStatuses.SingleOrDefault(i => currentStatusIds == "_" + i.ID + "_");
currentValue = status != null
? MFCUI.Image(status.Title, "/MFC/Items/STAT/STAT16.png", showText: true, cssClassOfText: "bold", textColor: status.Color)
: new MvcHtmlString("<b style=\"color: #169; \">请选择</b>");
} var ddl = MFCUI.DropdownListHtml(page, currentValue, linksListLeft, linksListRight1, null, "200px");
return new MvcHtmlString("状态:" + ddl);
}

有一个被调用的函数在这里:

基于上篇文章中提到的“业务代码设计原则”,有几个相关的点(和上篇中的顺序有差异):
1. 所有无关业务的东西都不在这里实现
比如MFCUI.ImageLink/Link, page.ParameterOf()/MergeParameter(从URL中取出或放入某个参数的数值),MFCUI.DropdownListhtml(显示下拉框)等,都与业务无关,只调用,不散开写。
这些函数都是公共使用的函数,应该早就写好,或这次写好留待后用。
2. 不写重复代码
某些函数入AddToList()是只在这个函数中调用的,没人会复用,但在这里本身被调用多次是重复代码,则写一个Private的函数放下面就可以了:
        private static List<MvcHtmlString> AddToList(WebViewPage page, string key, string currentStatusIds, Dictionary<string, IEnumerable<Status>> linkList)
{
var linksList = new List<MvcHtmlString>();
foreach (var link in linkList)
{
var statusIds = link.Value == null ? "" : link.Value.Aggregate<Status, string>(null, (current, status) => current + "_" + status.ID + "_");
linksList.Add(link.Value == null
? null
: MFCUI.Link(link.Key, page.MergeParameter(key, statusIds), displayAsBoldText: statusIds == currentStatusIds));
}
return linksList;
}

这样封装的代码没有复用价值,但可以减少维护时的阅读量。

3. 业务代码中只描述做什么,不描述怎么做。
乍一看这一大段代码,肯定不可能看懂“是怎么实现的”,其实,这就对了。
因为这样反而分离了要做什么和怎么做两块,党只关心要做什么(比如业务发生变更)时,就只看做什么的代码;想知道怎么做的时候再说。
下篇会通过一次“业务变更”来展示如何从代码分离中受益。

关于注释

读者可能注意到代码中注释很少,培训中也经常有人问到“注释应该占多大比例”的问题,这个问题要回答好还比较复杂。
注释的多少以能看懂代码能维护为准。要看懂代码,除了注释之外,命名、布局、函数封装等都有利于读懂代码,而在L型代码结构中,代码的使用次数是另外一个帮助复用、阅读、维护代码的方法。
火星人现在基本版的文件数只有622个(cs + cshtml),代码只有7500行(仅计算C#,由VS2012自带功能统计),而上面提到的MFCUI.ImageLink/Link一共被使用过280次/220次,parameterOf/MergeParameter 79次/62次,DropdownListHtml()33次。
在这种情况下,任何新手/新人想使用它们前,第一个想到的绝不是阅读注释或文档,而是先在界面上找一个自己想要的实例做示例,然后拷贝粘贴修改参数就可以了。最初的示例是由师傅写出来的(可复用的代码也是他写的),而之后大家不断参考使用,一点点就学会了。因此这些函数不但使用时很少有注释,函数接口定义处也几乎没有注释。
但是,若L型代码结构被复用的范围超出了示例代码的范围,情况就不同了。这时候就需要写点注释了。(所以说“多少注释才好”这个问题有答案,但比较复杂)
无论如何,更有参考价值的无疑是示例而非注释。

业务逻辑代码

刚才说的全是界面上的东西,底层怎么实现筛选的呢?
其实statusIds原封不动地传递了大约5层后到达这里:
            subItems = statusIds == null
? ItemsUnder(repository, rootID, includeHidden: includeHidden)
: ItemsUnder(repository, rootID, includeHidden: includeHidden).Where(i => i.Status != null && statusIds.Contains("_" + i.Status.ID + "_"));

这是它唯一被真正使用的地方。尽管我们会对很多东西进行筛选,但都不需要写任何一行代码了,因为这行代码位于“很多东西”的底层。日后如果技术上有什么变动,就修改它就可以了。

实现业务变更

现在我们需要实现“晚于某个状态的所有工作项”筛选器(比如自动集成时,只运行“所有晚于故事板上已完成的故事,以及部署中、部署完毕的故事”的测试用例,这个是我们自己的实际需求,写本博客时还没做),下篇文章将通过增加这个新业务,来展示L型代码 结构如何使得只通过业务代码而无需阅读技术代码就进行维护变为可能的。
(写下篇文章前需要写一些代码,会晚0.5~1小时左右发布)
待续