01-iOS架构设计|综述

1,760 阅读23分钟

前言

最近在和同行的朋友交流的过程中,我们围绕 “移动客户端架构设计"的问题展开了讨论,借此契机,通过本文作为综述进行概要介绍,再通过不同的小Topic的系列文章展开介绍,以记录一下自己在iOS客户端架构设计方面的理解。

一、综述

架构设计的意义:

  • 软件架构 是一张开发蓝图,是一个系统整体的规划,亦是应用软件工程方法开展系统建设工作的指导方针。
  • 在一个适宜当前阶段的系统建设需求的软件架构中,软件开发、运行和维护 可以做到 系统化规范化可度量
  • ... 进行软件架构,对于系统建设的益处不胜枚举,便不再赘述。我们马上进入正题,了解一下架构以及架构设计相关的一系列知识点。

1.软件架构的概念源于软件工程方法

1993年,电器电子工程师学会(IEEE)给出了 Software Engineering 的定义:
"将系统化的、规范化的、可度量的方法用于软件的开发、运行、和维护的过程,即将工程化应用于软件开发中."
软件工程是面向工程领域的,软件工程包含软件架构的设计。

软件工程的主要目标就是,使软件开发、运行和维护: 系统化规范化可度量

2.优秀的软件架构设计的几个指标

  • 1. 实用:
    首先,软件架构是为了让系统建设工作更加顺畅,避免增加不必要的工作。既不能过度设计,也不能疏于设计。
    • 优秀的软件架构不应该进行过度设计,如果设计复杂度较高,应考虑软件维护者能否接收这样的架构设计,以及可能带来的长期维护成本。
    • 过渡设计 会 加重 研发 团队的工作负担、加大研发成本
    • 疏于设计 同样会导致 到 后期 系统的迭代工作的繁重 、 也会导致团队开发进行系统交接的工作繁重且容易遗漏
    • 抛开业务谈架构是没有意义的,因为架构是为了业务服务的,空谈架构只是一种理想的状态。因此,结合实际的业务需求,进行实用于当前系统建设的需要的架构设计才是可靠的架构设计
    • 关于架构的技术选型:没有最好的方案,只有最适合的方案
  • 2. 健壮可靠:
    对于内外界多种不同类型的攻击,提供可靠、准确的输出。面对外界的变化,响应要迅速即时.同时还要做到灵活可扩展。
  • 3. 美观:
    代码要简洁、易读、易维护,能使维护者从直观上对功能产生更清晰的认识,从而能够快速地处理变化。
  • 4.组件抽象:
    优秀的软件架构 模块间没有复杂的依赖关系,易于扩展分离。

3.软件架构设计的关注点

软件架构设计关注变化

  • 软件架构设计更多是为可能出现的"变化"服务。
  • 在较恶劣的"变化"到来之时,维护者们为了快速交付任务,可能会随意堆积代码。
  • 在敏捷开发的氛围中,代码交付就像马拉松最后的冲刺,若是在很长的一段时间内都发生了代码肆意堆积的情形,会不断增加程序的复杂度,可能最后积重难返,最后难以维护迭代导致推翻重来!
  • 为了避免 这种 积重难返的 情况,要求在进行软件架构设计的时候,对项目的各种层级关系,通讯关系进行合适程度地划分
    • 功能服务划分
    • 通讯划分
    • 模块、组件划分
    • ......

4.架构设计思维

架构设计思维是每一位架构师都需要掌握的基本思维,其中包括简化思维分层思维分治思维迭代思维

4.1 简化思维

简化思维:
简化指 降低事物的复杂度 ,用更少的细节来代替较多的细节。
在软件架构中,抽象也是一种特殊的简化。
相对简化而言,抽象降低复杂度的力度更大。而不会破坏事物本身。

掌握简化和抽象思维,需要开发者对事物有宏观的理解,对组织形态有完整的认识,简化复杂部分,隐藏并不必暴露的细节,实现降低复杂度和提升代码可读性的目标。

具体方法:

  • 面向对象特性中的 封装、继承
  • 函数式编程
  • 面向协议编程
  • 泛型编程
  • ....

