如何来一次说干就干的重构 (流程篇)

摘要: 科学的重构流程。原文:如何来一次说干就干的重构 (流程篇)作者:逐鹿 IT, 猛猛如玉Fundebug经授权转载,版权归原作者所有。前言随着公司业务的爆炸式的增长,需求规模和用户规模也迅速地膨胀起来,这样给系统的三高(高性能、高并发、高可用)以及扩展性、可维护性都带来了考验。而旧系统因为早期设计的各种局限性(如早期参与人员的水平、架构设计的前瞻性、老板的急性子等等),逐渐满足不了现状和未来的新需求,暴露出各种问题。开发人员们像是拖着老破车上高速,苦不堪言。(说人话:老系统代码的坑太深了,开发们填不住了,要么被坑埋了,要么弃坑逃跑了…)那么这个时候,通常要面临一个问题:是继续填坑还是跑路走人 选择重构。填坑是不可能的,这辈子都不可能的。而选择重构是需要壮士断腕的勇气,因为重构是一项老大难、一件耗时耗力的事情,且多少会对现有业务开发造成影响,甚至是停滞。因此大多时候得不到产品经理和老板的支持,他们关心的只有一个:下个需求什么时候能上!至于其他的,都是你们研发该操心的。自己选择的重构路,跪着也要走完。如何来一次就干就干的重构呢?根据互联网常见项目重构流程,以及我的亲身参与的重构项目经历,梳理大中小型系统的常见重构流程如下:零:说服业务方重构不单是研发团队的事情,更是整个项目团队的事情。重构可以提升系统的三高,也可以优化改善业务流程,满足新的业务诉求等等。重构需要投入大量资源,必须要得到业务方的支持。通常这个时候需要对他们晓之以理,动之以情,阐述清楚重构的利弊,以及不重构的要害。在得到他们的支持后,重构的工作便正式开展。参与人员:技术 Leader一:树立重构目标,有的放矢重构是一项工程,是一场持久战,它不是一两个迭代、甚至一两个月能做好的事情,需要投入大量的人力、物力、时间精力等。那么在这场旷日持久的战斗中,我们的目标是什么?是通过更优秀更合理的架构来满足系统三高的需求,还是想通过重构来提高代码质量,或者引入新的技术和框架来升级整个系统,抑或通过重构来优化业务流程,实现原来实现不了的需求。有了目标后,才能做到有的放矢。参与人员:技术 Leader,架构师二:确定重构的范围,并对重构作出预测重构通常有以下几个级别的重构平台级别重构。针对整体平台的重构,如阿里早期是 LAMP 架构,后来整体迁移到了 Java 平台。系统级别重构。针对业务系统的重构,如通过引入微服务架构或者 SOA 架构,分解单体应用。架构级别重构。如通过架构的调整和重新设计,改善原有架构的不合理之处。如通过分层使业务解耦,引入缓存设计提升系统高并发等。业务级别重构。常见为某些业务需求因为系统设计的不合理性导致无法满足或有缺陷满足,需要通过业务系统的重构调整或数据库的重构来解决。模块/代码级别重构。这是最常见的重构。通常指使用设计模式、封装继承、优化拆解代码,使得代码的结构更良好,运行效率更高。确定这次重构是属于什么级别,确定重构的整体范围的大小,确定重构的技术选型,进而对重构工作进行科学的评测和预估。比如需要投入哪些成本,需要投入的人力和时间是多少,在重构的过程中能否支撑正常业务需求等等。在有了这些预测后,也对业务方有个交代,尤其是当他们追在后面问什么时候能上新需求。参与人员:技术 Leader,架构师,研发人员三:旧系统的熟悉和业务梳理重构不是和旧系统说散就散,而是要不断和旧系统战斗的过程。知己知彼,百战不殆。重构不仅需要清楚新系统的目标和未来,更需要对旧系统非常熟悉(尤其是坑)。此时需要参与重构的人员(尤其是参与旧系统的人员)来对旧系统业务和系统进行梳理,对原有资料信息进行收益和整理的工作,对旧系统的关键代码和数据库设计进行 Review 等等。以下是重构旧系统前需要准备的常见工作:旧系统资料和信息的收集,包含且不限于系统相关的设计文档和技术文档等文档资料,架构图、UML 图,数据库设计 ER 图等图形化资料业务线和业务流程的梳理,整理业务线上的各大项目、业务流程,并输出为文档旧系统关键代码的 Review有相关疑难点及时与相关与业务线上的人员沟通,将问题解决在”襁褓”中。参与人员:技术 Leader,架构师,研发人员四:数据库重构如果在重构中需要涉及数据库的重构,数据库的重构一般是最先开始的一步。系统需要重构的直接原因,也大多和数据库有关。在数据库重构时,我们清楚旧系统中数据库的各种设计缺陷和使用障碍,那么就可以对症下药,如通过三大范式或反范式来设计表,是否需要分库分表等等。参与人员:DBA,架构师五:后台系统重构后台系统重构前,必须需要依照前文所述的一些设计和技术文档。这些文档输出后并经讨论成型后,架构师进行系统架构设计,后台开发人员进行具体编码工作。通常这个过程是耗时最长的,也是非常重要的一环。后台的架构设计水平,决定着系统重构的水平,业务代码的质量,决定着系统重构的质量。因为这个过程比较漫长,且成果无法立竿见影。所以通常采用敏捷开发的模式,通过迭代的方式来进行后台系统重构。迭代的方式有几个好处:需要将整个重构过程进行有效规划和量化,做到胸有成竹每个阶段能有可见的成果,确保团队在长时间的重构过程中不陷于泥潭对已重构好的部分可以及时进行联调测试或观察,不断在迭代中总结、在总结中迭代另外在后台系统重构时,也需要有明确量化的目标和标准,比如各系统和业务模块支持多少 QPS,接口响应时间多长时间等,这样团队才能在重构的过程中不至于为了重构而重构。在重构过程中,定期进行 Code Review,及时发现重构的问题和质量的问题,避免出现破窗效应,引入拙劣的设计或垃圾代码,进而破坏整个系统。参与人员:技术 Leader,架构师,研发人员六:数据迁移与检查如果涉及数据库重构时,在新的数据库设计好后,就会有面临数据迁移的问题。一般分为全量迁移和增量迁移,全量迁移是将旧系统的数据一次性迁移到新的数据库中,增量迁移是在实行全量迁移后旧系统新产生的数据迁移到新系统上来,增量迁移一直到旧系统下线不再产生新数据后。通常迁移都是通过编写脚本或程序来实现,拒绝人工操作。迁移后自然需要对比新旧系统的数据,同样可以通过脚本或程序来进行对比,查缺补漏,定位分析。参与人员:DBA,研发人员七:系统检查、联调与测试在后台系统重构到一定程度时,同样也需要编写脚本和程序来对新旧系统的业务接口进行检查,及时发现重构中的问题,必要时候进行架构调整和数据库调整。当然,在重构时,开发人员能提高单元测试覆盖率当然是更好不过。当各系统和模块的依赖解决的差不多时,可以开始联调工作。当然最后还需要系统性的测试,如功能性测试、稳定性测试、性能测试,本地测试、模拟线上环境测试等。测试中发现的问题经验证修复后,达到上线的标准,即可灰度上线。参与人员:架构师,研发人员,测试人员八:灰度发布与观察万里长征已经走到最后,也到了最紧要的关头。灰度发布时,只接入一小部分流量,并及时跟踪和分析线上的 log 与监控告警,一有问题及时解决。当新系统趋于稳定时,可以逐渐加大灰度发布的范围和接入的流量,同时继续跟踪线上 log 与监控告警。参与人员:运维人员,测试人员,研发人员九:系统切换在系统切换时,需要提前制订系统切换方案,包含相应的规划与流程,甚至是应急预案与回滚方案,避免走一步看一步。切换完成后,新系统完全替换旧系统,旧系统下线,完成重构。参与人员:运维人员,测试人员结语通过上述几个步骤后,我们成功对系统进行重构。重构是一项大工程,但经历重构后的系统也并非完美无缺。重构不是终点,更像是起点。关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!

