0
点赞
收藏
分享

微信扫一扫

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


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

接上文,45分钟后……

新的筛选效果

现在需要在下拉框上加上两排新的筛选项(更早和更晚):


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


如果直接交给徒弟维护(或师傅离开后维护),可能会发生以下问题:

1. 以前的下拉框里边只有两排,现在要显示三排了,怎么办?

2. 三排了,框框变宽,显示不下了怎么办?

3. 以前的筛选条件是怎么写的?(希望改改就能写出新的来)


在非L型代码结构下,可能会寄希望与阅读DropdownListHtml接口:

public static MvcHtmlString DropdownListHtml(WebViewPage page, MvcHtmlString title, IEnumerable<MvcHtmlString> linksLeft,
IEnumerable<MvcHtmlString> linksRight1, IEnumerable<MvcHtmlString> linksRight2,
string width = "400px", string viewName = "~/Views/MFCControls/_HtmlDropdownList.cshtml", MvcHtmlString valueSpecial = null)

还好,接口算是明白,原来有个LinksRight2来处理右边的第二排;还有个width可以设置宽度。


即使看不太懂,在L型代码结构中,还有一些办法。(这也使得大家平时可以写成“看不太懂”的情况。虽然大家都希望能看懂,但要维护一套一看就懂的代码,代价很高)

如果师傅在,徒弟会问师傅该怎么办(如果他没看太懂);如果师傅不在,他会搜索全部DropdownListHtml以前使用的情况,并看到所有使用情况:

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

由于L型代码结构写底层的人同时也写上层(所以才叫“L”型,而不是“三”型或“川”型),所以所有底层技术都是随需开发的。因而如果支持三列链接显示,一定能找到一个以前使用过的例子。这些例子可以大大缓解注释、文档缺少的问题,甚至可以说比注释和文档更有效。

本人两年前学习C#以来,只买过1本C#的图书(后来因为几乎每看,就送人了),完全是按照一个大约130页的电子书上的案例完整开发了一遍,也就完成了入门工作。之后基本上遇到问题再搜Google或者Stackoverflow。L型代码结构其实想做的就是在企业内部完成类似的学习和使用过程。


完成后代码

新增或改动代码手工在前面加了//比较醒目,因为代码太长了,所以又补充了几句注释。

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.IsNormal)
.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.IsNormal)
.Select(status => status.Link(outerLink: page.MergeParameter(key, "_" + status.ID + "_"), title: status.Value.ToString(),
displayAsBoldText: "_" + status.ID + "_" == currentStatusIds)));

/*linksListRight1.Add(null);
linksListRight1.Add(MFCUI.Link("及更早", "#", textColor: "#EEE"));
inksListRight1.AddRange(allStatuses.Where(i => i.IsNormal).Skip(1)
.Select(status => MFCUI.Link("及更早", page.MergeParameter(key,
allStatuses.Where(s => s.IsNormal && s.Value <= status.Value).Aggregate<Status, string>(null, (current, s) => current + "_" + s.ID + "_")), title: status.Value.ToString(),
displayAsBoldText: currentStatusIds == allStatuses.Where(i => i.IsNormal && i.Value <= status.Value).Aggregate<Status, string>(null, (current, i) => current + "_" + i.ID + "_"))));
var linksListRight2 = new List<MvcHtmlString>();

linksListRight2.AddRange(new List<MvcHtmlString> { null, null, null, null, null});
linksListRight2.AddRange(allStatuses.Where(i => i.IsNormal).Take(allStatuses.Count(i => i.IsNormal) - 1)
.Select(status => MFCUI.Link("及更晚", page.MergeParameter(key,
allStatuses.Where(s => s.IsNormal && s.Value >= status.Value).Aggregate<Status, string>(null, (current, s) => current + "_" + s.ID + "_")), title: status.Value.ToString(),
displayAsBoldText: currentStatusIds == allStatuses.Where(i => i.IsNormal && i.Value >= status.Value).Aggregate<Status, string>(null, (current, i) => current + "_" + i.ID + "_"))));
linksListRight2.Add(MFCUI.Link("及更晚", "#", textColor: "#EEE"));*/

//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; //Is a group filter in left column.
currentValueText = linksRight1.SingleOrDefault(i => i.Value != null && currentStatusIds == i.Value.Aggregate<Status, string>(null, (current, status) => current + "_" + status.ID + "_"))
.Key ?? currentValueText; //Is a group filter in right column.
if (!string.IsNullOrEmpty(currentValueText))
{
currentValue = new MvcHtmlString("<b>" + currentValueText + "</b>");
}
else
{
var currentStatus = allStatuses.SingleOrDefault(i => currentStatusIds == "_" + i.ID + "_"); //Is a single status filter.
if (currentStatus != null)
currentValue = MFCUI.Image(currentStatus.Title, "/MFC/Items/STAT/STAT16.png", showText: true, cssClassOfText: "bold", textColor: currentStatus.Color);
else
{/*
currentStatus = allStatuses.SingleOrDefault(status => currentStatusIds == allStatuses.Where(i => i.IsNormal && i.Value <= status.Value)
.Aggregate<Status, string>(null, (current, i) => current + "_" + i.ID + "_")); //Is a equal-to or early-than filter.
if (currentStatus != null)
currentValue = new MvcHtmlString(MFCUI.Image(currentStatus.Title, "/MFC/Items/STAT/STAT16.png", showText: true, cssClassOfText: "bold", textColor: currentStatus.Color)
+ "<b> 及更早</b>");
else
{
currentStatus = allStatuses.SingleOrDefault(status => currentStatusIds == allStatuses.Where(i => i.IsNormal && i.Value >= status.Value)
.Aggregate<Status, string>(null, (current, i) => current + "_" + i.ID + "_")); //Is a equal-to or later-than filter.
if (currentStatus != null)
currentValue = new MvcHtmlString(MFCUI.Image(currentStatus.Title, "/MFC/Items/STAT/STAT16.png", showText: true, cssClassOfText: "bold", textColor: currentStatus.Color) +
"<b> 及更晚</b>");*/
else
currentValue = new MvcHtmlString("<b style=\"color: #169; \">请选择</b>");
}
}
}

