偿还技术债 - 通过重构拯救老代码

偿还技术债 - 通过重构拯救老代码

尝试去接管一个陈旧的代码库使他成为达成一个可控的状态?这几年的大型的旧web应用程序开发给了我们如下这些建议。

通过重构去拯救旧代码

松鼠会因为忘记自己把松果放在那里,使得每年多了几千棵松树。类似的,这些事情在项目中都没有什么关系。你的项目是可以被拯救的

无论代码多么的杂乱,让人疯狂。但是你的老板让你上了,你要相信不管前路多么曲折坎坷,你总能搞定的!

心无畏惧

公平的说。比起冲进一个四处鲜血还有巨龙守在门口的沼泽地待几年,你更愿意在一片清新的草原上散步。

不幸的是,你的老板和这片沼泽地的公爵达成了一个协议,派你上了。

不管怎么样,你上了,所要做的就是把这块泥泞的沼泽变成一块美丽的牧场

技术债 - 它怎么会这样

当你在项目中遇到一个又一个坑的时候,你可能在想:这是哪个“无能”的人做的?有人肯定能预见这个问题,但是为什么不是这样呢。

可能吧,不过更多的时候并不是这样。包括我们自己,很自信的写出的东西可能之后自己也不知道是什么了。

“无能”这个词并不适合解释这种情况。从工作本身的角度解释,它应该叫做“技术债”。

代码癌

“代码癌”这个东西充斥在各个项目的开发周期内,指的是为了快速解决当下问题采取的临时方案(原文中为“Ugly hacks”)。随着这种只完成了功能但是很难维护的东西的出现,然后技术债就开始越积越多。

然后,一点一点的,项目就开始失控。不经手真正代码的的人根本不会在意这一点,但是,最终,他可能最后落在你的手上。

对于任何项目来说,快速完成需求,推出新的功能都是非常重要的。如果做不到,珍贵的用户就会离开。举个例子,就像是他们宁愿去隔壁泥泞沼泽上的酒吧喝着长岛冰茶,也不会在你花了一整年修剪工整的草坪上傻站着。

所以,就算是一个健康的项目,技术债也一定会产生,但是为了避免代码最终变得无法维护,这种积累的技术债在某些时候一定要解决的。

为了避免让一群疯狂的受害者冲进你的沼泽地找你讨债。作为沼泽地的主人,你可能需要一些“重构”:交换一些代码的位置,让他能够更好地维护和拓展。

说服客户

对于这片泥沼的公爵,你需要告诉他他的土地上正在不断的进入小怪物。因为总有客户想要这样那样的功能。当你面对一个deadline定为一周但是你完成它至少要两周的需求,你需要学会向他解释,让他明白你需要花一点时间来做点小小的重构

如果做不到这一点,可以预见,技术债将不断增加,最后无法收场。

其实你的目的和你的客户是一致的,愉快而顺利的工作,然后大家都能赚钱~ 这个的前提就是需要有一个稳定的项目可以让你从容的写出你的代码。

争取你的自由

你需要学会如何将情况解释清楚。我觉得最好的办法就是向这片泥沼的主人解释清楚有哪些潜在的风险隐藏在角落里,要让他知道你的好意。你可以试试下面几条建议:

1.解释什么叫做“技术债”,告诉这种债务过高会让开发越来越慢。我们可能需要花费很多时间才能找到问题,而且要花费很多时间去解决它。

2.给出清晰的短期目标和计划,告诉客户你可以选择花一周去实现一个功能,也可以给我吗一周时间重构,然后花一天去完成这个功能。但是第二种方案对于以后的开发有很多好处。

3.告诉客户,其实你们最终的目标是一样的。而且,你们担忧的事情也是类似的。

一旦你和泥沼的主人达成一致,事实上最困难的事情就已经结束了。下面,让我们的项目走上正确的方向吧

不要再弄出一个新的沼泽地

你可以尝试修复它,不要重写它。

你可能说服了沼泽地的主人,你可以为他创造一个崭新的牧场。你会用更新的工具,更好的方式。但是,听我的,千万不要

重新的风险有很多:

1.这样做就像按下了一个危险的开关,新代码从来没在生产环境下跑过,谁都不知道会发生什么。

2.数据迁移,从旧的系统向新系统迁入和迁出数据很容易出现问题。

3.复刻旧的错误,你开始重写了,就有可能重复旧项目中的问题,旧项目的一些巧妙的特性也会被弄丢。很多问题都发生在系统的一些你本身刚开始就有疑问的地方。你会浪费很多时间,不管是你还是你的客户。