April 19, 2019 · 1 min · jiezi

基于泛型编程的序列化实现方法

写在前面序列化是一个转储-恢复的操作过程,即支持将一个对象转储到临时缓冲或者永久文件中和恢复临时缓冲或者永久文件中的内容到一个对象中等操作,其目的是可以在不同的应用程序之间共享和传输数据,以达到跨应用程序、跨语言和跨平台的解耦,以及当应用程序在客户现场发生异常或者崩溃时可以即时保存数据结构各内容的值到文件中,并在发回给开发者时再恢复数据结构各内容的值以协助分析和定位原因。泛型编程是一个对具有相同功能的不同类型的抽象实现过程,比如STL的源码实现,其支持在编译期由编译器自动推导具体类型并生成实现代码,同时依据具体类型的特定性质或者优化需要支持使用特化或者偏特化及模板元编程等特性进行具体实现。Hello World#include <iostream>int main(int argc, char* argv[]){ std::cout << “Hello World!” << std::endl; return 0;}泛型编程其实就在我们身边,我们经常使用的std和stl命名空间中的函数和类很多都是泛型编程实现的,如上述代码中的std::cout即是模板类std::basic_ostream的一种特化namespace std{ typedef basic_ostream<char> ostream;}从C++的标准输入输出开始除了上述提到的std::cout和std::basic_ostream外,C++还提供了各种形式的输入输出模板类,如std::basic_istream, std::basic_ifstream,std::basic_ofstream, std::basic_istringstream,std::basic_ostringstream等等,其主要实现了内建类型(built-in)的输入输出接口,比如对于Hello World可直接使用于字符串,然而对于自定义类型的输入输出,则需要重载实现操作符>>和<<,如对于下面的自定义类class MyClip{ bool mValid; int mIn; int mOut; std::string mFilePath;};如使用下面的方式则会出现一连串的编译错误MyClip clip;std::cout << clip;错误内容大致都是一些clip不支持<<操作符并在尝试将clip转为cout支持的一系列的内建类型如void*和int等等类型时转换操作不支持等信息。为了解决编译错误,我们则需要将类MyClip支持输入输出操作符>>和<<,类似实现代码如下inline std::istream& operator>>(std::istream& st, MyClip& clip){ st >> clip.mValid; st >> clip.mIn >> clip.mOut; st >> clip.mFilePath; return st;}inline std::ostream& operator<<(std::ostream& st, MyClip const& clip){ st << clip.mValid << ’ ‘; st << clip.mIn << ’ ’ << clip.mOut << ’ ‘; st << clip.mFilePath << ’ ‘; return st;}为了能正常访问类对象的私有成员变量,我们还需要在自定义类型里面增加序列化和反序列化的友元函数(回忆一下这里为何必须使用友元函数而不能直接重载操作符>>和<<?),如friend std::istream& operator>>(std::istream& st, MyClip& clip);friend std::ostream& operator<<(std::ostream& st, MyClip const& clip);这种序列化的实现方法是非常直观而且容易理解的,但缺陷是对于大型的项目开发中,由于自定义类型的数量较多,可能达到成千上万个甚至更多时,对于每个类型我们则需要实现2个函数,一个是序列化转储数据,另一个则是反序列化恢复数据,不仅仅增加了开发实现的代码数量,如果后期一旦对部分类的成员变量有所修改,则需要同时修改这2个函数。同时考虑到更复杂的自定义类型,比如含有继承关系和自定义类型的成员变量class MyVideo : public MyClip{ std::list<MyFilter> mFilters;};上述代码需要转储-恢复类MyVideo的对象内容时,事情会变得更复杂些,因为还需要转储-恢复基类,同时成员变量使用了STL模板容器list与自定义类’MyFilter`的结合,这种情况也需要自己去定义转储-恢复的实现方式。针对以上疑问,有没有一种方法能减少我们代码修改的工作量,同时又易于理解和维护呢?Boost序列化库对于使用C++标准输入输出的方法遇到的问题,好在Boost提供了一种良好的解决方式,则是将所有类型的转储-恢复操作抽象到一个函数中,易于理解,如对于上述类型,只需要将上述的2个友元函数替换为下面的一个友元函数template<typename Archive> friend void serialize(Archive&, MyClip&, unsigned int const);友元函数的实现类似下面的样子template<typename A>void serialize(A &ar, MyClip &clip, unsigned int const ver){ ar & BOOST_SERIALIZATION_NVP(clip.mValid); ar & BOOST_SERIALIZATION_NVP(clip.mIn); ar & BOOST_SERIALIZATION_NVP(clip.mOut); ar & BOOST_SERIALIZATION_NVP(clip.mFilePath);}其中BOOST_SERIALIZATION_NVP是Boost内部定义的一个宏,其主要作用是对各个变量进行打包。转储-恢复的使用则直接作用于操作符>>和<<,比如// storeMyClip clip;······std::ostringstream ostr;boost::archive::text_oarchive oa(ostr);oa << clip;// loadstd::istringstream istr(ostr.str());boost::archive::text_iarchive ia(istr);ia >> clip;这里使用的std::istringstream和std::ostringstream即是分别从字符串流中恢复数据以及将类对象的数据转储到字符串流中。对于类MyFilter和MyVideo则使用相同的方式,即分别增加一个友元模板函数serialize的实现即可,至于std::list模板类,boost已经帮我们实现了。这时我们发现,对于每一个定义的类,我们需要做的仅仅是在类内部声明一个友元模板函数,同时类外部实现这个模板函数即可,对于后期类的成员变量的修改,如增加、删除或者重命名成员变量,也仅仅是修改一个函数即可。Boost序列化库已经足够完美了,但故事并未结束!在用于端上开发时,我们发现引用Boost序列化库遇到了几个挑战端上的编译资料很少,官方对端上编译的资料基本没有,在切换不同的版本进行编译时经常会遇到各种奇怪的编译错误问题Boost在不同的C++开发标准之间兼容性不够好,尤其是使用libc++标准进行编译链接时遇到的问题较多Boost增加了端上发行包的体积Boost每次序列化都会增加序列化库及版本号等私有头信息,反序列化时再重新解析,降低了部分场景下的使用性能基于泛型编程的序列化实现方法为了解决使用Boost遇到的这些问题,我们觉得有必要重新实现序列化库,以剥离对Boost的依赖,同时能满足如下要求由于现有工程大量使用了Boost序列化库,因此兼容现有的代码以及开发者的习惯是首要目标尽量使得代码修改和重构的工作量最小兼容不同的C++开发标准提供比Boost序列化库更高的性能降低端上发行包的体积为了兼容现有使用Boost的代码以及保持当前开发者的习惯,同时使用代码修改的重构的工作量最小,我们应该保留模板函数serialize,同时对于模板函数内部的实现,为了提高效率也不需要对各成员变量重新打包,即直接使用如下定义#define BOOST_SERIALIZATION_NVP(value) value对于转储-恢复的接口调用,仍然延续目前的调用方式,只是将输入输出类修改为alivc::text_oarchive oa(ostr);alivc::text_iarchive ia(istr);好了,到此为止,序列化库对外的接口工作已经做好,剩下的就是内部的事情,应该如何重新设计和实现序列化库的内部框架才能满足要求呢?先来看一下当前的设计架构的处理流程图比如对于转储类text_oarchive,其支持的接口必须包括explicit text_oarchive(std::ostream& ost, unsigned int version = 0);template <typename T> text_oarchive& operator<<(T& v);template <typename T> text_oarchive& operator&(T& v);开发者调用操作符函数<<时,需要首先回调到相应类型的模板函数serialize中template <typename T>text_oarchive& operator<<(T& v){ serialize(*this, v, mversion); return *this;}当开始对具体类型的各个成员进行操作时,这时需要进行判断,如果此成员变量的类型已经是内建类型,则直接进行序列化,如果是自定义类型,则需要重新回调到对应类型的模板函数serialize中template <typename T>text_oarchive& operator&(T& v){ basic_save<T>::invoke(*this, v, mversion); return *this;}上述代码中的basic_save::invoke则会在编译期完成模板类型推导并选择直接对内建类型进行转储还是重新回调到成员变量对应类型的serialize函数继续重复上述过程。由于内建类型数量有限,因此这里我们选择使模板类basic_save的默认行为为回调到相应类型的serialize函数中template <typename T, bool E = false>struct basic_load_save{ template <typename A> static void invoke(A& ar, T& v, unsigned int version) { serialize(ar, v, version); }};template <typename T>struct basic_save : public basic_load_save<T, std::is_enum<T>::value>{};这时会发现上述代码的模板参数多了一个参数E,这里主要是需要对枚举类型进行特殊处理,使用偏特化的实现如下template <typename T>struct basic_load_save<T, true>{ template <typename A> static void invoke(A& ar, T& v, unsigned int version) { int tmp = v; ar & tmp; v = (T)tmp; }};到这里我们已经完成了重载操作符&的默认行为,即是不断进行回溯到相应的成员变量的类型中的模板函数serialize中,但对于碰到内建模型时,我们则需要让这个回溯过程停止,比如对于int类型template <typename T>struct basic_pod_save{ template <typename A> static void invoke(A& ar, T const& v, unsigned int) { ar.template save(v); }};template <>struct basic_save<int> : public basic_pod_save<int>{};这里对于int类型,则直接转储整数值到输出流中,此时text_oarchive则还需要增加一个终极转储函数template <typename T>void save(T const& v){ most << v << ’ ‘;}这里我们发现,在save成员函数中,我们已经将具体的成员变量的值输出到流中了。对于其它的内建类型,则使用相同的方式处理,要以参考C++ std::basic_ostream的源码实现。相应的,对于恢复操作的text_iarchive的操作流程如下图测试结果我们对使用Boost以及重新实现的序列化库进行了对比测试,其结果如下代码修改的重构的工作非常小,只需要删除Boost的相关头文件,以及将boost相关命名空间替换为alivc,BOOST_SERIALIZATION_FUNCTION以及BOOST_SERIALIZATION_NVP的宏替换Android端下的发行包体积大概减少了500KB目前的消息处理框架中,处理一次消息的平均时间由100us降低到了25us代码实现约300行,更轻量级未来还能做什么由于当前项目的原因,重新实现的序列化还没有支持转储-恢复指针所指向的内存数据,但当前的设计框架已经考虑了这种拓展性,未来会考虑支持。总结泛型编程能够大幅提高开发效率,尤其是在代码重用方面能发挥其优势,同时由于其类型推导及生成代码均在编译期完成,并不会降低性能序列化对于需要进行转储-恢复的解耦处理以及协助定位异常和崩溃的原因分析具有重要作用利用C++及模板自身的语言特性优势,结合合理的架构设计,即易于拓展又能尽量避免过度设计参考资料https://www.ibm.com/developerworks/cn/aix/library/au-boostserialization/本文作者:lifesider阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 25, 2019 · 2 min · jiezi