var ddl = MFCUI.DropdownListHtml(page, currentValue, linksListLeft, linksListRight1, /*linksListRight2, "300px"*/);
return new MvcHtmlString("状态:" + ddl);
}


这个代码已经有点太长了,等下改成三个函数。因为三个函数都不是要被复用的(只在这里有用),会以private形式写在主函数下面并折叠收起来,有人看的时候才打开。

时间统计与L型代码结构的效率

其中本文中的维护45分钟(编写者,也是师傅本人维护)。

如果师傅本人1个月后维护,估计要花费1小时;如果徒弟一个月后在师傅指导下维护,估计要花费2小时;如果很久以后徒弟自己维护,估计要4小时。但这都是第一次维护所需的时间,在维护过一次之后,速度很快就能向师傅靠拢了。

或许有人会说“如果从头写一堆SQL语句,也未必用的了4小时”,的确如此。但所需的代码行数会大大超过现在的代码行数,对专用的代码的测试也会更消耗时间;而日后一旦更改业务或技术,成本就更不用说了。

更大范围的复用框架

刚才只是一个小的控件的复用,如果能在更大框架下实现L型代码结构,那么编写和维护软件会更加容易。

比如本文及之前两篇文章是关于“筛选用户故事”这个功能的,由于用户故事和测试用例在火星人中是基于共同基类实现的,因此只要加上下面代码中的注释文字(未使之醒目而注释),立刻就能实现测试用例的筛选了,一共用了2分钟(菜单是共享的不用动):

public ActionResult IndexTree(int rootID, string whats, string whattypes/*, string statusIds*/)
{
if (string.IsNullOrEmpty(whats) || string.IsNullOrEmpty(whattypes))
return RedirectToAction("IndexTree", new
{
rootID,
whats = SystemItemWhat.Story,
whattypes = ItemWhattype.AllItemWhattypeString(SystemItemWhat.Story),
/*statusIds = Status.AllStatuses().Where(i => i.Value >= Status.FirstStatusOnboard.Value)*/
});

var root = _repository.ReadItemAt(rootID);
return IndexTreeView("用例树[" + root.Title + "]",
new ItemTreeRightPadViewModel(_repository, null,
rootID, whats, whattypes, /*statusIds,*/ Product.ProductsAccessibleToUserIDs(WebSecurity.CurrentUserId),
showOpenAsTreeRoot: true,
subItemsTreeColumnWidth: 350),
comments: root.WhatType == ItemWhattype.ProductProductline
? new MvcHtmlString("悬停并点击下面的产品名称后的" + MFCUI.Image("", "/MFC/ItemTrees/OpenAsTreeRoot16.png") + "以查看详细的产品树。")
: new MvcHtmlString("下面是首级子目录水平放置的故事树;点击各级故事目录后的“打开子树”图标 "
+ MFCUI.Image("打开子树", "/MFC/ItemTrees/OpenAsTreeRoot16.png") + " 以展开详细故事树。"
+ "首级目录排序和跨产品拖拽只能在"
+ MFCUI.ImageLink("故事树首页", "/ProductManagement/StoryTrees/Index")
+ " 进行"));
}

下面是筛选“所有故事板上的用户故事的测试用例”:


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

如果之前的筛选是单独编码的,那么这2分钟就又变成4小时了——如果没有遇到什么Bug的话。

总结

1. 业务代码是那些上层的可能因业务升级而变更的代码。

2. 业务代码中包含“做什么”而不包含“怎么做”。

由于业务升级只涉及“做什么”,因此只需要修改业务代码。

3. L型代码结构利用公共的技术底层来搭建业务代码。

由于L型代码结构的底层是因需产生的,因此在重用时有很多现成的“例子”来代替或因此可以减少文档和注释的数量。

3. 新手/新人/徒弟看完业务代码后,应该能够进行“业务维护”以实现新的业务要求,这是业务代码编写的第一原则;能进行“技术维护”是更高水平的人的事情。

实际上,很多代码因为为了让新手/新人/徒弟看懂怎么实现的,而令他们很难维护,是舍本逐末的做法。

如何让新手学习、练习以得以进步的内容不在本文范围内(在同系列中有大量描述)。

举报

相关推荐

0 条评论