这里可以举一下反例:

  • 可封装成公用模块的UI、模型、工具类等,被多次重复创建、冗余
    • 不遵守 统一的代码规范
      • 缺乏统一的代码规范
      • 或者团队经过多次人员变更,快速推动业务迭代,而不遵守代码规范
      • 再或者两者兼备
    • 未提前设计好应用架构和可靠的管理模式
      • 过于追求快速业务迭代,而忽略了重要的前期设计工作
      • ...
  • 过分追求 抽象、封装 或 过渡使用一些依赖库,把简单的是实现复杂化
    • 比如在Swift工程,过多使用RxSwift以及RxCocoa等库,把一些事件处理,复杂化了
    • ...

4.2 分层思维

在架构设计中,分层架构一种比较流行的架构模式。几乎每一种架构都会用到分层思维,以达到关注点分离的目标。

具体方法:

  • 常见的设计模式中有MVCMVPMVVM等设计模式就是将不同工作职责的代码划分到不同的源码文件中去管理的
  • ...

4.3 分治思维

分治即分而治之。
可以理解为将一个复杂的问题划分为多个子问题,分别进行处理。最后达到解决这个问题的目标。
在算法中,分治法是一个非常流行的解决复杂问题的工具,他将一个问题划分为多个子问题,逐一处理,最后合并到一起,组成整个问题的解决方案。

4.4 迭代思维

没有任何一款优秀的架构可以一蹴而就,在架构创建完成后,持续收集用户反馈并进行迭代改进是必不可少的环节,这就是迭代思维。
推进迭代和重构、改进既有架构是一个架构师义不容辞的责任。

关于软件架构设计,肯定是围绕当前的业务需求进行敲定的。当需求变更/迭代开发时,关于模块的业务代码可能有所改变,因此要能随机应变的去选择适合当前需求的设计模式和架构方法。

关于架构技术方案: 没有最好的方案,只有最适合的方案

  • 用户规模、服务场景 某种程度上决定了业务复杂度
  • 业务复杂度,则决定了功能的实现
  • 功能的实现需求需要结合成本等从而确定技术方案
  • 技术方案确定后,就可以分析架构设计问题了

5.架构设计原则

    1. 开-闭原则
    • 【开】 指的是对扩展开放,即要支持方便地扩展;
      • 对外暴露的是可以有多个选项
      • 在Swift中用 public 修饰符 修饰 相关的属性 和 方法
    • 【闭】 指的是对修改关闭,即要严格限制对已有内容的修改
      • 对具体实进行封装隔离,防止外部破坏封装
      • 在Swift中用 private 修饰符 修饰 相关的属性 和 方法
    • 开-闭原则是最抽象也是最重要的OOD原则。
      • 做法:
      • 我们首先会全部用 privatefileprivateprotected等修饰符 修饰;
        • 针对不同的模块
        • 针对相同的模块
      • 然后会选出需要对外暴露的API和属性,或者编写一些API来公布(用 public 修饰符 修饰)
    1. 里氏替换原则
    • 子类必须能够替换其父类,否则不应当设计成其子类;
    • 换句话说,父类出现的地方,都应该能由其子类代替,所以,子类只能去扩展基类,而不是隐藏或者覆盖基类;
    • 公共部分抽象为父类的方法实现
      • 共有属性
      • 共有函数
      • ...
    • 公共部分独立到每个子类本身
    • 子类尽量不去重写父类的实现
      • 通过 组合 公共接口 + 非公接口 的形式,在子类实现新的功能
    1. 依赖倒置原则
    • 依赖倒置原则:“设计和实现要依赖于抽象而非具体”:
      • 【一方面】
        • 抽象化更符合人的思维习惯;
          • 抽象:
            • 用协议来定义类的接口(面向协议开发、非Delegate模式)
            • 具体的实现遵守协议
              • 可能有多种不同的实现方案,并有具体的不同类来实现;
                • 如语言国际化处理,针对不同地区的业务接口实现
                • 如同一套接口,针对不同平台的SDK内部的实现(社会化分享、支付、授权登录、安全认证等)
          • 案例:
            • 在使用Swift网络库Moya时,我们对不同的模块可能有不同的Target
              • 每个Target内部的接口实现是具体的
              • 针对Target的设计是一个协议TargetType,它是抽象的
            • 在用初始化NetworkProvider实例的时候,要把具体的Target类作为参数传递给NetworkProvider实例
              • 传递进去的Target类是具体的,
              • 构造的给NetworkProvider携带TargetType的类型参数是抽象的
      • 【另一方面】
        • 根据里氏替换原则,可以很容易将原来的抽象替换为扩展后的具体,这样可以很好的支持开-闭原则。
    1. 单一职责原则
    • 单一职责原则:不要存在多于一个导致类变更的原因,是高内聚低耦合的一个体现
    1. 接口隔离原则
    • “将大的接口打散成多个小的独立的接口
    • 由于Java类支持实现多个接口,可以很容易的让类具有多种接口的特征,同时每个类可以选择性地只实现目标接口。
    1. 最少知道原则
    • 一个对象就尽可能少的去了解其它对象”,从而实现松耦合;
    • 如果一个类的职责过多,由于多个职责耦合在了一起,任何一个职责的变更都可能引起其它职责的问题,严重影响了代码的可维护性和可重用性
    1. 合成/聚合复用原则
    • 合成/聚合复用原则
    • 如果新对象的某些功能在别的已经创建好的对象里面已经实现,那么应当尽量使用别的对象提供的功能,使之成为新对象的一部分,而不要再重新创建
    • 新对象可通过向这些对象的委派达到复用已有功能的效果
    • 简而言之,要尽量使用合成/聚合,而非使用继承

