zoukankan      html  css  js  c++  java
  • 简单干净的C# MVC设计案例:BrothersDropdownList()

    团队切换器

    在/Teams/Details?id=xxx的页面,有这样一个控件,使得不需要回到/Teams/Index就能轻松切换团队:

    由于这种团队切换控件比比皆是,比如在团队故事板中(以及其他地方若干处):

    所以希望开发一个控件。

    这个控件应该能:

    1. 里边所包含的链接自动跟随页面的链接。

    比如第一张图里边,指向/Teams/Details?id=XXX;而第二张图里边,则是指向/Agile/TeamStoryBoards/Details?teamID=xxx

    2. 下拉菜单里边的所有链接,会因为id或teamID不同而指向指定团队的页面。

    设计思路

    调用代码:
            <div class = "link-span spliter">
                切换团队:@Team.TeamsDropdownList(this, "id")
            </div>
    
    调用代码总是很像自然语言,他说:在这个页面(this)的链接里有个字段("id")有差异,随Team而变化。在调用代码中,任何比自然语言多的内容基本上都是垃圾内容。

    实际上可以看出,Url本身不能硬编码写进去,而是应该从页面自动获取;获取后,把不同链接中的id=xxx换成不同Team的ID。
    实际代码:
        public partial class Team : Department
        {
            public static HelperResult TeamsDropdownList(WebViewPage page, string urlKey) //urlKey就是"id"或"teamID"这两个,用来表示需要替换URL中的哪个参数的
            {
                var currentTeamID = page.ParameterOf(urlKey); //提取id或teamID的实际值,里边利用了page.Request.RawUrl,也可以用page.Request.Queries(我们以前不知道有这个)。
                var program = Program.Default(); //取得缺省部门,部门是团队Team的上一个级别。
                var currentTeam = string.IsNullOrEmpty(currentTeamID) ? null : program.Teams().Single(i => i.ID.ToString() == currentTeamID); //下拉菜单中的当前团队。
                return MFCUI.DropdownListHtml(page,
                    currentTeam == null ? new MvcHtmlString("选择团队") : MFCUI.ImageLink(currentTeam.Title, page.Request.RawUrl, displayAsBoldTextOnPage: page),
                    program.Teams().Select(i => MFCUI.ImageLink(i.Title, page.MergeParameter(urlKey, i.ID.ToString()),
                        displayAsText: currentTeam != null && i.ID == currentTeam.ID)));
            }
    
    MFCUI.DropdownListHtml是另外一个产生下拉菜单的控件,不多说了,关键是其中的一句话:
    page.MergeParameter(urlKey, i.ID.ToString())
    这句话把page.Request.RawUrl中的"id"或"teamID"替换为program.Teams()中不同团队的ID,生成其新链接。
    这样,这个方法就能在任何页面,利用一个id/teamID等区分字段名来自动产生团队切换的效果,而链接到底指向哪里不关心。

    扩展

    之前说的这个东西不错,可是在另外一种场景中不能直接用,比如下面这个“切换产品”:


    调用代码长得很像:
        <div class="link-span spliter">
            切换产品:@Product.ProductsDropdownList(this, "id")
        </div>
    
    拷贝代码改写的结果是给Product增加一个类似函数:
            public static HelperResult ProductsDropdownList(WebViewPage page, string urlKey)
            {
                var currentTeamID = page.ParameterOf(urlKey);
                var productLine = ProductLine.Default();
                var currentProduct = string.IsNullOrEmpty(currentTeamID) ? null : productLine.Products().Single(i => i.ID.ToString() == currentTeamID);
                return MFCUI.DropdownListHtml(page,
                    currentProduct == null ? new MvcHtmlString("选择团队") : MFCUI.ImageLink(currentProduct.Title, page.Request.RawUrl, displayAsBoldTextOnPage: page),
                    productLine.Products().Select(i => MFCUI.ImageLink(i.Title, page.MergeParameter(urlKey, i.ID.ToString()),
                        displayAsText: currentProduct != null && i.ID == currentProduct.ID)));
            }
    
    这两个方法的内容几乎完全相同,这是代码坏味道的先兆;比如以后修改了MFCUI.DropdownListHtml,就要回来修改两个地方。
    幸亏Product和Team都继承自基类Item(用于处理树状结构,如Father,Brothers,SubItems之类的),这样就可以用基类进行改写:
    不过,改写的时候最好遵循某些步骤,否则很容易在半路上撂挑子。

    步骤1:改写其中一个函数,去掉具体的类型

    我先拿Product里边那个动刀,在原来函数的下面(不要直接给改了),增加一个新的函数:
            public static HelperResult BrothersDropdownList(WebViewPage page, string defaultText, Item father, string whattype, string urlKey)
            {
                var currentId = page.ParameterOf(urlKey);
                var currentItem = string.IsNullOrEmpty(currentId) ? null : father.SubItems().Single(i => i.ID.ToString() == currentId);
                return MFCUI.DropdownListHtml(page,
                    currentItem == null
                        ? new MvcHtmlString(defaultText)
                        : MFCUI.ImageLink(currentItem.Title, page.Request.RawUrl, displayAsBoldTextOnPage: page),
                    father.SubItems().Where(i => i.WhatType == whattype).Select(i => MFCUI.ImageLink(i.Title, page.MergeParameter(urlKey, i.ID.ToString()),
                        displayAsText: currentItem != null && i.ID == currentItem.ID)));
            }
    
    所有不同点,都被提取成一个参数:
    defaultText用来处理“选择团队”和“选择产品”;father用来处理Program.Default()和ProductLine().Default();
    whattype是我们用于在 Item基类层面上区分不同的类型的,比如产品有产品线(ProductLine)-产品(Product)-版本-发布等层级;而团队有公司-子公司-部门(Program)-团队(Team)等层级。
    father.SubItems().Where(i => i.WhatType == whattype)
    就是问father的下一级别中的whattype类型的。这样如果传入father=ProductLine.Default()和whattype=ItemWhatType.ProductProduct,就能得到所有产品,和原来代码结果相同。
    现在, 这个方法虽然还在Products下,但里边已经和Product完全无关了,这是步骤1的目标

    在网页上测试一下:
        <div class="link-span spliter">
            切换产品:@Product.ProductsDropdownList(this, "id")
            切换产品:@Product.BrothersDropdownList(this, "切换产品", ProductLine.Default(), ItemWhattype.ProductProduct, "id")
        </div>
    
    最好的测试方法,就是像上面这样做“对比测试”,然后看两者有何不同;这个很像敏捷开发里边提到重构时,要验证产品的外部功能没有发生变化一样。

    步骤2:挪到基类中

    基类叫Item,函数名不变还是BrothersDropdownList,代码不写了。
    重新做对比测试:
        <div class="link-span spliter">
            切换产品:@Product.ProductsDropdownList(this, "id")
            切换产品:@Product.BrothersDropdownList(this, "切换产品", ProductLine.Default(), ItemWhattype.ProductProduct, "id")
            切换产品:@Item.BrothersDropdownList(this, "切换产品", ProductLine.Default(), ItemWhattype.ProductProduct, "id")
        </div>
    

    结果是三个控件功能完全相同,好了,删除前两个;再去Team那边把Team的方法删除,调用也做相应修改。
    以后任何Item派生类都能在桌面上扔这样一个控件来切换了。
    在火星人里边所有东西都是Item,所以复用的次数非常多。

    总结

    1. 任何相似代码都是坏味道
    2. 调用代码的信息量应该与自然语言相同
    3. 重构时做对比测试。

    一些其他前提:凡是用两次的东西都可能值得做个积累。
    这个“积累”或许是个方法,或许是个基类。比如上面提到的MFCUI.DropdownList()/this.ParameterOf()/this.MergeParamegter()这些,都是以前为了别的事情做好了的。
    如果没有积累,每次想复用的时候就会发现要做很多准备工作,会情不自禁地想“算了这次先这样吧”。时间长了破罐子破摔,代码就越来越烂了。


  • 相关阅读:
    机器码call和jmp地址的计算
    linux下系统对于sigsegv错误时的处理
    elf文件中的.plt .rel.dyn .rel.plt .got .got.plt的关系
    docker随谈
    nginx性能优化技巧
    ubuntu安装php常见错误集锦
    ubuntu php5.6源码安装
    关于mysqld_safe
    ubuntu mysql5.7源码安装
    linux下nginx模块开发入门
  • 原文地址:https://www.cnblogs.com/riskyer/p/3238874.html
Copyright © 2011-2022 走看看