『码』出高质量

本文从易理解、可维护、可扩展三个维度简要介绍了对高质量代码的理解。git

同时,提出了一种新的 GUI 模式:MVVS。github

本文同时发表于个人我的博客编程

Overview


我的认为,高质量的代码首先应该是『简单的』。redux

『简单』又能够从如下三个维度去度量:设计模式

  • 易理解
  • 可维护
  • 可扩展

本文将从上述三点展开讨论,谈谈我我的的理解。数组

易理解


高质量的代码必定是易于理解的代码,读起来应该像言情小说,而不是诘诎的文言文。markdown

高质量的代码能够是优雅的,但必定不是炫技的。数据结构

高质量的代码应该是深刻浅出的,尽可能用简单、朴实的方式去表达。多线程

易理解面向的是整个团队,而不是写代码的那我的。架构

易理解是团队高效、无缝协做的基础。

影响代码可理解性的因素有不少,好比:

『良好命名』

  • 好的命名便是成功的一半,也是最具挑战性的事情之一;
  • 总的原则就是体现『作什么』,而不是『怎么作』;
  • 在阅读优秀开源代码以及系统 API 时多留意、多思考。

『清晰结构』

( 这里指的是代码的组织结构 )

  • 相关代码组织在一块儿,如:initdeinit/dealloc、UIViewController 生命周期方法viewDidLoadviewWillAppearviewDidAppear等;
  • 不一样组织间用空行隔开;
  • 或者经过 Category、Extension 的方式来组织。

『合理抽象』

  • 抽象的关键是隐藏细节(『细节是魔鬼』);
  • 在咱们不关心具体细节时能很容易地忽略细节、抓住重点、抓住主干;
  • 抽象存在于每一个层级:变量、方法、类、模块。

『线性逻辑』

  • 每一个实体 (变量、方法、类、模块) 在不一样的层级上都只围绕一件事展开 --『高内聚』;
  • 模块内部的类、类内部的方法、方法内部的语句尽可能都能经过线性的方式串连起来;
  • 而若是它们间造成的是一张网或离散的点都是坏代码的味道 --『低内聚』;
  • 如,类内部存在大量不相关的方法,方法内部存在过多的ifswitch语句。一个类动辄几千行、一个方法动辄几百行,此时就值得咱们高度警戒;

很明显,线性的逻辑比网状的逻辑更易于理解。

『最小依赖』

( 依赖能够从两个角度来度量:依赖的多少以及强弱 )

  • 依赖确定是越少越好 --『低耦合』;
  • 过多的依赖无疑会增长代码的复杂性,每每也是代码设计出问题的一个信号;
  • 提升内聚、面向接口编程等都是下降依赖的有效方式;
  • 同时,依赖关系越弱越好,继承无疑是最早考虑到的代码复用方式,可是继承也是最强的一种依赖、耦合关系。
  • 代码复用,应优先考虑组合。

『精简体积』

简单讲,就是无用代码及时删除;

在阅读代码时常常会遇到一些莫名其妙的逻辑,通过一番调查,发现已经是废弃的代码;

无形中增长了理解成本,而且随着时间推移,后浪们也不敢去删这些代码,最终越积越多。

『朴实表述』

正如名言:代码首先是写给人看的,其次才是让机器执行的;

所以,尽可能用平易、朴实的方式去描述、去表达,让你们都能『看得懂』;

总之,好的代码必定是简单的、清晰的。

易理解是高质量代码最基础的要求,上述只是影响代码易理解性的几个小点,更多的须要咱们在实际编码过程当中不断思考总结。

『 浅谈高质量移动开发 』一文中对类的设计、方法的设计等有更具体的讨论,感兴趣的同窗能够看看。

可维护性


移动端开发最主要的场景就是基于 GUI 的业务开发。

所以,本文谈到的可维护性也主要是围绕 GUI 流程展开。

关于 GUI 的设计模式 (MV*) 在近几年也是老生常谈的话题之一。

不管 GUI 模式如何演化,我的认为其核心原则不曾改变:

  • 单向数据流
  • 数据完整性
  • 数据驱动 UI

『单向数据流』

不管哪一种 GUI 模式,从大的方向上均可以分为两层:

  • Domain Layer:数据层(业务逻辑)
  • Presentation Layer:表现层(UI)

单向数据流是指『业务数据』必定是从『数据层』流向『表现层』,毫不容许反过来。

『表现层』能够响应 UI 事件并传递给『数据层』, 至于『数据层』如何处理这些事件纯属其『内政』,『表现层』无权干涉。

简单归纳,『 单向数据流 』背后有两个『 流 』:

  • 数据流:从『 数据层 』流向『 表现层 』;
  • 事件流:从『 表现层 』流向『 数据层 』。

关于事件,不少响应式框架会将其定义为独立的数据结构,如:Redux中的ActionBLoC中的Event

我我的认为这种设计有一个比较严重的问题:不能经过『 command + 单击 』的方式『 链式 』地阅读代码,须要经过全局搜索。

