代码逻辑是分方法写好 还是在一个方法写好_那些两年前的代码

点击上方的终端研发部,右上角选择“设为星标

每日早9点半,技术文章准时送上

公众号后台回复“学习”,获取作者独家秘制精品资料

05c1b278818c068fb1f5022f5f34d0d9.png

往期文章

Flutter也上架构了!

记五月的一个Android面试经

Flutter  + MVP +Kotlin 实战!

漫画:App 防止 Fiddler 抓包小技巧!

漫画: 解密IP 、TCP和DNS与HTTP 的亲密关系

从Flutter,Weex,RN 几大跨平台,论虚拟 DOM?

05c1b278818c068fb1f5022f5f34d0d9.png

来源:FeelsChaotic

原文链接:https://www.jianshu.com/p/0e31122c38f7

为什么我们谈论代码?

也许有人会认为,谈论代码已经有点落后了——代码不再是问题,我们应当关注模型、需求、功能设计上。运用 Google 的自动编程框架 AutoML 和UIzard 的 pix2code就可以自动生成代码,看起来我们正在临近代码的终结点。

但是,注意但是!我们永远无法抛弃代码, 就算语言继续抽象,领域特定语言数量继续增加,也终结不了代码。因为代码呈现了需求的细节,这些细节无法被忽略或抽象,必须明确之。将需求明确到机器可以执行的细节程度,就是编程要做的事。

我们可以创造各种与需求接近的语言,我们可以创造帮助把需求解析和汇整为框架结构的各种工具。然而,我们永远无法抛弃需求中的精确性和细节 —— 所以代码永存。

为什么我们总是在写烂代码?

有的人是因为大量的业务工作导致失去思考,有的人则是把提高代码质量寄希望于重构,但是往往在写烂代码的人不知道自己写的就是烂代码,他们没有掌握好代码的技巧,或者根本没有见过好代码,从而不知道什么是好的实践。

针对这种情况,下面会介绍好代码的特性和重构的技巧。从一个小的命名规范开始、逐步讲到函数、再到类、再到模块单元、乃至整个设计。

培养你的代码感

就好像好的读者不一定是好的作者,能分辨整洁代码和肮脏代码,也不意味着会写整洁代码。

写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的「代码感」。这种「代码感」就是关键所在。有些人生而有之,有些人费点劲才能得到。缺乏「代码感」的程序员,看混乱是混乱,无处着手。有「代码感」的程序员才能从混乱中看出其他的可能与变化,选出最好的方案。

想要得到「代码感」,最根本的途径是反复练习,接下来我将介绍大量的重构技巧和 demo ,强烈建议你在阅读时带上思考,对照自己的代码。

好代码需要遵循什么?重构有哪些技巧?

技巧一:起一个清晰、合理、有意义的命名

有意义的命名是体现表达力的一种方式。

1、方法名应当是动词或动词短语,表达你的意图。如 deletePage 或 savePage。

2、类名和对象名应该是名词或名词短语。如 Customer、WikiPage、Account和AddressParser。

3、不要以数字来命名,除非是 changeJson2Map() 这种情况。

4、单字母名称仅用于短方法中的本地变量。比如循环体内的 i,但是你不应该在类变量里使用 i ,名称长短应与其作用域大小相对应

5、别给名称添加不必要的语境。对于 Address 类的实体来说,AccountAddress 和 CustomerAddress 都是不错的名称,不过用在类名上就不太好了。

6、遵循专业的术语。如果名词无法用英文表达,一定要用中文拼音,则不能用拼音缩写

命名我往往会修改好几次才会定下名字来,借助 IDE 重命名的代价极低,所以当你遇到不合理的命名时,不要畏惧麻烦,直接修改吧!

技巧二:保持函数短小、少的入参、同一抽象层级

我们都知道函数要尽量短小,职责要单一。但是你有没有忽视过其他的问题?

来看下这段糟糕的示例代码:

89f2cfafb79437ddc86409b27bbc5e48.png

这段代码至少犯了 4 个错误

  1. 用了标识参数 isFormat,这样方法签名立刻变得复杂起来,也表示了函数不止做一件事情,这应该把函数一分为二。

  2. 使用了多元参数。如果函数需要两个、三个或三个以上的参数,就说明其中一些参数应该封装成类了。

  3. 使用了输出参数 url,输出参数比复杂的输入参数还要难以理解。读函数时,我们惯于认为信息通过参数输入函数,通过返回值从函数中输出。我们不期望信息通过参数输出,输出参数往往包含着陷阱。如果函数要对输入参数进行转换操作,转换结果就该体现为返回值上。

    例: appendFooter(s);这个函数是把 s添加到什么东西后面吗?或者它把什么东西添加到了 s后面?s是输入参数还是输出参数?如果是要给s添加个Footer,最好是这样设计:s.appendFooter(); 。

  4. 没有保持同一抽象层级。函数中混杂不同抽象层级,去拼接代码的同时又发起了网络请求,还处理了请求结果,这往往让人迷惑。