4.你需要紧跟着业务,你在做新工程的时候,旧的工程也需要同时跟进业务需求。这样相同的东西需要在两边同时开展。

所以,听我的,不要从头开始,优化现有的

让问题变得可见

这种事情很容易让人不爽,把问题都暴露出来。就像是让沼泽里的怪物们统统跑到了你的眼前向你的脸上吐痰,巨龙喷着火要烧掉你的头发,住在蘑菇旁的小侏儒踢着你的小腿。

不管你有没有注意到,这些害虫一直在侵蚀你的项目,你得想办法摆脱他们。所以必须得在任何时候都能看到到底问题出在哪

预见错误。每周花一点时间处理最常出现的一些问题。将问题图表化,可能能更有效的做到这一点,

监控环境变化。这可能是找到瓶颈和预见危机的关键(见下文)。

这样,你就能知道是什么问题对你的项目伤害最大,你就能提早发现问题展开救治,而不是最后面对一个垂死的病人束手无策。

处理最重要的问题

做到这一点。你就需要有一个对系统完整的目标。就像你清楚的知道你最终要构建的完美的牧场是什么样,在你的每一步工作都是朝着这个目标。

这样你就不会忘记你每一小步做了什么。在这一小步一小步中间,你就会发现自己到达了自己最终的目标。

结合上文所说的监控方式以及你的最终目标,确定什么是你最先要处理的。你最大的问题应该不是马上实现某个目标,而且开始管理重构路上你找出的问题

还有个建议,有些小问题就像是一写坡脚的小精灵,你可能看她们不爽,但是实际上她们是无害的。你最好把时间花到如何踢出真正对人有伤害的那些食人魔。

这行代码是我写的

解决问题是最重要的,但是不意味着其中的实现细节不重要。事实上,他们同样重要。

这里有个沼泽生存原则,保持你的宿营地比你发现他的时候更干净。随着你持续的整理,注意不留下什么垃圾。你的环境就会越来越好直到你发现这篇泥沼变成了绿洲。

态度决定一切:

1.细心。代码是你写的,你要能向所有接触它的解释清楚它,不要草率马虎。

2.团队要一样细心。你辛苦的填上一个有一个坑,但是后来人还在不断挖坑的话,依然还是那个样子。

3.纪律。团队任何一个人开始让事情变糟,都不要让他逃掉责任。

4.保持小步骤。在正确的方向上,进度比完美实现重要。

5.小小的胜利都能让人信心十足。你会发现一些地方优秀的修改会促使你想去修复它边上的问题。

建立标准模块

一个对文明评价的重要指标是 在每平方米的土地上有多少图书馆。同样的,这对你的项目来说也是一个类似的指标(当然在项目里我们不叫他(Library)图书馆)。

举个例子,即使在最烂的沼泽地里,也会有几个不是特别糟糕的景点。无论何时,当你发现了一些好代码,他们完成了一些很不错的事情,就把他们挪到你的标准模块里,是的他们可以复用。

毫无疑问,你要修复的代码做的都是不好的。它们完全忽略和和避开了能让程序员觉得更轻松的可以陈祚最佳实践标准的原则。没有人会帮你搞定他们,你只有一条路可走:在你的新代码中实践下面的标准

让你的代码标准化和模块化。有些情况下你不得不使用旧的方式和不标准的方式处理旧的问题,但是这不意味着,你不能不在新的代码中实践这些正确的东西。

用你的新模块重构

每当你的新模块写好,你就可以用它来重构旧的。你做这件事的时候没必要向马拉松的冲刺阶段一样紧迫。每当你偶然发现你有一个好的新方式替代那部分老代码的时候。保持小步骤,最后你会发现一点一点你就找不到那些困惑你的老代码了。

通常,上述步骤是可行的。不过有时候你发现有些代码包含无数的依赖关系四处分布(比如会话访问,各种模块和服务纠缠在一起)。这时候,有个好方法是把它们的依赖方法和函数从他们内部暴露出来,外部就可以通过这些函数和方法访问他们。这种方式使得这些代码相对独立,以后更利于分离和移动它们。

同时,一旦代码独立了,它们就变得可测试了。

通过测试建立信任

自由穿行的食人魔们可以比带着枷锁的食人魔更可怕。把系统中最为关键的部件(同时它们可能造成最严重的问题)加上锁链对于掌控一个系统是非常重要的。在每次构建的时候进行测试(最好是自动化的)尤为关键

如果有更多的对你的系统行为的自动化测试,你就能在改动项目的时候有更充足的信心。一点有东西出错,这些测试就能让你在发版前及时修改它们

