重构技巧之添加参数

最近在经手一个Winform项目,里面有个需求:TreeView控件选择不同的TreeNode时,需要展示不同的右键菜单。记录一下重构过程。

需求

现将公司内部需求初始需求转换如下:
如果TreeView选中的TreeNode是国家,则右键菜单展示”添加省”,一个选项。
如果TreeView选中的TreeNode是省份,则右键菜单展示”添加市”,”删除”两个选项。
如果TreeView选中的TreeNode是城市,则右键菜单展示”添加区”,”删除”两个选项。
如果TreeView选中的TreeNode是区,则右键菜单展示,”删除”一个选项。
这部分的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var area = TreeView.SelectedNode.Tag as Area; //
if(area!=null)
{
var areaType=area.type;
ContextMenuStrip menuStrip = new ContextMenuStrip();
if(areaType==AreaType.国家)
{
menuStrip.Items.Add(new ToolStripMenuItem("添加省"));
}
if(areaType==AreaType.省份)
{
menuStrip.Items.Add(new ToolStripMenuItem("添加市"));
menuStrip.Items.Add(new ToolStripMenuItem("删除"));
}
if(areaType==AreaType.城市)
{
menuStrip.Items.Add(new ToolStripMenuItem("添加区"));
menuStrip.Items.Add(new ToolStripMenuItem("删除"));
}
if(areaType==AreaType.区)
{
menuStrip.Items.Add(new ToolStripMenuItem("删除"));
}

menuStrip.Show(e.Location);
}

后来业务发展了,除了AreaType.国家这个选项,右键菜单都需要添加一个”打开地图”的右键菜单,且位于右键菜单的最上方。
重构之后的示例代码变成了如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var area = TreeView.SelectedNode.Tag as Area; //
if(area!=null)
{
var areaType=area.type;
ContextMenuStrip menuStrip = new ContextMenuStrip();
if(areaType==AreaType.国家)
{
menuStrip.Items.Add(new ToolStripMenuItem("添加省"));
}
if(areaType==AreaType.省份)
{
menuStrip.Items.Add(new ToolStripMenuItem("打开地图"));
menuStrip.Items.Add(new ToolStripMenuItem("添加市"));
menuStrip.Items.Add(new ToolStripMenuItem("删除"));
}
if(areaType==AreaType.城市)
{
menuStrip.Items.Add(new ToolStripMenuItem("打开地图"));
menuStrip.Items.Add(new ToolStripMenuItem("添加区"));
menuStrip.Items.Add(new ToolStripMenuItem("删除"));

}
if(areaType==AreaType.区)
{
menuStrip.Items.Add(new ToolStripMenuItem("打开地图"));
menuStrip.Items.Add(new ToolStripMenuItem("删除"));
}

menuStrip.Show(e.Location);
}

这次需求变动,已经让我闻到了代码中的坏味道。增删改一个右键菜单需要修改好几处地方,删除右键菜单也是同理。
故需要对代码进行重构。

重构

提取函数,伪代码如下

1
2
3
4
5
6
7
8
private ContextMenuStrip menuStrip = new ContextMenuStrip(); //菜单转为私有属性
private void AddToolStripMenuItem(string controlName, bool isAdd)
{
if (isAdd)
{
this.menuStrip.Items.Add(new ToolStripMenuItem(controlName););
}
}

故调用处的实现代码变成了如下

1
2
3
4
5
6
7
8
9
10
11
12
var area = TreeView.SelectedNode.Tag as Area; //
if(area!=null)
{
var areaType=area.type;
this.menuStrip.Items.Clear();
AddToolStripMenuItem("添加省",areaType==AreaType.国家);
AddToolStripMenuItem("打开地图",!areaType==AreaType.国家);
AddToolStripMenuItem("添加市",areaType==AreaType.省份);
AddToolStripMenuItem("添加区",areaType==AreaType.城市);
AddToolStripMenuItem("删除",!areaType==AreaType.国家);
menuStrip.Show(e.Location);
}

若以后需求变更,打开地图需要改成打开百度地图,然后新增打开高德地图也只需要修改一行代码,并新增一行代码即可。

总结

该重构过程运用的是提取函数和函数添加参数。
AddToolStripMenuItem函数只关心菜单项是否添加,而不关心菜单项添加的逻辑是什么。
重构之后,右键菜单的可能的菜单项也一目了然,每一个菜单项对应的添加条件也一目了然。若isAdd参数对应的表达式过长,则可以将该表达式封装成函数进行传递。
通过该重构提高了项目的可维护性,也算是一种成就。

委托在重构中的运用

最近在工作中的一次重构过程中运用到了委托,记录一下。

场景

有一段示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
public class ProjectAClass
{
public static void CallMethod(bool isTrue)
{
if (isTrue)
{
ProjectB_StaticClass.DoSomething();
}
}

}

需求

后来因为业务的不断发展,需要把ProjectB从整个解决方案中移除掉。
ProjectB_StaticClassB.DoSomething();需要替换成 ProjectC_StaticClassC.DoSomething();
但是项目ProjectC需要引用ProjectA,故不能直接进行引用,否则会造成项目之间的循环依赖。

解决方案

ProjectAClass添加一个委托事件,CallMethod函数添加一个Dosometing dosometing的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ProjectAClass
{
public delegate void Dosometing();

public static void CallMethod(bool isTrue, Dosometing dosometing)
{
if (isTrue)
{
dosometing?.Invoke();
}
}
}

具体的调用代码处就变成了

1
ProjectAClass.CallMethod(true,()=>{ProjectC_StaticClassC.DoSomething();});

扩展

若ProjectAClass.CallMethod方法多地方出现,然而没有办法一下修改到位。可以通过参数默认值的方式来实现修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void CallMethod(bool isTrue, Dosometing dosometing=null)
{
if (isTrue)
{
if(dosometing == null)
{
ProjectB_StaticClass.DoSomething();
}
else
{
dosometing.Invoke(); // or dosometing()
}
}
}

该方法可以避免原来的调用方法进行大面积的修改。

总结

重构之后的代码ProjectAClass只需要判断条件是否满足,满足则dosometing,而不需要知道dosometing这个委托方法中具体细节。

用Dictionary替换switch case的注意事项

最近试图重构一段现做现卖的祖传代码,结果改完之后,性能急速下降,下面给出示意代码的截图,以便提醒自己工作需要更加认真和细心。

示例代码

错误的重构

差异对比
Dictionary的执行时间竟然是Switch case的四倍以上?原因是啥?我们来看一下各个动物的构造函数
构造函数
即生成Dictionary的每一个键值对的值的时候,都实例化了一个Animal的子类,每个子类的实例化都等待了十秒钟,总实例化就耗费了40秒钟。

正确的重构

正确重构
先获取对象的type,然后通过Activator.CreateInstance(type)创建对象。

总结

switch case转换成dictionary算得上是一种重构,起到了减少代码量,提高可维护性的效果。
这次我的失误也算是明白了一个深刻的道理,所有的重构需要建立在完整的测试机制的前提下,否则可能会造成严重的损失。
最后一句箴言如无必要,勿动祖传代码