这严重影响了开发效率。

我的以为事件能够直接是『 数据层 』暴露给『 表现层 』的接口,即有事件须要处理时,直接调用相应的接口便可。

为何?

我相信任何一位移动开发者对于『单向数据流』都耳熟能详,但其背后深层次的缘由不见得都能说清楚。

首先,数据流不能从『表现层』流向『数据层』,说的究竟是什么?

其真正的含义是『表现层』不能直接修改『数据层』的数据。为何?

从设计的角度讲,数据管理是『数据层』的职责,『表现层』不该越俎代庖。 不然,『表现层』就违反了『单一职责』原则,也违反了『高内聚』的设计理念。

从现实的角度讲,『表现层』直接修改数据对于代码维护性来讲是一个『灾难』。

首先,直接后果就是可能形成『不一样步』:

  • UI 刷新与数据修改不一样步,数据修改后 UI 没有及时刷新;
  • 多个 UI 场景间不一样步,有的数据可能被多个业务模块所使用,若是由其中某一处直接修改,其余模块极可能没法感知到这一修改,形成不一样步。

以下图,Scenes1 直接修改了底层数据,但并未通知 Scenes二、Scenes3 (Scenes1 可能根本不知道 Scenes二、Scenes3 的存在),形成数据不一样步:

『不一样步』常见的后果就是在使用 TableView 时数组越界。

对于此类问题,咱们常常是对数组访问加个保护,而不多也很难从根源上解决问题。

尤为是多业务场景共享数据时。由于你根本不知道是『 谁 』在『 何时 』修改了数据源。

其次,还可能引发多线程问题:

  • 『数据层』可能有专职的线程去管理数据,若是『表现层』擅自修改数据,极可能引起多线程问题。

如何解

『数据层』必定不能向『表现层』直接暴露可修改 (mutable) 的属性。

对于引用类型来讲,状况可能更复杂一些。理想状况下,『数据层』返回给『表现层』的数据应该是final的或是深拷贝的。

总之,要作到『表现层』毫无直接修改底层数据的可能性。

此时,你们可能有疑问了,即便是『表现层』经过事件触发『数据层』内部去修改数据,仍是可能会引发不一样步的问题。

对,这就须要经过『数据完整性』、『数据驱动 UI』来解决了。

『数据完整性』

数据完整性指的是数据不该该存在中间临时状态。 如上图左则所示,可能会致使意想不到的结果。

在须要修改时,应该对数据做总体替换,这也是咱们常说的『数据不可变性』。

在函数式编程中,全部数据都是不可变的,全部操做的结果都是生成新的数据,而不是在原有数据上做修改。

严格遵照『数据完整性』、『数据不可变性』原则,能很好地避免中间状态问题。

『数据驱动 UI』

底层数据发生变化后,上层 UI 如何感知并刷新?

总的原则就是『数据驱动 UI』

直白点,就是『数据层』有渠道、有方法在数据变化时能主动通知到全部关注该数据的『表现层』对象。

看似很简单?实则不少项目都没能作到这一点。

有没有闻到『响应式编程』的味道。 有同窗一听到『响应式编程』就以为很复杂,难于接受。

其实,我我的认为『响应式编程』并不是必定要使用诸如Rx*ReactiveCocoa或 iOS 原生的KVO这样的框架或技术。(它们只是一种实现手段而以) 其核心在于:

  • 『数据层』主动通知,『表现层』被动响应;
  • 数据的全部使用方都能及时感知到数据的变化。

所以,像 Delegate、Callback 甚至 Notification 均可以用来实现『响应式编程』。

『响应式编程』其实就是『观察者』设计模式的一种应用场景。

好了,咱们来回顾总结一下:

  • 『单向数据流』:保证数据的修改仅发生在『数据层』内部,这也是『数据完整性』、『数据驱动 UI』得以实现的前提;
  • 『数据完整性』:任何数据的修改都是总体替换,实现『数据不可变』的语义,避免出现数据的中间状态;
  • 『数据驱动 UI』:保证数据修改后能及时通知 UI,避免状态不一样步。

MVVS

基于『单向数据流』、『数据完整性』以及『数据驱动 UI』的原则,咱们提出一种新的 GUI 模式:MVVS

  • M:Manager,处理业务逻辑,管理业务数据,将数据转换为 ViewState 并通知上层 UI;
  • V:View,UIViewController/UIView,面向 ViewState 编程;
  • VS:ViewState,当前的 UI 状态,将『数据驱动 UI』进一步拆分:数据驱动状态、状态驱动 UI。

MVVS vs. MVVM,至关于将 MVVM 中的 VIewModel 拆分为 MVVS 中的 Manager 和 ViewState,使得各自的职责更加清晰。

MVVS 中的 ViewState 受 Flutter 响应式框架 flutter_bloc 中 state 启发。

Manager、View、ViewState 间的关系以下图所示:

ViewState

ViewState 表明当前的 UI 状态,为 UI 提供展现用的数据。