6.架构设计步骤与方法

在我的软件工程系列文章中,也有简单提及架构设计的:面向对象-架构设计

架构设计步骤 一般而言,我们可以将架构设计分为:认识系统、设计系统、创建系统和收集反馈四个阶段:

  • 1. 认识系统: 认识系统的数据模型、业务组成、模块组成等
    • 明确三个目标:
      • 1.) 设计者希望系统是什么样的?
      • 2.) 使用者希望系统是什么样的?
      • 3.) 成本预估目标如何?
    • 认清两个问题:
      • 1.) 现有系统存在什么问题?
      • 2.) 影响系统的外部条件是什么?
  • 2. 设计系统: 进行架构模式选型,选择适合的架构模式,对架构模式调研分析,确定系统架构设计方案;进行技术选型,选择合适的技术栈去实现编码需求。
    • 在选型阶段,需要注意一下三点:
      • 1.) 对于选定的技术方案和架构模式,团队成员的接受程度如何?学习成本如何?
      • 2.) 选定的模型存在什么优势和劣势?团队成员是否也应该明确这些优势和劣势?
      • 3.) 选定的技术模型流行度有多高?它为什么如此流行或者不流行?
  • 3. 创建系统: 选择系统的实现环境,对系统进行部署,进入开发/改造阶段。
    • 创建系统阶段 需要注意 将系统源码、改造日志等资料使用代码托管工具进行管理(Git、SVN)
    • 要时刻跟踪设计计划,避免在创建过程中忘记目标,迷失方向。从而导致产生系统创建偏离需求的情况
  • 4. 收集反馈: 系统创建完成,持续调研,收集反馈,对系统做下一步的规划
    • 1.) 可以展开代码评审会,进行代码复审
    • 2.) 使用类似留言板的匿名开放工具,使团队成员可以发表自己的意见和建议
    • 3.) 走访除开发团队外的合作成员呢,向他们咨询是否对团队成员最近的开发效率产生疑问
    • 4.) 在(内部)社区或论坛发表架构改进的相关文章及实际使用感想,收集网络上相同行业人员的反馈
    • 5.)也可以通过集成埋点收集系统间网络链路交互的通讯日志
      • 合理集成埋点有益于在庞大的系统中 排查 系统异常,有助于运维
      • 在客户端开发中,还可以进行页面埋点 和 业务 埋点
        • 若是有条件,公司还可以将埋点数据推送到大数据风控系统,进行分析:
        • 页面埋点: 可以做热力图分析,进而逐步得出用户对业务服务的偏好和习惯,从而以此为依据优化迭代我们的App
        • 业务埋点: 可以进行漏斗分析,进而了解在哪一个业务步骤、哪一个业务链路丢失了用户的访问。从而有针对性的迭代改善 该业务模块,提高用户转化率
        • ......

架构设计的方法 在前面篇幅的介绍,更多是软件工程版图里面,针对系统设计的一些理论思想的介绍。
然而,在实际开发中,针对iOS,也有具体的实践方法提供,他们包括但不限于:

  • 模块化开发
  • 组件化开发
  • 源码二进制化
  • 软件设计模式
  • ...... 我们将通过后面的篇幅,针对这几个具体的实践方法展开讨论

二、模块化开发

1. 区分组件模块的概念

组件(Component)模块(Module)是一对容易混淆的名词,也常常被用来相互替换。两者是否有差异往往取决专业背景、所在领域、以及视角。

1.1 在移动开发领域,我们有明晰的普遍认识:

模块化开发:

  • 【模块化开发】 指的是: 将一个系统或应用程序划分多个功能模块
  • 每个模块具有独立的功能接口,并且可以单独开发测试维护
  • 【模块化开发】目的是为了提高代码复用性可维护性可扩展性,以及降低开发测试的复杂度难度

组件化开发:

  • 【组件化开发】 则是在模块化开发的基础上,将模块进一步划分为更小的组件
  • 每个组件有自己的界面业务逻辑,并且可以独立部署和升级
  • 【组件化开发】 的目的是为了进一步提高代码复用性和可维护性,同时还可以实现更高的代码并行开发更灵活的应用程序构建发布

总的来说:

  • 模块化开发组件化开发 都是为了提高软件开发的效率和质量
  • 组件化开发相对于模块化开发 更加细粒度灵活,可以更好地满足大型应用程序的需求(换言之,一个模块可以集成若干组件,以实现模块的功能)
  • 个人总结,从设计上来看
    • 组件强调复用
    • 模块强调职责(内聚、分离)

1.2 了解一下 其它领域对模块和组件的介绍:

Module Module, 中文为模块或模组。

  • 它的核心意义是分离职责,属于代码级模块化的产出
  • 本身是一组具有一定内聚性代码的组合,职责明确
  • 对外的接口可以是松散的,也可以是集中的。SEI的定义如下: An implementation unit of software that provides a coherent set of responsibilities.
  • 它以问题分解的形式,来解决软件设计问题。它更强调一个内聚的概念,形式上可以是Java中的包,也可以是一个源代码目录。

