[Flutter] - Clean Architecture

本文通过Flutter进行Clean Code的学习,也就是软件构建里面被前人总结出来的经验。

什么是软件构建

  • 定义问题
  • 需求分析
  • 规划构建
  • 软件架构
  • 详细设计
  • 编码与调试
  • 单位测速
  • 集成测试
  • 集成
  • 系统测试
  • 保障维护

前期准备

越好的前期工作,带来的损失和成本将会越少

但是,一旦我们找到了一份需求,就再也不做更改是几乎不可能的,通常在编写代码前,我们自己或者说我们的客户也不知道或如何描述想要的是什么,我们需要花更多的时间在理解项目上,参与项目的时间越长,对项目的理解也就越深入,开发过程能够帮助我们更好理解自己的需求,同时这也是需求改变的主要来源,如果严格按照需求行事,实际上就是计划不对客户的要求做出回应

但是即使我们需要改动,我们也最好大概知道我们需要改动多少,IBM和其他研究公司发现,平均项目在开发过程中,需求会有25%的变化(Boehm 1981, Jones 1994, Jones 2000),在典型的项目中,需求的变更导致的返工占到返工总量的75%到85%,大部分人在学习代码的时写的项目,其真实的代码量或许是完成时的2倍或许更多。

建立一套变更控制程序:如果我们的更改方案过于平凡,让我们无法跟上进度,我们就最好需要一套固定的变更控制程序,这样,我们可以只需再特定时候处理变更,而客户方也知道我们会处理这些变更

软件构架

好的构架使编码变得更容易,而糟糕的构架则使构建几乎寸步难行。如果在开发的时候进行架构的变更,代价也是高昂的,特别是类似Flutter这类正在发展中的言语,主要的第三方库的变更或许会导致这个项目的大部分主要逻辑代码进行更改如(Bloc),架构变更如同需求变更一样,看起来一个很小的改动,影响也许使非常深远的,无论使为了改进它,还是修复它,所以我们最好避免这部分的变动。

我们在制作一个软件时,往往把功能进行分割,就像拼图一样,但是就像拼图一样,如果我们不把它拼起来,那么很有可能无法理解我们正在开发的这个功能。如Flutter里我们完成了一个Bloc的功能的编写,但是如何将他放入系统里却没有好的方法。而如果在最开始就考虑放入系统的话,又会导致一些局限性问题的产生。好比Use Cases,我们或许不确定这个Use Cases是否真的会被使用,或如何被使用,所以在定义Use Cases上,我们尽量定义没有局限性的情况。

构造块

根据程序的规模不同,各个构造块可能时单个类,也可能时多个类组成的子系统,他们共同实现一种高层功能,比如:构建界面,验证输入,访问数据。每个需求都至少有一个构造块覆盖它,如果有多个构造块,那么它们需要相互配合而不是冲突。

  • 明确定义各个构造块的责任
  • 每个构造块应该负责某一个区域
    • 对其他构造块负责的区域知道的越少越好(松散耦合)
    • 讲设计的信息局限与构造块之内
  • 明确定义每个构造块的通信规则,架构应该能够描述
    • 直接使用哪些构造块
    • 间接使用哪些构造块
    • 不能使用哪些构造块

主要的类

架构需要详细定义所使用的主要的类

  • 指出每个主要的类的职责
  • 该类如何与其他系统交互
  • 包含对类的继承体系,状态转换,对象持久化等描述
  • 如果系统够大,应包含如何讲这个类组成子系统的描述

架构无须说明每一个类,利用80/20法则:对那些构成系统80%的行为的20%类进行详细说明。

如果对某个类在系统中的角色没有一个清晰的构思,那么编写这个类就是一件令人灰心丧气的事

数据设计

数据通常只应该由一个子系统或一个类直接访问,且以受控且抽象的方式来访问数据(隐藏秘密)。

用户界面设计

架构应该详细定义

  • Web页面格式
  • GUI
  • 命令行接口

等主要元素。且应该模块化,以便在替换新的界面的时候不影响业务规则和程序的输出部分。但是相信编写过Bloc的Flutter程序员会发现,因为Bloc的逻辑部分代码是直接作用于界面上的,所以如果要不影响业务规则,我们只需要不更改State即可,但是很多时候,我们需要添加新的State,这时候需要观察新的State和旧的State的转换关系,或者我们需要增加是一些新的UI样式,那就要根据现有的State进行新的逻辑编写。