代码重构那些事儿

大家好,这是我今天演讲的目录,分Java,JavaScript,ABAP三门编程语言来讲述。Java•JAD•javap•Java Decompiler•Source Monitor•Visual VM•Refactor Menu in EclipseABAP•Code inspector•Refactor feature in AIE•Code coverageJavaScript•ESLint for Fiori Apps•Check Jenkins build log•JSlint for Sublime Text 2•Code check in WebIDE•Profile in Chrome在方法里引入一个布尔类型的参数控制方法的行为,这种做法正确吗?看看stackoverflow上是怎么说的。Java里定义常量的最佳实践:http://developer.51cto.com/ar…Java里这两种定义常量的方法,哪种更好?package one;public interface Constants { String NAME = “孙悟空”; int BP = 10000;}或package two;public class Constants { public static final String NAME = “贝吉塔”; public static final int BP = 9000;}为什么我们不应该在Java 接口中使用Array:https://eclipsesource.com/blo…避免Array的原因之一:Array若使用不当,会造成性能问题避免Array的原因之一:Array若使用不当,会造成性能问题避免Array的原因之二:Array是面向过程编程领域的概念,使用Java面向对象的集合类,比如List,而不是Array看个具体例子:String[] array = { “乔布斯”, “张小龙” };List list = Arrays.asList( array );System.out.println( list );// 打印输出 [乔布斯, 张小龙]System.out.println( array );// -> [Ljava.lang.String;@6f548414list.equals( Arrays.asList( “乔布斯”, “张小龙” ) )// -> truearray.equals( new String[] { “乔布斯”, “张小龙” } )// -> false看出差距了吧?Arrays不是类型安全的!下面的代码能通过编译,但是运行时会报ArrayStoreException的异常:Number[] numbers = new Integer[10];numbers[0] = Long.valueOf( 0 ); 而使用JDK的集合类比如List,就能在编译器即检测出这类错误。Javascript里有趣的逗号function a() { console.log(“I was called!”); return “Jerry”;}var b = a(), a;然后执行下面的代码:console.log(b);会打印出Jerry再看这段代码:var d = (function c(){ return a(),a;})();console.log(d);会打印出:I was called!function a() { console.log(“I was called!”); return “Jerry”;}再看这段代码呢?(function() { var e = f = 1;})();直接报错:Uncaught ReferenceError: f is not definedJavaScript里有趣的分号var b = function(para) { return { doSomething: function() { console.log(“hello: " + para); return para; } }}var a = 1, x = 3, y = 4, ss = a + b(x + y).doSomething() // 打印出 hello: 7console.log(s) // 打印出 8function test(i){ var result = i++; return result}console.log(“test: " + test(3)) // 打印出undefined继续看这段代码s = function(x){ console.log(“called: " + x ); return x}(1 + 2).toString()s = function(x){ console.log(“called: " + x ); return x}(1 + 2).toString()// 打印出 called: 3小技巧 - 如何把您自己增强逻辑植入到legacy遗留代码中var bigFunction = function() { // big logic console.log(“big logic”); // 这句话模拟我们在一段很冗长的遗留代码里植入自己的新逻辑}// 下面这种解决方案不会直接修改遗留函数本身,显得比较优雅var _old = bigFunction;bigFunction = function() { if ( _old ) { _old(); } console.log(“our own enhancement”);}bigFunction();// 第三种解决方案采用了面向切片编程思想,显得更加高级var bigFunction = function() { // big logic console.log(“big logic”);}bigFunction = ( bigFunction || function() {} ).after( function() { console.log(“our own logic”);});bigFunction();如何优雅的在一个函数里增添性能测试统计的工具代码var append_doms = function() { var d = new Date(); // dirty code - nothing to do with application logic!!! for( var i = 0; i < 100000; i++) { var div = document.createElement( “div”); document.body.appendChild(div); } // dirty code - nothing to do with application logic!!! console.log(” time consumed: " + ( new Date() - d));};function test() { append_doms();}传统方案:在充满了业务逻辑的函数体里强行加入红色标准的搜集性能测试的工具代码,这个实现显得很丑陋:再看看采用面向切片编程思路的解决方案:AOP - Aspect Oriented Programmingvar append_doms = function() { for( var i = 0; i < 100000; i++) { var div = document.createElement( “div”); document.body.appendChild(div); }};var log_time = function( func, log_name) { return func = ( function() { var d; return func.before( function(){ d = new Date(); }).after( function(){ console.log( log_name + ( new Date() - d)); }); })(); };function test() { log_time(append_doms, “consumed time: “)();}如何避免代码中大量的IF - ELSE 检查在调用真正的OData API之前,系统有大量的IF ELSE对API的输入参宿进行检查:var send = function() { var value = input.value; if( value.length === ’’ ) { return false; } else if( value.length > MAX_LENGTH) { return false; } … // lots of else else { // call OData API }}更优雅的解决方案:把这些不同的检查规则封装到一个个JavaScript函数里,再把这些函数作为一个规则对象的属性:var valid_rules = { not_empty: function( value ) { return value.length !== ‘’; }, max_length: function( value ) { return value.length <= MAX_LENGTH ; } }实现一个新的检查函数,变量检查对象的属性,执行校验逻辑:var valid_check = function() { for( var i in valid_rules ) { if ( vali_rules[i].apply( this, arguments) === false ) { return false; } }}现在的OData调用函数非常优雅了:var send = function( value ) { if ( valid_check( value ) === false ) { return; } // call OData API}通过这种方式消除了IF ELSE。另一种通过职责链 Chain of Responsibility 的设计模式 design pattern消除IF ELSE分支的代码重构方式:先看传统方式的实现:// Priority: ActiveX > HTML5 > Flash > Form(default)function isActiveXSupported(){ //… return false;}function isHTML5Supported(){ //… return false;}function isFlashSupported(){ //… return false;}好多的IF -ELSE啊:var uploadAPI;if ( isActiveXSupported()) { // lots of initialization work uploadAPI = { “name”: “ActiveX”};}else if( isHTML5Supported()) { // lots of initialization work uploadAPI = { “name”: “HTML5”};}else if( isFlashSupported()) { // lots of initialization work uploadAPI = { “name”: “Flash”};}else { // lots of initialization work uploadAPI = { “name”: “Form”};}console.log(uploadAPI);再看职责链设计模式的实现:Chain of Responsibilityvar getActiveX = function() { try { // lots of initialization work return { “name”: “ActiveX”}; } catch (e) { return null; }}var getHTML5 = function() { try { // lots of initialization work return { “name”: “HTML5”}; } catch (e) { return null; }}代码整洁优雅:var uploadAPI = getActiveX.after(getHTML5).after(getFlash).after(getForm)();console.log(uploadAPI);Java中的Stringpublic class stringTest {public static void main(String[] args) { String userName = “Jerry”; String skill = “JS”; String job = “Developer”; String info = userName + skill + job; System.out.println(info);}}用javap将上面的Hello World程序反编译出来学习:要获取更多Jerry的原创文章,请关注公众号"汪子熙”: ...