高级别测试

有一个有效的测试方式叫做:关键场景验收测试。对于一个电子商务系统,肯定包括一个结账的流程。没有订单你肯定不赚钱,测试这个订单流程,你就能在客户没有发觉之前解决这个问题。有了这样的测试,你就可以避免自己不知不觉的引进重大问题

低级别测试

这种低级别测试我们推荐单元测试。使用上文说的小步骤逐一分解依赖的方式,你会使得你的代码有一套清晰的输入和输出结构。这种情况下就很适合用单元测试覆盖这些代码的功能。你可以定义一组输入,并且定义它预期的输出。这些测试可以确保这些代码的行为和你预期的一致

不要测试所有的东西

没必要测试系统的每一行代码。原则上让系统的每一个方面可以使我们对所有事情充满信心,但是让所有东西可测试并为他们编写和维护测试用例会让成本变过高。根据我的经验,你最好把时间花在为重要的业务逻辑和有可能造成重大问题的代码上(即使他们看起来很简单)。

这样你就相当于把沼泽里最危险的怪物们锁起来了,整个沼泽变得更安全咯。

隔离和更换

结合我们上文提到的战略模式和技术,我们总结唯一个原则:“隔离和替换”。

每当你遇到某些代码,他们的内部逻辑很难理解,而且打算用那种小步骤重构的方式整理出来。下面有些步骤建议:

1.隔离混乱的部分,把他们放到一些单独的方法里。

2.把依赖的部分拆出来作为他的参数

3.把逻辑里的副作用分离出来(比如向数据库保存数据),这里也是小步骤进行

4.隔离单独的逻辑到不同的方法里

5.添加单元测试覆盖逻辑模块

6.重写的逻辑模块。就算他能通过测试,也要保证至少他的行为要类似老系统中的行为。

7.保持新老版本同时能在线上运行。保证旧版本也能运行

8.log记录下新老版本的输出。

9.对比日志,看看他们输出的不同,如果有不同就为该模块添加一个新的单元测试,然后开始修复它。

10.重复上述步骤直到他们一致为止。

11.最后新版本能完全替换旧版本的相关逻辑

这种做法就像是不需要停车就能为他换了发动机,甚至是没有人能主要到他的内部发生了变化。

说服自己

在这里,上述的态度和方法,至少对于我来说,是一个拯救一个濒临崩溃的系统之路

最初我还在犹豫,为什么要把我的时间花在这样一个长期的旧工程上。但是这期间我也获得了很多能帮助我变得更好的技能

1.识别糟糕的代码。你认识到什么是糟糕的代码因为你花了大量的是在在它上面。你在新项目中就不会使用它这种方式因为你知道它会在未来搞出多少问题。

2.回归本质。我认识到系统的每一个方面都值得质疑和重新评估。项目里没什么是无懈可击的,你会发现许多更现代的框架可以帮你处理很多问题。

3.你和项目同时成长。随着你对项目的关注越多,你就会看到更多的挑战。你知道了哪些是这个系统的瓶颈,越来越多的人在依赖的工作成果,而你是整件事情的中心。

4.人们总是不断的生产新的东西。如果赶在在他们对项目造成伤害之前,理解他们的运作方式也是个不错的技能。

5.有趣的经历。回头看你这一路的历程,看看这个系统的样子和你几年前第一次遇到他的时候。是不是也蛮有成就感的呢。

去完成这个巨大(至少是长期)的任务吧

你可以选择做一个胆小的农民,期待中谁能把你从这片沼泽地带到迪士尼乐园。或者你可以摆正你的姿态,扮演好救世主的角色。在任何一个有坚固地面的沼泽地里,你都可以找到你的位置。

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。

hibernate缓存详解 - 2016-07-22 18:07:56

为什么要用hibernate缓存? hibernate是一个 持久层框架 ,经常访问物理数据库。为了降低应用程序对物理数据源访问的次数,从而提高应用程序的运行性能,我们想到使用hibernate缓存机制。缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或事件会同步缓存和物理数据源的数据。 hibernate缓存的原理 缓存的主要作用是查询 。 hibernate缓存包括三大类: hibernate一级缓存、hibernate二级缓存和hibernate查询缓存 。 一

MAT使用的几张图例 - 2016-07-22 18:07:51