思考下,你会怎么重构这段代码?

我们展示重构后的情况:

a09167c8ee8386ade8434dde441fe741.png

阅读这样的代码你会觉得很舒服,代码拥有自顶向下的阅读顺序,主程序就像是一系列 TO 起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续 TO 起头段落,呈现出总-分的结构。

技巧三:短小、单一权责、内聚的类,暴露操作,隐藏数据细节

类的名称其实就表现了权责,如果无法为某个类命以精确的名称,说明这个类太长了,就应该拆分为几个高内聚的小类。

那么怎么评估类的内聚性?类中的方法和变量互相依赖、互相结合成一个逻辑整体,如果类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。

保持内聚性就会得到许多短小的类,仅仅是将较大的函数切分为小函数,就将导致更多的类出现。想想看一个有许多变量的大函数。你想把该函数中某一小部分拆解成单独的函数。不过,你想要拆出来的代码使用了该函数中声明的 4 个变量。是否必须将这 4 个变量都作为参数传递到新函数中去呢?

完全没必要!只要将 4 个变量提升为类的实体变量,完全无需传递任何变量就能拆解代码了。将函数拆分为小块后,你会发现类也丧失了内聚性,因为堆积了越来越多被少量函数共享的实体变量。

等一下!如果有些函数想要共享某些变量,为什么不让它们拥有自己的类呢?当类的变量越来越多,且变量的无关性越来越大,就拆分它!所以,将大函数拆为许多小函数,往往也是将类拆分为多个小类的时机。

你以为这就结束了?停止你乱加取值器和赋值器的行为!我们不能暴露变量的数据细节和数据形态,应该以抽象形态表述数据。

著名的得墨忒耳律认为:模块不应了解它所操作对象的内部情形,即每个单元(对象或方法)应当对其他单元只拥有有限的了解,不应该有链式调用。

哈?我们觉得方便的链式调用风格,实际上暴露了其他单元的内部细节??

我认为是要区别情况来对待,链式调用风格比较整洁和有表现力,但是不能随意滥用,举个简单例子:

a.getB().getC().doSomething() 这种链式调用就违反了得墨忒耳定律,如果把a.getB().getC().doSomething() 改成 a.doSomething(),仍然违反了得墨忒耳定律。因为a里面会有b.getC().doSomething(),所以 b 类中还应该有一个doSomething()方法去调用 c 的 doSomething()a.doSomething()再来调用b.doSomethine()ab的具体实现不可知。

链式风格用在 a.method1().method2().method3();这种情况会比较合理。所以能不能用链式,需要看链的是一个类的内部还是不同类的连接。

技巧四:分离不同的模块

系统应将初始化过程和初始化之后的运行时逻辑分离开,但我们经常看到初始化的代码被混杂到运行时代码逻辑中。下面就是个典型的例子:

bfc2d8e88b65c2dbd322681854ccf738.png

你会自以为很优雅,因为延迟了初始化,在真正用到对象之前,无需操心这种对象的构造,而且也保证永远不会返回 null 值。

然而,就算我们不调用到getService()方法,MyServiceImpl 的依赖也需要导入,以保证顺利编译。如果MyServiceImpl 是个重型对象,单元测试也会是个问题。我们必须给这些延迟初始化的对象指派恰当的测试替身(TEST DOUBLE) 或仿制对象(MOCK OBJECT)。

我们应当将这个初始化过程从正常的运行时逻辑中分离出来,方法有很多:

1. 交给 init 模块

将全部构造过程移到 init 模块中,设计其他模块时,无需关心对象是否已经构造,默认所有对象都已正确构造。

2. 抽象工厂方法

系统其他模块与如何构建对象的细节是分离开的,它只拥有抽象工厂方法的接口,具体细节是由 init 这边的接口实现类实现的。但其他模块能完全控制实体何时创建,甚至能给构造器传递参数。

3. 依赖注入中的控制反转

对象不负责实例化对自身的依赖,而是把工作移交给容器,实现控制的反转。比如 Android Dagger2 和 JavaEE Spring 都是这方面的实践。

4. Builder 模式

可以简单地把构造和构造的细节分离。

我们拆分了初始化和正常运行时逻辑,还有什么可以继续拆分的呢?