February 3, 2019 · 4 min · jiezi

编写可维护的代码

编写可维护的代码前言我们在修改他人代码的时候,阅读他人代码所花的时间经常比实现功能的时间还要更多如果程序结构不清晰,代码混乱 。牵一发而动全身。那维护起来就更难维护了可读性可理解性:他人可以接手代码并理解它直观性 : 代码逻辑清晰可调试性 :出错时,方便定位问题所在如何提高可读性代码格式化适当添加注释函数与方法大段代码注释需有意义如何优化代码找出代码的坏味道使用重构手法将其解决掉代码的坏味道在我们的程序中,可以闻到很多的坏味道。主要有以下这些点命名不规范或无意义命名存在使用缩写、不规范、无意义例子:var a = xxx,b = xxx重复代码相同(或相似)的代码在项目中出现了多次,如果需求发生更改,则需要同时修改多个地方过长函数程序越长越难理解,一个函数应该只完成一个功能过长的类一个类的职责过多,一个类应该是一个独立的整体。过长参数列表太长的参数列表难以理解,不易使用。当需要修改的时候,会更加容易出错数据泥团有些数据项总是成群结队的待在一起。例如两个类中相同的字段、许多函数签名相同的参数。这些都应该提炼到一个对象中,将很多参数列缩短,简化函数调用类似的函数整体上实现的功能差不多,但是由于有一点点区别。所以写成了多个函数重构手法提炼函数针对一个比较长的函数,提炼成一个个完成特定功能的函数。例子// 提炼前function test11() { var day = $(‘day’); var yearVal = ‘2016’; var monthVal = ‘10’; var dayVal = ‘10’; day.val(dayVal); switch (monthVal) { case 4: case 6: case 9: case 11: if (dayVal > 30) { day.val(30); } break; case 2: if ( yearVal % 4 == 0 && (yearVal % 100 != 0 || yearVal % 400 == 0) && monthVal == 2 ) { if (dayVal > 29) { day.val(29); } } else { if (dayVal > 28) { day.val(28); } } break; default: if (dayVal > 31) { day.val(31); } }}// 提炼后function test12() { var day = $(‘day’); var yearVal = ‘2016’; var monthVal = ‘10’; var dayVal = ‘10’; var maxDay = getMaxDay(yearVal, monthVal); if (dayVal > maxDay) { day.val(maxDay); } else { day.val(dayVal); }}function getMaxDay(year, month) { var maxDay = 0; switch (month) { case 4: case 6: case 9: case 11: maxDay = 30; break; case 2: if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) { maxDay = 29; } else { maxDay = 28; } break; default: maxDay = 31; } return maxDay;}例子中,提炼前的代码,需要很费劲的看完整个函数,才会明白做了什么处理,提炼后的代码。只需要稍微看一下,就知道 getMaxDay 是获取当前月份的最大天数优点:如果每个函数的粒度都很小,那么函数被复用的机会就更大;这会使高层函数读起来就想一系列注释;如果函数都是细粒度,那么函数的覆写也会更容易些内联函数有时候,一个函数的本体与函数名一样简单易懂,就要用到这种手法。这种手法用于处理优化过度的问题举个例子:function biggerThanZero(num) { return num > 0;}function test() { var num = 10; if (biggerThanZero(num)) { //do something }}//内联后function test() { var num = 10; if (num > 0) { //do something }}引入解释性变量当表达式比较复杂难以阅读的时候,就可以通过临时变量来帮助你将表达式分解为容易管理的形式有些时候,运用提炼函数会更好一点举两个简单的例子:// 例子 1// beforefunction test2() { if ( platform.toUpperCase().indexOf(‘MAC’) > -1 && browser.toUpperCase().indexOf(‘IE’) > -1 && wasInitialized() && resize > 0 ) { // do something }}// afterfunction test2() { var isMacOs = platform.toUpperCase().indexOf(‘MAC’) > -1; var isIEBrowser = browser.toUpperCase().indexOf(‘IE’) > -1; var wasResized = resize > 0; if (isMacOs && isIEBrowser && wasInitialized() && wasResized) { // do something }}// ————————————————–// 例子2// beforefunction caluPrice(quantity, itemPrice) { return ( quantity * itemPrice - Math.max(0, quantity - 500) * itemPrice * 0.05 + Math.min(quantity * itemPrice * 0.1, 100) );}// afterfunction caluPrice(quantity, itemPrice) { var basePrice = quantity * itemPrice; var discount = Math.max(0, quantity - 500) * itemPrice * 0.05; var shiping = Math.min(basePrice * 0.1, 100); return basePrice - discount + shiping;}在两个例子中,引入解释性的变量之后,可读性大大增加。函数的意图就比较明显,单看变量命名就已经能大概知道具体的实现分解临时变量除了 for 循环里用来收集结果的变量,其他的临时变量都应该只被赋值一次。因为被赋值超过一次,就意味着他在函数中承担了多个责任。一个变量承担多个责任。会令代码看起来容易迷惑举个例子:// 分解临时变量// beforefunction test3() { var temp = 2 * (width + height); console.log(temp); // do something temp = height * width; // do something console.log(temp);}// afterfunction test4() { var perimeter = 2 * (width + height); console.log(perimeter); // do something var area = height * width; // do something console.log(area);}在这个例子中,temp 分别被赋予了两次,如果代码块较长的情况,会增加风险,因为你不知道他在哪里被改掉了替换算法当你重构的时候,发现实现同样的功能有一个更清晰的方式,就应该将原有的算法替换成你的算法。举个例子:// 替换算法// beforefunction getWeekDay() { var weekStr = ‘’; switch (date.format(’d’)) { case 0: weekStr = ‘日’; break; case 1: weekStr = ‘一’; break; case 2: weekStr = ‘二’; break; case 3: weekStr = ‘三’; break; case 4: weekStr = ‘四’; break; case 5: weekStr = ‘五’; break; case 6: weekStr = ‘六’; break; } return weekStr;}// afterfunction getWeekDay() { var weekDays = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]; return weekDays[date.format(’d’)];}以字面常量取代魔法数(eg:状态码)在计算机科学中,魔法数是历史最悠久的不良现象之一。魔法数是指程序中莫名其妙的数字。拥有特殊意义,却又不能明确表现出这种意义的数字举个例子:// beforefunction test5(x) { if (x == 1) { console.log(‘完成’); } else if (x == 2) { console.log(‘上传中’); } else if (x == 3) { console.log(‘上传失败’); } else { console.log(‘未知的错误’); }}function test6(x) { if (x == 3) { // do something }}// aftervar UploadStatus = { START: 0, UPLOADING: 1, SUCCESS: 2, ERROR: 3, UNKNOWN: 4};function test7(x) { if (x == UploadStatus.START) { console.log(‘未开始’); } else if (x == UploadStatus.UPLOADING) { console.log(‘上传中’); } else if (x == UploadStatus.SUCCESS) { console.log(‘上传成功’); } else if (x == UploadStatus.ERROR) { console.log(‘上传失败’); } else { console.log(‘未知的错误’); }}function test8(x) { if (x == UploadStatus.ERROR) { // do something }}对于魔法数,应该用一个枚举对象或一个常量来赋予其可见的意义。这样,你在用到的时候,就能够明确的知道它代表的是什么意思而且,当需求变化的时候,只需要改变一个地方即可分解条件表达式复杂的条件逻辑是导致复杂度上升的地点之一。因为必须编写代码来处理不同的分支,很容易就写出一个相当长的函数将每个分支条件分解成新函数可以突出条件逻辑,更清楚表明每个分支的作用以及原因举个例子:// 分解条件表达式// 商品在冬季和夏季单价不一样// beforevar SUMMER_START = ‘06-01’;var SUMMER_END = ‘09-01’;function test9() { var quantity = 2; var winterRate = 0.5; var winterServiceCharge = 9; var summerRate = 0.6; var charge = 0; if (date.before(SUMMER_START) || date.after(SUMMER_END)) { charge = quantity * winterRate + winterServiceCharge; } else { charge = quantity * summerRate; } return charge;}// afterfunction test9() { var quantity = 2; return notSummer(date) ? winterCharge(quantity) : summerCharge(quantity);}function notSummer(date) { return date.before(SUMMER_START) || date.after(SUMMER_END);}function summerCharge(quantity) { var summerRate = 0.6; return quantity * summerRate;}function winterCharge(quantity) { var winterRate = 0.5; var winterServiceCharge = 9; return quantity * winterRate + winterServiceCharge;}合并条件表达式当发现一系列的条件检查,检查条件不一样,但是行为却一致。就可以将它们合并为一个条件表达式举个例子:// 合并条件表达式// beforefunction test10(x) { var isFireFox = ‘xxxx’; var isIE = ‘xxxx’; var isChrome = ‘xxxx’; if (isFireFox) { return true; } if (isIE) { return true; } if (isChrome) { return true; } return false;}// afterfunction test10(x) { var isFireFox = ‘xxxx’; var isIE = ‘xxxx’; var isChrome = ‘xxxx’; if (isFireFox || isIE || isChrome) { return true; } return false;}合并后的代码会告诉你,实际上只有一个条件检查,只是有多个并列条件需要检查而已合并重复的条件片段条件表达式上有着相同的一段代码,就应该将它搬离出来// 合并重复片段// beforefunction test11(isSpecial) { var total, price = 1; if (isSpecial) { total = price * 0.95; // 这里处理一些业务 } else { total = price * 0.8; // 这里处理一些业务 }}// afterfunction test12(isSpecial) { var total, price = 1; if (isSpecial) { total = price * 0.95; } else { total = price * 0.8; } // 这里处理一些业务}在不同的条件里面做了同样的事情,应该将其抽离出条件判断。这样代码量少而且逻辑更加清晰以卫语句取代嵌套条件表达式如果某个条件较为罕见,应该单独检查该条件,并在该条件为真时立即从函数中返回。这样的检查就叫卫语句举个例子:// 以卫语句取代嵌套条件表达式// beforefunction getPayMent() { var result = 0; if (isDead) { result = deadAmount(); } else { if (isSepartated) { result = separtedAmount(); } else { if (isRetired) { result = retiredAmount(); } else { result = normalPayAmount(); } } } return result;}// afterfunction getPayMent() { if (isDead) { return deadAmount(); } if (isSepartated) { return separtedAmount(); } if (isRetired) { return retiredAmount(); } return normalPayAmount();}函数改名(命名)当函数名称不能表达函数的用途,就应该改名变量和函数应使用合乎逻辑的名字。eg:获取产品列表 -> getProductList()变量名应为名词,因为变量名描述的大部分是一个事物。eg: 产品 -> product函数名应为动词开始,因为函数描述的是一个动作eg:获取产品列表 -> getProductList()将查询函数和修改函数分开如果某个函数只向你提供一个值,没有任何副作用。这个函数就可以任意的调用。这样的函数称为纯函数如果遇到一个既有返回值,又有副作用的函数。就应该将查询与修改动作分离出来举个例子:// beforefunction test13(people) { for (var i = 0, len = people.length; i < len; i++) { if (people[i].name == ‘andy’) { // do something 例如进行DOM 操作之类的 return ‘andy’; } if (people[i].name == ‘ChunYang’) { // do something 例如进行DOM 操作之类的 return ‘ChunYang’; } }}// afterfunction test14(people) { var p = find(people); // do something 例如进行DOM 操作之类的 // doSomeThing(p);}function find(people) { for (var i = 0, len = people.length; i < len; i++) { if (people[i].name == ‘andy’) { return ‘andy’; } if (people[i].name == ‘ChunYang’) { return ‘ChunYang’; } }}令函数携带参数如果发现两个函数,做着类似的工作。区别只在于其中几个变量的不同。就可以通过参数来处理。这样可以去除重复的代码,提高灵活性关键点: 找出不同的地方和重复的地方。推荐书籍《重构 改善既有代码的设计 》 基于 java 的《代码大全》相关链接个人博客代码片段 ...

December 20, 2018 · 5 min · jiezi