下面三个是内存泄漏可能性比较大的地方 problem suspect 1 problem suspect 2 problem suspect 3 点击detail 可以看详细 在dominator_tree 可以对象按照group by package 分类 便于查看那部分代码出问题 选中一个节点 右键查看with incoming reference 可以看  ps :( ListObjects with incoming references  表示的是 当前查看的对象,被外部应用 ListObjec

Hibernate的核心接口和类 - 2016-07-22 18:07:25

Hibernate的核心类和接口一共有6个,分别为: Session、SessionFactory、 Transaction、Query、Criteria和Configuration 。这6个核心和类接口在任何开发中都会用到。通过这些接口,不仅可以对持久化对象进行存取,还能够进行事务控制。下面对这6个核心接口和类分别加以介绍。 Configuration Configuration类的作用是对Hibernate进行配置,以及对它进行启动。在Hibernate的启动过程中,Configuration 类的实
1、概述 在我开始构思这几篇关于“自己动手设计ESB中间件”的文章时,曾有好几次动过放弃的念头。原因倒不是因为对冗长的文章产生了惰性,而是ESB中所涉及到的技术知识和需要突破的设计难点实在是比较多,再冗长的几篇博文甚至无法对它们全部进行概述,另外如果在思路上稍微有一点差池就会误导读者。 一个可以稳定使用的ESB中间件凝聚了一个团队很多参与者的心血,一个人肯定是无法完成这些工作的 。但是笔者思索再三,还是下决心将这这即便文章完成,因为这是对本专题从第19篇文章到第39篇文章中所介绍的知识点的最好的总结。我们
1、Exactly_once简介 Exactly_once语义是Flink的特性之一,那么Flink到底提供了什么层次的Excactly_once?有人说是是每个算子保证只处理一次,有人说是每条数据保证只处理一次。其实理解这个语义并不难,直接在官方文档中就可以看出: 从图中可以看出:Exactly_once是为有状态的计算准备的! 换句话说,没有状态的算子操作(operator),Flink无法也无需保证其只被处理Exactly_once!为什么无需呢?因为即使失败的情况下,无状态的operator(ma
简单工厂模式是指专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 从图中我们可以清楚的看到,该模式中主要包含下面3种 角色: 工厂(Creator)角色 它是工厂模式的核心,负责实现创建所有势力的内部逻辑。工厂类可以被外界直接调用,创建所需的产品的对象。 抽象(Product)角色 简单工厂模式所创建的所有对象的父类,负责描述所有实例所共有的公共接口。 具体产品(Concrete Product)角色 是该模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。一般来讲是

Mybatis学习第一天 - 2016-07-22 17:07:07

  Mybatis第一天 2      MyBatis介绍 MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。 MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集

IP数据报的分片和组装过程 - 2016-07-22 17:07:07

        一份数据从一个主机通过以太网发送到里一个主机时,是要经过很多层路由转发的。其中过程相对比较的复杂,在这里我们要讨论的是IP在路由中转发时是以怎样的形式转发的和目的主机在接受到这写数据报时又是怎样处理的。        首先我们需要了解的是整个 IP数据报的格式 : IP的转发控制都是由IP数据报的头部决定的。在这里我们就不详细的讨论首部的所有字段,我们就讨论一下个分片有关的总长度字段。        在IP数据报中,总长度是16位的字段,依次数据报的最大长度为2^16-1=65535字节,

Java的纤程库 - Quasar - 2016-07-22 17:07:06

最近遇到的一个问题大概是微服务架构中经常会遇到的一个问题: 服务  A  是我们开发的系统,它的业务需要调用  B  、  C  、  D  等多个服务,这些服务是通过http的访问提供的。 问题是  B  、  C  、  D  这些服务都是第三方提供的,不能保证它们的响应时间,快的话十几毫秒,慢的话甚至1秒多,所以这些服务的Latency比较长。幸运地是这些服务都是集群部署的,容错率和并发支持都比较高,所以不担心它们的并发性能,唯一不爽的就是就是它们的Latency太高了。 系统A会从Client接收

操作系统知识点整理 - 2016-07-22 17:07:03

作业 用户在一次解题或一个事务处理过程中要求计算机系统所做工作的集合。它包括用户程序、所需要的数据及控制命令等。作业是由一系列有序的步骤组成的。 进程 一个程序在一个数据集合上的一次运行过程。所以一个程序在不同数据集合上运行,乃至一个程序在同样数据集合的多次运行都是不同的进程。 线程 线程是进程中的一个实体,是被系统独立调度和执行的基本单位。 进程和线程的区别 进程是程序的一次执行。线程可以理解为进程中执行的一段程序片段。 进程是独立的,这表现在内存空间、上下文环境上;线程运行在进程空间内。一般来讲(不适