正常运行时逻辑除了业务逻辑,往往还混合了持久化、事务、打印日志、埋点等模块,如果说 OOP 是把问题划分到单个模块的话,那么 AOP 就是把涉及到众多模块的某一类问题进行统一管理。比如按 OOP 思想,设计一个打印日志 LogUtils 类,但是这个类是横跨并嵌入众多模块里的,在各个模块里分散得很厉害,到处都能见到。而利用 AOP 思想,我们无需再去到处调用 LogUtils 了,声明哪些方法需要打印日志,AOP 会在编译时把打印语句插进方法切面。AOP 思想有很多实践:

1. 代理

代理适用于简单的情况,例如在单独的对象或类中包装方法调用。然而,JDK提供的动态代理仅能与接口协同工作。对于代理类,你得使用字节码操作库,比如CGLIB、ASM或Javassist 。

2. AOP 框架

把持久化工作用 AOP 交给容器,使用描述性配置文件或 API 或注解来声明你的意图,驱动依赖注入(DI)容器,DI容器再实体化主要对象,并按需将对象连接起来。

Android中的 AOP 思想、框架选型和具体应用场景可详见: 一文读懂 AOP | 你想要的最全面 AOP 方法探讨

概言之, 最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯 Java 对象实现。不同的领域之间用最不具有侵害性的「方面」或「类方面」工具整合起来。

技巧五:用异常代替错误码,但不传递异常,不传递 null

if (deletePage(page) == SUCCESS),咋看之下好像没什么问题,但是返回错误码,就是在要求调用者立刻处理错误。你马上就会看到这样的场景:

83138cb5da0b0ae450a998660e1c5f1e.png

熟悉不?更恶心的是这种情况:

b84cf19461fe4faec33015b91ff72a20.png

当你开始编写错误码时,请注意!这意味着你可能在代码中到处存在 if(code == CODE),其他许多类都得导入和使用这个错误类。当错误类修改时,所有这些其他的类都需要重新编译。而且,错误码和状态码一样,会引入大量的 if-else 和 switch,随着状态扩展,if 就像面条一样拉长。回忆一下,你是不是用了不同的 code 来区分不同的错误?不同的用户状态?不同的表现场景?

所以忠告有 2 点:

  1. 使用异常替代返回错误码,将错误处理代码从主路径中分离

  2. 不仅仅分离错误处理代码,还要把 try-catch 代码块的主体部分抽离出来,另外形成函数,函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。

重构后:

a7181b6b4599865ae2d1948c994fedd6.png

在上例中,异常使我们把正常代码和错误代码隔离开来,但是我不建议你滥用异常,思考一下,如果你在低层的某个方法中抛出异常,而把 catch 放在高级层级,你就得在 catch 语句和抛出异常处之间的每个方法签名中声明该异常。每个调用这个函数的函数都要修改,捕获新异常,或在其签名中添加合适的throw子句。以此类推,最终得到的就是一个从最底端贯穿到最高端的修改链。封装完全被打破了,在抛出路径中的每个函数都要去了解下一层级的异常细节。

所以不要传递异常,在合适的地方,及时解决它

还有另一种情况你经常看到:

031ae9b66bb6a2cf0625e318b3f6c401.png

真是可怕!到处都是判空和特殊操作!如果你打算在方法中返回 null 值,不如抛出异常,或是返回空对象或特例对象。你可以学习Collections.emptyList( )的实现,创建一个类,把异常行为封装到特例对象中。

对付返回 null 的第三方 API 也是如此,我们可以用新方法包装这个 API,从而干掉判空。

技巧六:保持边界整洁,掌控第三方代码

我们经常会使用第三方开源库,怎么将外来代码干净利落地整合进自己的代码中。是每个工程师需要掌握的技巧,我们希望每次替换库变得简单容易,所以首先要缩小库的引用范围!怎么缩小?

1. 封装:不直接调用第三方api,而是包装多一层,从而控制第三方代码的边界,业务代码只知道包装层,不关心工具类的具体实现细节。在你测试自己的代码时,打包也有助于模拟第三方调用。打包的好处还在于你不必绑死在某个特定厂商的API 设计上。你可以定义自己感觉舒服的API。

2. 使用 ADAPTER 模式

代码整洁之道还提出个有意思的做法,为第三方代码编写学习性测试。

我们可以编写测试来遍览和理解第三方代码。在编写学习性测试中,我们通过核对试验来检测自己对 API 的理解程度。测试帮助我们聚焦于我们想从 API 得到的东西。

当第三方开源库发布了新版本,我们可以运行学习性测试,马上看到:程序包的行为有没有改变?是否与我们的需要兼容?是否影响了旧功能?

技巧七:保持良好的垂直格式和水平格式

垂直格式上

1、最顶部展示高层次的概念和算法,细节往下渐次展开,越是细节和底层,就应该放在源文件的越底部。

