新闻资讯
把3000行代码重构成15行,这样做!
把三千行代码重构为 15 行
那年我刚毕业,进了现在这个公司。公司是搞数据中心环境监控的,里面充斥着嵌入式、精密空调、总线、RFID 的概念,我一个都不懂。还好,公司之前用 Delphi 写的老客户端因为太慢,然后就搞了个 Webform 的替代,恰好我对 Asp.Net 还算了解,我对业务的不了解并不妨碍我称成为这个公司的一个程序员。小公司也有小公司的好,人少,进去很快负责代码开发。我当然也就搞这个数据中心智能管理系统啦。这个系统非常的庞大,尤其牛逼的是支持客户端组态,然后动态生成网页,数据还能通过 Socket 实时监控 (那时我还真就不懂网络编程)。这个对于当时的我来说,真真是高、大、上呐!!当时跟着了解整个系统大半个月才算能够调试,写一些简单的页面。在维护系统的过程中,时不时要扩展一些功能,也就接触了下面这个类:
看到没有,就是当年最最流行的三层架构的产物,对于刚出茅庐的毛头小子来说,这是多么专业的文件头注释,还有反射也就算了,这构造函数还能静态的,还能私有的?那时刚接触这么高大上的代码的我,瞬间给跪了!
但是,类写多了,我就感觉越来越别扭,就是下面这段代码:
每增加一个表,除了要改接口、要改 DAL、要改 BLL 之外,还得在这个工厂类添加一个方法,真真是累到手抽筋,即使有当时公司了的 G 工给我推荐的神器 —— 动软代码生成器,这粘贴复制的几遍,也是让我感觉到异常繁琐,有时候打键盘稍微累了点,还把复制出来代码改错了,你妹的,难道这就是程序员该干的事情,不,绝对不是!我想起了一句至理名言: 当你觉得代码重复出现在程序中的时候,就应该重构了。是的,在这句话的指导下,我开始了折腾,决定挑战这个高大上的代码,事实证明,思想的力量是无穷的。那么,怎么修改呢,仔细观察之后,发现其中 className 的生成跟返回的类型非常类似,只是一个是类名,一个是字符串,这两者之间应该能够关联起来。于是 google 了一下 (当时 GFW 还没猖獗起来哈),隐隐约约就找到了 “ 反射” 这两个字,深入了解之后,确定可以完成。接下来,就是返回的类型了,返回的类型并不固定,但是似乎很有规律…… 这个似乎好像在哪里见过,对了, 模板,C++ 课程上有讲过的,于是再次 google,了解到了 C# 中使用了泛型代替了 C++ 中的模板。在学习完泛型和反射之后,并参考了网上的一些文章,我捣鼓出了下面的代码:
没错,就是它了,三层架构年代最流行的工厂类……
看着原来滚十几屏幕的代码,变成了十多行的代码,真是爽到了骨子里去了,太干净了!唯一让我担忧的是,我进公司的时候,帮忙整理公司申请软件著作权都是需要代码量的,根据代码多少行来评估软件的大小,万一老板知道了我非但没有帮公司增加代码量,还减少了,会不会立即把我开掉?我没敢给我们老板展示我优秀的成果。所幸,这段代码非但没有出过任何问题,还避免了以前同事老是在新增一个类之后,把代码复制过来,但是没有正确修改的问题,大大提高了效率。虽然,我没敢大事宣布我的劳动成果,但是这次成功的修改,则彻底让我走上了代码重构的不归路。看到这里,大家应该知道这个案例是否真实的了吧。我相信,从 08 年开始的码农们,看到这种类似的代码绝对不比我少。那么,我想告诉你们的是什么呢?
- 要在编程过程中多思考
- 编程的思想很重要,请多看点经典的书
- 从小处着眼,慢慢重构,尤其在应对一个大型的系统
- 当重复出现的时候,你应该考虑重构了
- 粘贴复制的代码越少,你的系统越稳定
少用代码生成器
- 因为使用了动软代码生成器,生成代码方便,就没多想了。
- 三层架构的概念倒是了解了,但是没有去深入思考就拿来应用
- 遇到重复的代码,没有重构的概念,这是思想的问题 —— 思想比你的能力重要
至今为止,还是很多人使用代码生成器,那么我们应该怎么对待这个问题呢。我认为,代码生成器确实可以减少你不少工作,但是少用,那些重复性的工作,除了部分确实是没有办法的,其他大部分都是可以通过框架解决的,举例来说,像三层架构,真正需要用到代码生成器的,也就是 Model 类而已,其他的完全可以在框架中完成。因此 你要竭尽全力的思考怎么在框架中来减少你的重复性工作,而不是依赖于代码生成器。另外,如果你还是在用相关的代码生成工具,请 重新定义 “动软代码生成器” 的代码模板,自己写一个模板;或者 使用 CodeSmith 来完全制定自己的代码生成,因为动软给的代码模板真心乱,比如下面这段代码:
for ( int n = 0; n < rowsCount; n++)
{
model = new DBAccess.Model.eventweek();
if(dt.Rows[n][ "GroupNo"].ToString()!= "")
{
model.GroupNo= int.Parse(dt.Rows[n][ "GroupNo"].ToString());
}
if(dt.Rows[n][ "Week0"].ToString()!= "")
{
model.Week0= int.Parse(dt.Rows[n][ "Week0"].ToString());
}
if(dt.Rows[n][ "Week1"].ToString()!= "")
{
model.Week1= int.Parse(dt.Rows[n][ "Week1"].ToString());
}
}
首先,你就不能用 var row=dt.Rows [n] 替代吗?其次,直接用 int.Parse 如果抛出了异常性能得有多低?再次,这段代码要是有点修改,我不是要每个 dt.Rows [n] 得改一遍?
不要重复发明轮子
我们再来看看其他的一些代码:
public List< string> GetDevices( string dev){
List< string> devs= new List< string>();
int start= 0;
for( int i= 0;i<dev.Length;i++){
if(dev[i]== '^'){
devs.Add(dev.SubString(start,i));
start=i+ 1;
}
}
return devs;
}
有没有很眼熟,没错,这就是对 String.Split () 函数的简单实现。我的前辈应该是从 c++ 程序员转过来的,习惯了各种功能自己实现一遍,但是他忽略了 C# 的很多东西。我们不去评判这段代码的优劣,而实际上他在很长一段时间都运行得很好。我们来看看使用这一段代码有什么不好的地方:
- 重复发明轮子。花费了额外的时间,函数的健壮性和很差
-
可读性差。其实是一个很简单的功能,但是用上了这么一段函数,起初我还以为有什么特别的功能。
- 了解你所学的编程语言的特性。你可以看一本基础的入门书籍,把所有的特性浏览一遍,或者上 MSDN,把相关的内容过一遍。
- 在你决定动手发明一个轮子之前,先搜索一下现成的解决方案。你还可以到 CodeProject、GitHub 之类的网站搜索一下。在知乎上有很多人都在批评这么一种现象,老是问一些重复性的问题,然后又职责知乎没落了,没有人回答他的问题,实际上相关问题已经有了很详细的解答,那提问之前,不能首先去搜一下是否有现成的答案,反而指责没有回答他的问题呢?
-
你有一定的基础之后,还应该去读一下相关的经典书籍,深入了解其中的原理。比如,你觉得你有一定的基础了,我建议你去把《CLR Via C#》多读几遍,你了解原理越多,你越是能够利用这编程语言的特性,从而来实现原本那些你认为要靠自己写代码的功能。
线程使用越来越多,我越是觉得浪费,因为这些现场仅仅只需完成一次或者有限的几次,大部分时间都是没有意义的,那么怎么办呢?我决定自己写一个任务类,来完成相关的事情。说干就干,我很快把这个类写出来了。
public abstract class MissionBase : IMission {
private DateTime _nextExecuteTime;
protected virtual DateTime[] ExecuteTimePoints { get; private set; }
protected virtual int IntervalSeconds { get; private set; }
protected IEngine Engine { get; private set; }
public bool IsCanceled{ get{……}}
public bool IsExecuting{ get{……}}
public bool IsTimeToExecute{ get{……}}
public abstract bool Enable { get; }
public abstract string Name { get; }
protected MissionBase( IEngine engine)
{
ExecuteTimePoints = null; //默认采用间隔的方式 IntervalSeconds = 60 * 60; //默认的间隔为1个小时 Engine = engine;
}
/// 任务的执行方法 public void Done()
{
if (Interlocked.CompareExchange( ref _isExecuting, 1, 0) == 1) return;
try {
……
}
finally {
Interlocked.CompareExchange( ref _isExecuting, 0, 1);
}
}
///实际方法的执行 protected abstract void DoneReal();
}
但是,实际上这个任务方法,并不好用,要写的代码不少,而且可靠性还没有保障。当然,我可以继续完善这个类,但是我决定搜索一下是否还有其他的方法。直到有一天,我再次阅读《CLR Via C#》,看到线程这一章,讲到了 System.Threading.Timer 以及 ThreadPool 类时,我就知道了, 使用 Timer 类完全可以解决我的这个用尽量少的线程完成定时任务的问题。因为从原理上来说,Timer 类无论你声明了多少个,其实就只有一个线程在执行。当你到了执行时间时,这个管理线程会用 ThreadPool 来执行 Timer 中的函数,因为使用的 ThreadPool,执行完成之后,线程就马上回收了,这个其实就完全实现了我所需要的功能。
等你无法重构的时候再考虑重写
系统中确实存在很多不合理的地方,但是有不少的这种代码,恰恰是为了解决一些特定场景下的问题的。也就是说,所有的规范以及编程的原则,其实也是有条件限制的,他可能在大部分的时候是正确的,能够指导你完成你的任务,但是,并不是在所有地方都是适用的。比如数据库范式,但实际中我们的设计往往会考虑冗余,这是违背范式的,但是为什么还有那么多人趋之若鹜呢?因为我们可能需要用空间换时间。
如果我们一开始就考虑重写,那么你可能会陷入以下的困境:
-
需要花更大的精力来完成一些看似简单的 BUG
你要知道,有一部分看似错误或者非常不优美的代码,其实恰恰是为了解决一些非常刁钻的问题的。 -
再也无法兼容老的系统了
你急于把原有系统重写,却往往忽略了对原有系统的兼容,那么你新的系统的推进则会十分缓慢。 而老系统的维护,又会陷入及其尴尬的情况。 -
过度设计,导致重写计划迟迟无法完成
有重写冲动的程序员往往是在架构设计上有一些读到的见解,他们善于利用所学的各种设计模式和架构技巧来建立系统,但是越是想尽可能的利用设计模式,越是陷入过度设计的困局,导致重写的计划迟迟都无法完成。 -
无法有效利用现有系统已经完成并测试的代码
如果你确实有必要进行重写,我还是建议你把代码尽可能的重构。 因为重构之后的系统,能够让你更轻易的重写,又最大限度了保留以前可用的 业务代码。
class MainEngine:IEngine{
public MainEngine (ConfigSettings config){
}
public void Start ();
public void Stop ();
}
需要增加新的业务功能时,程序员写的代码往往是这样的:首先时修改配置类
class ConfigSettings{
public bool NewFuncEnable{ get; private set;}
public ConfigSettings(){
NewFuncEnable=xx; //从配置文件读取 }
}
接着修改主程序:
class MainEngine:IEngine{
private NewFuncClass newCls= new NewFuncClass();
public MainEngine (ConfigSettings config){
}
public void Start (){
if(config.NewFuncEnable)
newCls.Start();
}
public void Stop (){
if(config.NewFuncEnable)
newCls.Stop();
}
}
在修改的过程中,往往是根据配置文件来判断新功能是否启用。上面代码会造成什么问题呢:
- 主程序代码和扩展功能耦合性太强,每增加一个功能都要修改主程序代码,这里非常非常容易出错。尤其是新的人进度开发组,很容易就忘主程序中增加了一些致命性的代码。比如上述的扩展功能,可能是在特定的项目中才会有这个扩展功能,但是,写代码的人忘记增加是否启用的配置选项了,导致所有的项目都应用了这个功能,而这个功能需要特定的表,这样就悲剧了。即使是你增加了配置,也是非常的不美观,因为在通用的版本中使用了这个配置,往往会让定制项目以外的人员感到困惑。
- 增加扩展功能的人还需对整个 MainEngine 代码有一定的熟悉,否则,他根本就不知道在 Start 方法和 Stop 方法进行 newClas 的对应方法的调用
- 如果你打算对这段代码进行重写,那么,你会感到非常的困难,因为你分不清楚 newCls 这个新实例的作用,要么你花大精力去把所有代码理清楚,要么直接就把这段新增的业务代码去掉了。
private void RegisterTaskHandlerBundles() { var bundles = xxx.BLL.Caches.ServiceBundleCache.Instance.GetBundles( "TaskHandlerBundle"); if (bundles != null && bundles.Count > 0) { var asmCache = new Dictionary< string, Assembly>(); foreach ( var bundle in bundles) { try { if (!asmCache.ContainsKey(bundle.Category)) asmCache.Add(bundle.Category, Assembly.Load(bundle.AssemblyName)); var handler = (ITaskHandler)asmCache[bundle.Category].CreateInstance(bundle.ClassName, false, BindingFlags.Default, null, new object[] { this, bundle }, null, null); _taskHandlerBundles.Add(bundle, handler); } catch (Exception e) { NLogHelper.Instance.Error( "加载bundle[Name:{0},Assembly:{1}:Class:{2}]异常:{3}", bundle.Name, bundle.AssemblyName, bundle.ClassName, e.Message); } } } }
修改 MainEngine 代码
class MainEngine:IEngine{
private NewFuncClass newCls= new NewFuncClass();
public MainEngine (ConfigSettings config){
RegisterTaskHandlerBundles();
}
public void Start (){
_taskHandlerBundles.Start();
}
public void Stop (){
_taskHandlerBundles.Stop();
}
}
OK,现在我们再来看看怎么实现原来的新增功能:你只需按规范新建一个类,继承 ITaskHandler 接口,并实现接口的方法。最后在 XTGL_ServiceBundle 表中新增一条记录即可。我们再来看看这么做有什么好处:
- 新增的类只需按规范写即可,完全对 MainEngine 代码没有任何影响。你甚至可以把这个 MainEngine 代码写在一个新建的 Dll 中。
- 新增功能的这个业务类跟原来的代码解耦,非常方便进行新功能的业务测试,而无需考虑原有框架的影响
- 新增功能的业务类与架构完全分离,我们在重写代码中只要保证接口的稳定性,无论我们怎么把系统架构重写,我们可以马上就重用上原有的业务功能代码。
重构的目标之一,就是把框架和业务完全分离。有志于深入了解的同学,可以了解下反射、Ioc 和插件话编程等。
学会单元测试,培养你的重构意识
所谓重构
- 把基础打牢固
- 多看点优秀的代码
- 避免复制粘贴,如果看见重复代码时应该有意识要消灭它
- 减少对代码生成器的依赖
- 在处理现有代码时尽量用重构代替重写,在重写之前一定要先重构
- 尽量让所有的方法都是可测试的
如果你坚持这么去做了,一段时间之后感觉自然就出来了。重构的目的,是让你的代码更为精简、稳定、能够重用,是最大程度的让功能和业务分离。 在重构的过程中,你的阅读代码的能力、写出优秀代码的能力以及系统架构能力都会稳步提升。你成为一个优秀的程序员将指日可待。作者:马非码
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/31556440/viewspace-2674448/,如需转载,请注明出处,否则将追究法律责任。
回复列表