cocoapods-downloader设计概要

604 阅读8分钟

1. 前言

cocoapods-downloader是从cocoapods中分离出的一个很小的库,用于文件下载,虽然整体结构简单,代码量很少,在看完所有代码后,我觉得还是有一些东西值得分享。本文旨在描述这个库在设计上的一些风格体现,还有一些特定于ruby这门语言的有意思的实现。

  • 本文参考源码版本为 1.5.1。
  • 阅读参考

本文篇幅较短,除了2.3部分可能需要对cocoapods-downloader这个库的源码稍微看过,其他部分对于是否熟悉ruby或这个库都没有任何要求,可放心食用。

2. Downloader的设计

2.1 支持功能

从项目主页的readme中可以看出其具体的功能,其支持多种协议的文件下载,主要还是围绕版本管理系统的协议,如:git/svn/hg/http等,另外还支持自定义命令执行方式和输出的钩子。本文也主要围绕这两点进行展开,实际上这已经是这个库的全部内容了。

2.2 类与接口的设计

  • 多种协议的扩展

前面也说了,这个库支持多种下载协议,那么其是如何组织代码的呢?打开源码后,可以看到符合如下类图的相关类:

为了相对简洁,图中忽略了最外层的命名空间Pod,后面的图例也保持一致。

Untitled (7).png

从上图可以看出,其通过继承的方式,将Downloader设计为一个抽象Downloader::Base类和多个具体下载类,在以前的文章中(聊聊cocoapods-target的设计)也多少提到过关于继承的使用,在这种场景下,是比较符合继承的:

  • Downloader::Base作为一个抽象的基类,包含基本的相关属性和方法:如target_path/url/download,并定义了基本的工作流程。
  • 多个具体下载类继承自Downloader::Base,通过覆写Downloader::Base的抽象方法来实现自己的实际下载细节,本质上这是一种细化,而不是扩展。

具体的下载类,也可以拥有专属自己的参数(如Downloader::Git类拥有tag和commit),这里会有一些可能有不同实现的考虑,其具体可行性后面再说:

  • 专属于子类参数的成员变量;
  • 还是用一个容器成员变量来保存所有额外参数?

如果选择用容器保存参数,其可以放在Downloader::Base类中,这样所有的子类接口保持一致,听起来是不错;而如果有单独的成员变量,从可读性来说,也不是个坏方案。这一小节仅仅是在聊协议的扩展性设计,所以涉及到的代码构件还不全,这些子类的确是可以直接拿来用的,以上两种方案都可以选择,但是在实际设计中,我们可能希望接口更简单易用且稳定,所以我们继续往下看。

ruby并没有所谓的抽象类一说,甚至很多语言都没有,但是相信你完全可以模拟。

  • 接口的透明性

现在我们看一下当前Client是如何和下载类交互的(这里使用Downloader::GitDownloader::Http举例):

Untitled (8).png

可以看到,Client要使用某个下载协议时,需要直接与具体的Downloader子类进行交互,这样会产生直接依赖的耦合,耦合有必要的也有不健康的,在一个可预见的场景下,这种直接的耦合会产生比如以下问题:

  • Client需要明确知道自己需要哪种协议和对应的子类,如果协议是动态确定的,使用方需要花费更多精力去写适配代码。

在知道上述问题后,我们可以断定其属于一种不健康的耦合,我们应该对其进行修改。面向对象编程中有一条重要原则:不应该针对具体类型编程,而应该通过接口进行抽象。我们现在其实就是完全面向具体的子类,那么在这个场景下,还可以继续抽象么?

如何抽象,我们可以思考一下:现在最大的问题是Client需要知道的细节太多了,是否可以让其不需要考虑具体使用哪个子类就可以进行下载呢?

  1. 想要完成这样的需求,首先肯定是需要接口进行统一,这样就可以对外只暴露抽象类型,不需要暴露实具体的子类

前面我们讨论不同实现点时引出了另一种实现方式:向父类添加一个容器参数来记录具体子类的特有参数,这种方式在当前场景的可行性如何?

不同类型的下载协议拥有部分通用参数,而特定的参数全都放到一个扩展参数容器中,我认为这种逻辑并没有影响当前的设计,也是符合直觉的。cocoapods-downloader也是这么做的,在它的设计中,Downloader::Base类拥有一个options成员变量,其是一个Hash类型,由构造函数传入,而每个子类具体支持哪些可选的参数,通过重写一个类方法options(和options成员变量不要搞混)来实现,如Git支持[:commit, :tag, :branch, :submodules],这样子类的外部接口就可以和父类完全保持一致。

  1. 抽象类型是无法直接实例化的,我们需要将具体的子类构造过程进行隔离