错误处理

错误处理被证实微现代计算机科学中最棘手的问题之一,在编写Flutter的app的时候,我们可能会遇到一下问题

  • 数据获取失败
    • 服务端错误
    • 网络错误
    • 本地数据库错误
  • 用户验证失败、会话过期
  • 没有APP权限

因为通过bloc封装了Use Cases和处理的逻辑,这部分的逻辑错误其实不多,而正在需要处理是错误一般是上面我们无法控制的错误,错误处理牵连整个系统,最好在架构层次上就对待它。且我们需要考虑

  • 只是检测错误还是要纠正
  • 错误检测是主动还是被动
  • 如何传播错误
  • 如何处理异常
  • 在什么层次上处理错误、异常

我们可以将异常看作Exception,把错误看作Failure

过渡工程

初期编写程序的时候,我们往往不知道一个地方到底需要多少容错性,所以可能造成一个地方的维护代码过多。在软件中,链条的强度不是取决于最薄弱的一环,而是所有薄弱环节的乘积

一些常见注意事项

编程约定

实现的阶段必须与架构保持一致,并且这种一致性是内在的,固有的,这正是变量名称、类的名称,子程序名称,格式约定,注释约定等这些针对构建活动的指导方针的关键所在。

假如没有一种同一的规则,我们所创作出来的东西将会充斥着各种不同风格,导致程序显得混乱而糟蹋,这些不同的风格会成为我们承重的负担。

我们的画风需要一致

不局限于语言

编程工具不应该决定我们的编程思路,“在一种语言上编程”的程序员将他们的思想局限于“语言直接支持的那些构件”。如果语言工具是初级的,那么程序员的思想也是初级的。

险恶的问题

险恶的问题:只有通过解决或部分解决才能被明确的问题,意思就是我们必须先把这个问题解决一遍以便能够明确定义它,然后再次解决这个问题

我们在学校中所开发的程序和在职业生涯中所开发的程序的主要差异就在于,学校里的程序所解决的设计问题很少,如果有的话也是险恶的,学校的编程作业都是能让我们从头到尾直线进行而设计的,如果你刚刚完成了一份课题,而这个教授对课题要求进行了变动,那么你一定会把那个教授骂上一整天,但是这个过程在专业编程中每日可见。

太大的程序

没有谁的大脑能够容得下一个现代的计算机程序,作为程序员,我们不应该试着在同一时间把整个程序都塞进自己的大脑,而应该试着以某种方式去组织程序,以便能够在一个时刻可以专注于一个特定的部分,在软件架构的层次上,可以通过把整个系统分解为多个子系统来降低问题的复杂度,人类更易于理解许多项简单的信息,而不是一个复杂的信息,这在Flutter里面如何设计Bloc里很有帮助,对于某一个接口或者某一个功能,我们最好单独设计一个Bloc而不是把他们合在一起

过多的层次

我们通常通过私有方法等进行信息隐藏,不考虑其他的问题,信息隐藏的最后一个障碍是试图在系统架构层和编码层均避免性能上的损耗,,我们不必再任何一层去担心,因为再架构层按照信息隐藏的目标去设计系统并不会于按照性能目标去设计相冲突。

更常见的担心来自编码层,由于有了额外的层次的对象实例化和子程序的调用等,间接访问对象会带来性能上的损耗,实际上,这种担心为时过早,因为我们能够衡量系统的性能, 并且再出妨碍新能的瓶颈所在之前,再编码层能微性能目标所作的最好准备,便是做出高度模块化的设计。等我们真正遇到了性能的瓶颈,我们就可以针对个别的类或者子程序进行优化而不会影响系统的剩余部分。

如何设计

  • 最小复杂度
  • 易于维护
  • 松散耦合
  • 可扩展性
  • 可重用性
  • 高扇入
  • 低扇出
  • 可移植性
  • 精简性
  • 层次性
  • 标准技术

参考文献

  • 代码大全 - 第二版丨Steve McConnell丨7-121-02298-2