Component Component,中文称为组件,或者构件

  • 使用非常比较广泛,它的核心意义在于复用 (可以复用的模块,概念上与模块基本等同,只是明显有依赖性的要求
  • 相对模块,对于依赖性有更高的要求。我大概整理了一下,两个定义 (参考Component based software programming)
  • Component-Based Software Development中提倡的组件定义如下(Component Software):
    • 符合特定的接口要求(交互的要求)
    • 具有明确的上下文依赖 (复用的要求)
  • 它可以独立发布(二进制或源代码的形式),也可以进行组合。这样软件开发就变成了组件的组装了
  • 和OOP中的Objects相比,一个类也可以视为一个组件,但更多的情况下,组件提供了更为高层的系统视角。Component如同一片树林,Object只是树

组件模块两者的关系取决于软件本身和视角。

  • Eclipse框架下的一个插件可能包含若干个模块,因为从Eclipse的角度来看,每个Plugin是用来复用的
  • 而一个应用的模块(如GUI)下也可能使用了多个组件,因为复用的是每个控件

另外也有人从发布形式上来区分,其实也不尽然

  • 一个模组可能以静态库、动态库存在(skia被应用到一个应用中的场景)
  • 一个组件也可能以源码的形式存在(Chromium中的Browser Component)
  • 发布形式其实是取决于产品需求的,不可能准确的区分出两个设计上的概念

参考:

2.iOS中的模块开发

从前文中,我们对模块的概念有了一定程度的了解,那我们如何在iOS开发中进行模块化开发呢?我们常常会遇到几个问题:

  • 独立 模块内聚解耦 设计
  • 不同 模块之间的通讯方式
  • 集成一个 模块的组件的划分
  • ......

六边形战士 BeeHive

阿里推出了一个名为BeeHive的开源库,是基于SpringBootMicroService微服务理念实现的。

  • BeeHive是一套比较可靠的模块化开发环境,在大型软件系统中有蚂蚁金融mPaaS开发套件等正在使用它。
  • 海内外项目中,若涉及银行金融业务的且采购了蚂蚁金融 mPaaS开发套件 的基本都采用了模块化开发从而实现了超级App的实践
    • 题外话: 我 有幸 和 阿里同事 参与了 mPaaS 开发套件的 首次了落地,就是国内 广发银行的信用卡服务平台:发现精彩
    • 后来陆陆续续有很多银行都接入了 基于 mPaaS开发平台 搭建的 金融服务
    • 经过多年的打磨,平台成熟稳定
    • iOS移动开发套件内部的模块化开发,可以说是与BeeHive有着千丝万缕的联系的
  • 通过 BeeHive 可以使模块间的具体实现与接口解耦更加便捷。
  • 该库 是一个 OC库,我们在下一篇文章中实践时,再进一步讨论OC和Swift项目中实现模块化开发的技术方案的具体选择

3.开发设计-建模功能工具

  1. 开发一个软件系统,首先需要确定需求
  2. 并基于需求对软件系统进行一定的分层设计
  3. 在层次划分的基础上进行模块划分
  4. 模块划分过后就是挑选组件用于集成实现模块
  • 模块划分之后,我们需要做的工作就是模块选择模块设计
    • 模块选择1: 公司的现存开发套件的模块,基于已有模块的基础上进行适当改造迭代
    • 模块选择2: 采购 厂商的SDK 服务,如前面提及的 mPaaS 蚂蚁金融云服务等
    • 模块选择3: 去获取开源第三方模块,并基于此项目需求,进行适当改造
    • 模块选择4: 重新设计开发
  • 模块设计之后,我们可以选择二方、三方、或公司现存的组件进行模块的实现,也可能从头开始设计研发一个组件。

无论是组件开发还是模块开发,我们都需要对其进行一定的设计工作,尽量避免不必要地重复工作和回避一些开发风险。

我们可以通过StarUML这个建模工具进行软件的设计工作,StarUML支持:

4. 具体的实践

在本文中,仅是围绕iOS架构设计的若干知识点,做一个相对简洁的概述。
对于iOS模块化开发的更详尽的介绍和相关的实践可以参考我的这篇文章:

三、组件化

1. 组件化基本介绍

在iOS开发中,我们可以将一个项目按照功能层级进行分层和一定的封装。

  • 通过 不同模块的独立封装不同类型的模块的分层管理 以及 多个模块的重组从而实现一定的功能集合的可复用的大模块,这种模块我们常常称之为组件
  • 在iOS平台中,对于不同组件代码的管理有多种
    • 手动管理
      • 手动拖组件到项目的工程
      • 手动添加系统依赖库
      • 手动配置环境变量等
    • Cocoapods管理
      • 通过Cocoapods把不同的组件划分到不同的工程
      • 通过组件之间的依赖来管理组件的库的集成
      • ......
    • 其它组件管理工具
      • 目前最流行广泛的就是Cocoapods,其它管理方式暂时不铺展开来介绍

2. 具体的实践

在本文中,仅是围绕iOS架构设计的若干知识点,做一个相对简洁的概述。
对于iOS组件化开发的更详尽的介绍和相关的实践可以参考我的这两篇文章:

四、二进制化

1. 二进制化基本介绍

在iOS开发中,可以将模块打包成静态库/动态库的形式,然后集成把库集成在一个项目里面。

  • 这样既可以很好的隔离管理 业务代码、开发库代码;
  • 也可以让开发业务功能的同事集中注意力关注业务实现。而开发框架的同事专注于迭代框架。这在庞大的软件系统体系里面,是一个很有效的管理方式
  • 另外,我们在开发一个项目的时候,有时候也会引用一些付费的软件包(如LBS模块、ShareKit模块、Push模块等)他们也常常以闭源二进制库的形式为我们服务

2. 具体的实践

在本文中,仅是围绕iOS架构设计的若干知识点,做一个相对简洁的概述。
对于iOS开发包二进制化的更详尽的介绍和相关的实践可以参考我的这篇文章:

五、设计模式

1. 设计模式的基本介绍

设计模式是属于一种编程经验的集合和总结,在经过许多人许多年的使用之后,基本稳定下来的一种编程模式。
通常通过具体的设计模式编程,具有以下优点:适用性强,实用性强,可复用,易修改 在iOS中,近几年比较流行的设计模式以以MVX为主,如 MVCMVPMVVM

2. 具体的实践

在本文中,仅是围绕iOS架构设计的若干知识点,做一个相对简洁的概述。
对于近几年流行的iOS设计模式的更详尽的介绍和相关的实践可以参考我的这篇文章:

六、架构设计与架构演进

1. 架构设计

(待完善)

2. 架构演进

(待完善)

架构演进案例

在市面上比较流行的大厂项目的开发团队都对其产品的架构演进做了一定的分享,通过仔细比较,我认为值得关注学习的几篇有(包括但不限于):

总结

在本文中,仅是围绕iOS架构设计的若干知识点,做一个相对简洁的概述,仅是一个引子,并没有针对子问题铺展开来介绍。若您关注具体的某个子问题,可以点击每个子问题分述下的具体链接进行了解。也非常欢迎广大网友们发表意见,我们相互探讨共同进步!

专题系列文章

iOS架构设计

探索iOS底层原理