面对这个问题,业内有一十分成熟的设计模式可以拿来使用,相信大家也知道就是工厂模式,工厂模式也有多种变体,这种场景下,cocoapods-downloader采取简单工厂的设计对子类构造过程进行封装,最终的类图变成了这样:

Untitled (9).png

可以看到Client只依赖一个抽象类和Factory类,而Factory类会依赖具体的Downloader子类,实际上在cocoapods-downloader的实现中并没有一个具体的Factory类,而是交给了Downloader这个module来处理,绰绰有余。

2.3 钩子的实现

cocoapods-downloader对于钩子的实现主要是通过ruby灵活的mixin机制来实现的,其通过一个override_api方法,传入一个代码块:

require 'cocoapods-downloader'
module Pod
  module Downloader
    class Base
      override_api do
        def self.execute_command(executable, command, raise_on_failure = false)
          puts "Will download"
          super
        end

        def self.ui_action(ui_message)
          puts ui_message.green
          yield
        end
      end
    end
  end
end

示例代码是对Downloader::Base进行的hook,当然你也可以对具体子类进行hook。

对于这块的具体实现,需要对照源码来理解,我在重点的位置也加了注释:

  • 第一部分
# cocoapods-downloader/lib/api_exposable.rb
module APIExposable
      def expose_api(mod = nil, &block)
        if mod.nil?
          if block.nil?
            raise "Either a module or a block that's used to create a module is required."
          else
# 通过Block构造一个module
            mod = Module.new(&block)
          end
        elsif mod && block
          raise 'Only a module *or* is required, not both.'
        end
# 插入到实例方法
        include mod
# 插入到类方法
        extend mod
      end
	    alias override_api expose_api
  end
  • 第二部分
#  cocoapods-downloader/lib/base.rb
module Pod
  module Downloader
    class Base
      extend APIExposable
      expose_api API
			.......
		end
	end
end

第一部分是钩子的主要实现部分,第二部分是原始的实现中利用这种钩子设计实现了默认的命令执行/UI输出等功能。

前面说了其依靠的是rubymixin机制,rubymixin机制有多种形式,在第一部分中包含了最重要的两种mixin方法:include modextend mod

  • include

    被导入的module会被插入到当前类在继承链内所处位置的上方。

  • extend

    如果是类对象调用,相当于为其元类调用include

说白了,就是依靠方法查找的顺序完成的钩子,就这么简单。

2.4 其他

  • Downloader.preprocess_options(options) ⇒ Hash<Symbol,String>

    这是在1.1.0版本中新增的一个方法(PR),其主要是为了解决cocoapods通过:git && :branch下载pod时,无法处理其缓存的有效性,原理是提前拉取远程branch对应的commit hash值来完成的,但是其实现也导致使用:git && :branch下载pod时无法进行浅克隆

3. 主观优化点

3.1 工厂类对具体子类的耦合

在仔细观察cocoapods-downloader的官方设计类图时,发现其中依然会产生Factory类对具体子类的耦合问题,这种耦合在一般的简单工厂中很常见,而且在这个场景下产生的影响并不大,因为具体子类的增删改查已经独立于Client了,唯一有影响的是会带动Factory的修改,那么有办法再进一步优化么?

我认为是可以的,目前的依赖关系来看,Client对具体子类其实是一种间接依赖的关系,在构建Client的代码时,实际上也需要具体子类文件的参与,当然这也是产生的问题之一,但是这个问题在这种同组件内类关系中的场景下并没有多少影响,真正会产生影响的一般发生在组件层级,但是其解决的方案倒是比较类似:我们可以让Factory不依赖具体子类,而是让具体子类去依赖Factory进行注册,关系会变成如下:

Untitled (10).png

修改之后的类图中,Factory与具体子类依赖关系被反转了,具体子类的增删改查也不会再导致Factory的修改,需要修改的地方少了,那么其整体的架构就变的更加稳定。

4. 结尾

感谢阅读,文中不免有很多主观想法和一些勘误,或者你认为不对的地方,如果能和我聊聊或者评论留下你的想法,非常欢迎!

参考资料