2、紧密相关或相似的代码应该互相靠近,调用者应该尽可能放在被调用者的上面,实体变量要靠近调用处,相关性弱的代码用空行隔开。

水平格式上

1、代码不宜太宽,避免左右拖动滚动条的差劲体验。

2、用空格字符把相关性较弱的事物分隔开。

3、遵守缩进规则。

技巧八:为代码添加必要的注释,维护注释

请注意,我说的是必要的注释,只有当代码无法自解释时,才需要注释。

好的代码可以实现自文档,自注释,只有差的代码才需要到处都注释。

如果你开始写注释了,就要思考下:是否代码有模糊不清的地方?命名是否有表达力?是否准确合理?函数是否职责过重,做了太多事情,所以你必须为这个函数写长长的注释?如果是这样,你应该重构代码,而不是写自认为对维护有帮助的注释。很多情况下只需要改下命名、拆分函数,就可以免去注释。

不要以为写完注释就完了,注释和代码一样,需要维护。

如何规避重构的风险?

在写代码之前,强烈建议你先完成单元测试,然后一边实现功能一边调整单测覆盖场景。

实现功能时,代码一开始都冗长而复杂,完成功能后,通过单元测试和验收测试,我们可以放心地重构代码,每改动一小块,就及时运行测试,看功能是否被破坏,不断分解函数、选用更好的名称、消除重复、切分关注面,模块化系统性关注面,缩小函数和类的尺寸,同时保持测试通过。

如何保持代码的优雅?

只要遵循以下规则,代码就能变得优雅:

1、编写更多的测试,用测试驱动设计和架构。

测试编写得越多,就越能持续走向编写较易测试的代码,持续走向简单的设计,系统就会越贴近 OOP 低耦合高内聚的目标。没有了测试,你就会失去保证生产代码可扩展的一切要素。正是单元测试让你的代码可扩展、可维护、可复用。原因很简单:有了测试,你就不担心对代码的修改!没有测试,每次修改都可能带来缺陷。无论架构多有扩展性,无论设计划分得有多好,没有了测试,你就很难做改动,因为你担忧改动会引入不可预知的缺陷。

2、保持重构,当加入新功能的时候,要思考是否合理,是否需要重构这个打开修改的模块。

3、不要重复,重复代码代表遗漏了抽象,重复代码可能成为函数或干脆抽成另一个类。

4、保持意图清晰,选用好的命名,短的函数和类,良好的单元测试提高代码的表达力。

5、尽可能减少类和方法的数量,避免一味死板地遵循以上 4 条原则,从而导致类和方法的膨胀。

开始重构,逐步改进

衡量成长比较简便的方法,就是看三个月前,一年前,自己写的代码是不是傻逼,越觉得傻逼就成长越快;或者反过来,看三个月前,一年前的自己,是不是能胜任当下的工作,如果完全没问题那就是没有成长。

既然聊到代码规范和重构技巧,Talk is cheap. Show me the code. 就以自己两年前的代码为例,但当我拿起两年前的项目时……

68102912654f3078388abbb61a5a06db.png

简单粗暴放上 gif,重构过程更直观。

099e516a39f97aefdbf218107dbe8a9f.gif

80d0ca09f03efa3ab48d6ca33df16a31.gif

5d981f52bb5b9389c19d84db0a6cee8a.gif

6e23f7f73e06ac9e4728afcba80c6fed.gif

最后

技巧是可以学习掌握的,重点是有意识培养自己的代码感,培养解耦的思想。不要生搬硬套技巧,不要过度设计,选择当下最适合最简单的方案。

同时我们需要不断回顾自己写过的代码,如果觉得无需改动,要么是设计足够优秀,要么就是没有输入,没有成长。

如果你对自己有更高的要求,希望以下的资料可以帮助你。

帮助你管理代码质量的工具

  • SonarLint

  • 阿里编码规范插件

更多方法论书籍

  • 「重构-改善既有代码的设计」

  • 「代码整洁之道」

  • 「设计模式-可复用面向对象软件的基础」

  • 「驯服烂代码」

  • 「修改代码的艺术」

  • 「编写可读代码的艺术」

有意思的网站

  • https://refactoring.guru/

阅读更多

What外包,技术外包如何防坑?

除了敲代码,你还有什么副业吗?

Github几个非常值得学习的项目

Android 架构组件 - 让天下没有难做的 App

手机商对“鸿蒙”进行了密集测试:比安卓系统快?

相信自己,没有做不到的,只有想不到的

在这里获得的不仅仅是技术!

9c8325989e9c57d430f8300501da4cc2.png

bb582da2ec22792e1ac8da94fccbdbbb.gif

喜欢就给个“在看”  118da0cda537012e40cd266145bad7e0.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值