原则上,ViewState 与 View 一一对应。 对于过于简单的 UI 也能够没有与之对应的 ViewState。

如上图,与 ViewController 对应的是 Root ViewState,表明当前该模块的总体状态。

根据不一样的状态,ViewState 能够是不一样的子类型,如:LoadingViewState、ErrorViewState、EmptyViewState、LoadedViewState 等。ViewController 再根据不一样的状态做出不一样的响应。

其余

上面咱们主要讲述了 GUI 架构上会影响代码可维护性的几个关键点 影响代码可维护性的因素还有不少,如:

  • 最少状态: 在代码中常常会看到不少的状态变量,如:is***(isFirstLoadisNewUser)、has***(hasLoaded)、***Count(userCount)等等。

    这些状态自己的维护以及其对代码总体的维护都是很是容易出问题的。

    是否在全部须要修改的地方都正确的修改了?

    修改后使用到这些状态的地方都及时通知到了?

    所以,状态要越少越好,能不加就不加。

    如,***Count是否能够从数据集上动态计算获得

  • 最小权限: 不管是模块仍是类暴露的接口都应遵照最小权限原则

    在具体开发过程当中能够经过『 依赖倒置 』原则,让接口需求方提出接口需求,避免由实现方直接提供接口而无心中暴露过多细节。

    具体能够参看『 面向对象设计原则『SOLID』在开发中的应用 』

  • 写纯函数: 纯函数几乎没有外界依赖,其在可维护性、易理解上有自然优点。

    通常类方法都有纯函数特性,所以,能写类方法的时候就不要写实例方法。

    『 函数式思惟 』一文中对函数式编程有过简单讨论

可扩展性


『惟一不变的就是变化』

面对变化,咱们惟以积极心态对之。

所以,代码的可扩展性也是咱们须要重点思考的问题之一。

『 SOLID 』中的『 O — OCP,开放-封闭原则 』,就是用于指导可扩展性的原则之一。

OCP:『 对扩展开放,对修改封闭 』。

强调『可扩展性』就是要求咱们写『活代码』。 在平时 Code Review 时,我常常开玩笑地说『你这个代码写的太死』。

23 种设计模式中的『 策略模式 』、『 模板方法 』都是用于指导提高可扩展性的方法。

『 策略模式 』

提升代码可扩展性最有效的方式之一就是面向接口编程。

『 论面向接口编程 』一文中对此有详细介绍,在此再也不赘述。

在具体开发时如何写出扩展性高的代码呢?

『扩展性』面向的是将来,

所以,首先要思考的是『 什么是可能会变的 』

再将『 可变部分 』隔离出来,并以接口的形式去抽象它。

熟悉设计模式的同窗可能已经看出来了,这其实就是『策略模式,Strategy 』。

在以前的文章中,咱们也提到过,如:登陆模块、多 Tab 页面都是典型的可经过『 策略模式 』来提升扩展性的场景。

登陆模块的例子在『 面向对象设计原则『SOLID』在开发中的应用 』一文中有详细介绍

多 Tab 页面的例子在『 论面向接口编程 』一文中有详细介绍

『 模板方法 』

『 策略模式 』的基础是面向接口编程。

而『 模板方法,Template Method 』的基础是继承。

一样,在『 面向对象设计原则『SOLID』在开发中的应用 』一文中对『 模板方法 』模式有过简单的介绍。

『 iOS 高效开发解决方案 』一文中介绍过经过『 模板方法 』模式提升 UI 组件的扩展性。

提高代码可扩展性的方法毫不仅上述 2 种模式,但其背后的思想很是重要,在平时开发时可灵活应用。

其余


除了上述提到的『 易理解 』、『 可维护 』、『 可扩展 』以外,还有不少点是值得咱们去思考和关注的,如:

  • 错误处理: 如何优雅地处理错误其实很是重要,但每每被忽略。在设计接口时须要关注出错的状况;

  • 关键路径打 log: 错误是没法避免的,除了在出错时给用户一个较好地提示外,咱们也须要去了解出错的缘由,此时 log 就显得尤其重要,要养成在关键路径打 log 的习惯。不然,用户反馈问题后,两眼一抹黑,无从下手;

  • 适时重构: 重构不必定是要对代码作出『 翻天覆地 』的改变,小到对变量重命名、抽取一个方法等『 微小 』的优化都算是重构。总之,在当前代码结构已再也不适应新业务须要时,就须要及时重构,切不可在原有基础上打补丁,代码的恶化每每就是今后开始的。

    对待代码咱们一样要有敬畏之心:『 勿以善小而不为,勿以恶小而为之 』。

小结


写出高质量的代码可谓『 功在当时,利在将来 』

每一位开发者都应为开发出高质量的代码也努力

代码设计能力的提高非一日之功,须要咱们长期不断地学习、实践、思考、总结

优秀的书籍、优秀的开源代码咱们要学习

糟糕的代码咱们也要去反思,去总结

在代码上一样要作到『 勿以善小而不为,勿以恶小而为之 』

诸君共勉!

相关文章
相关标签/搜索