聊聊cocoapods-target的设计

1,049 阅读8分钟

1. 前言

由于近期又捡起了ruby,在构造一些cocoapods的小工具时也针对性的学习了一下源码,之前虽然看过但是是以大流程的理解为主。但是本文并不是去对源码中某一部分进行分析,而是从我们每天接触的podfile中的DSL出发,来聊聊其现有的设计从我个人主观上来看是否好用,所以本文实际上是聊的是podfile中的DSL设计。

2. cocoapods中target的定义

如果你对podfile的定义已经非常了解,请直接跳过2。

2.1 常规使用

一个比较简单的例子如下:

target 'MyApp' do
	pod 'AFNetworking'
end

从上面的代码看,我们为一个名为MyApp的target引入了一个AFNetworking的依赖,从cocoapods的podfile指南来看,我们在其内声明的target,应该对应于Xcode工程中的一个target,所以这段代码看是非常通俗易懂的。

2.2 继承

那么如果我们拥有多个target时,其中包含一些共用的依赖,在podfile中该如何声明呢?cocoapods官方给出的解决办法是继承,如下:

platform :ios, '9.0'
inhibit_all_warnings!

target 'MyApp' do
  pod 'AFNetworking'

  target 'MyApp2' do
     pod 'SDWebImage'
  end
end

可以看到MyAppMyApp2这两个target引入了同一个依赖AFNetworking,这是通过继承的方式做到的,另外对于继承还提供了抽象target继承模式这两个概念:

  • 抽象target

    普通的继承,要求子target拥有父target的所有依赖,那么如果两个target是共用一部分依赖,各自又有不同的依赖呢?此时可以使用抽象target,抽象target并不要求工程中有对应的target存在,使用如下:

    abstract_target 'Networking' do
      pod 'AlamoFire'
    
      target 'Networking App 1'
      target 'Networking App 2'
    end
    
  • 继承模式

    在继承时,可以声明具体的继承行为::none/:search_paths/:complete,默认的为:complete。

    当一个库被集成到项目中时,实际上可以分为两个部分:接口和实现,接口也就是各种接口文件的查找,而实现也就是具体的二进制文件的链接,这些其实也就对应着上面这些继承模式。

3. 如何看待

在讨论之前,先说一下一些基本问题,xcode工程包括workspace、project、target、configuration的结构,而对于库的依赖,cocoapods为什么要使用target关键字声明呢?因为在工程中,target才是一个产品单元。

3.1 继承的问题

在遇到多target存在公共依赖的场景,cocoapods的解决方案是继承,而这种继承是用一种嵌套的词法作用域来完成的,在我看来,首先依赖关系中使用继承本身就不太好,而它又搞出这种嵌套DSL来表达继承,简直是离谱。

  1. 先说为什么我不赞同继承

    在官方文档中明确的写着,一个podfile中的target应该对应于工程中的一个target,所以target与target之间,按照工程结构来说本身就应该属于同级关系,而不应该存在继承关系,为什么?原因有二:

    1. 继承的目的实际上是用于共用依赖,而对于依赖的声明,我们可以通过其他形式比如group进行组织,再通过组合的方式mixin到target中,待会我们再详细讨论这种设计。
    2. 引入一种在xcode中不存在的关系,会增加使用者的使用成本:如果在座的没看过文档,从一个已有的podfile声明中能看出来继承关系么?

    综上所述,我认为引入继承并不是明智之举,我们有其他的更好的方式来完成其所需要的功能。

  2. 再说嵌套的DSL来定义继承问题

    要找到这个设计的问题,需要引出一个例子:

    abstract_target 'Group1' do
      pod 'SomeLib1'
    	pod 'SomeLib2'
    	abstract_target 'Group2' do
    		pod 'SomeLib3'
    		pod 'SomeLib4'
    		target 'MyApp1'
    		target 'MyApp3' do
    			pod 'SomeLib5'
    		end
    	end
    	target 'MyApp2'
    end
    

    从上面的代码看,我们有3个target需要引入依赖,其中MyApp1依赖两组:Group1Group2,而MyApp2只依赖一组:Group1,而MyApp3除了依赖Group1Group2以外,还依赖一个单独的Pod。

    这两组依赖(Group1Group2)本应该属于同级关系,但是podfile的DSL嵌套设计使其只能单继承,为了完成这种依赖关系声明,导致Group2莫名其妙继承了Group1,嗯,说白了实际上是单继承的问题。我们日常接触的编程语言中,有很大一部分的继承设计都是单继承,而像依赖管理这种场景,一般情况都可以认为是组合性行为,而不是继承性行为,如果把继承和组合这两种复用代码的设计进行甄别,区别应该是这样的:继承是自上而下的,而组合是自下而上的,这里说的上下指的是设计初衷,继承是先产生父类事物,再进一步细化其属性到子类事物,而组合是发现多个事物存在共同行为,对这些行为进行一定的规则分组封装来复用,这种场景下行为的分组是不符合继承的设计的,因为不同行为的定义很容易被分离到不同的组,而继承只能拥有一个父类,这里你可能会说多继承,的确一些语言支持这种特性,但如果继承是这样诞生的,那么有多个父类的多继承在现代应该成为主流。

    这里引用了Ruby语言作者松本行弘在《松本行弘的程序世界》中的一段话,其中也提到最初引用继承设计的Simula编程语言也是单继承。

    当然你可能会说,我们可以另外定义方法来分组啊,的确,但是本文的目的是在于讨论cocoapods本身提供podfile DSL的设计问题,不能以不用来逃避其存在的我认为是问题的问题。

3.2 表达性问题

前面说的都是和依赖有关的问题,这里我们来说下target对照工程时的表达性问题,我认为一个好的依赖管理工具,它的依赖声明方式应该是简洁易懂,甚至一眼就能明白的:

  1. 按照podfile的DSL设计,target首先对应于xcode工程中的同名target,而去哪个project中找,在不同的target中可以分别设定,或者在顶层作用域中设定。这里我认为这种设定不太合理的原因主要还是和工程自身的结构产生了反转,增加了使用成本,我们知道在Xcode工程中,project管理了旗下的所有target,是project引用着target,而不是target引用project,这里我引用对照:

    # 原始定义
    target 'MyApp' do
    	project 'MyApp'
    end
    
    # 修改后
    project 'MyApp' do
    	target 'MyApp' do
    	end
    end
    

    这两种,对于一个苹果开发者,哪种更容易被理解呢?说到这里,我对podfile中的target关键字产生了疑惑:为什么一边认为自己对应于工程,另一边又和工程本身的设计南辕北辙。

  2. 而当依赖情况变得复杂时,比如上面3.1-2里的示例,臃肿的单继承DSL语义导致可读性非常糟糕,这种情况看一眼足够,解释都显得多余。

  3. 从整体来看,cocoapods其实是希望将工程和依赖的关系进行结合,让两者的界限变得模糊,但是从实际情况看,这种结合遇到一些复杂情况后暴露出来的缺点也非常突出,而其在简单场景下表现出来的表达性强的优点,并不是其独有的,我认为是有办法做到不管是简单场景还是复杂场景都能达到同样表达性强的。

3.3 我的想法

既然我吐槽了cocoapods的podfile的DSL这么多缺点,那么我也应该提出自己认为相对更好的方案,这里引入一个我稍微修改的示例:

# 依赖分组
group :group1 do
	pod 'APod'
	pod 'BPod'
end
group :group2 do
	pod 'CPod'
	pod 'DPod'
end

project 'MyProject' do
	# 从MyProject下查找MyApp target
	target 'MyApp' do
		# 完全依赖
		pod_group :group1
		# Debug配置下依赖
		pod_group :group1, :configuration => 'Debug'
		# 只依赖接口搜索
		pod_group :group1, :mode => :search_paths
		# 增加组以外的依赖,也支持以上参数
		pod 'XPod'
	end
	# 从MyProject下查找MyApp2 target
	target 'MyApp2' do
		pod_group :group2
	end
end

project 'OtherProject' do
# 从OtherProject下查找OtherApp target
	target 'OtherApp' do
		pod_group [:group1, :group2]
	end
end

对于我给出的示例,改动的想法如下:

  • 将依赖组管理和工程结构声明进行分割,这样引入依赖时能更加灵活。
  • 由于上面的修改,所以我们对于工程结构的声明可以按照工程自身的结构进行表达,加强可读性的同时降低了上手成本。
  • 对于配置相关的定义,我觉得cocoapods本身的设计是可以的,但是由于我们引入了依赖组概念,对配置需要有一个比较完整的定义:配置是target的属性,而不是依赖项(pod)的属性,所以当需要对某个或某组pod进行配置定义时,只能在实际引入语句中(或者说可部署target作用域内)定义,比如上面的pod_group :group1, :configuration => 'Debug'pod 'XPod',而group内的pod语句是无法定义配置的,因为其并不在一个可部署target作用域内,而在一个容器中。

暂时并没有实际去实现这个新的DSL,只是突然想到就想谈谈,如果实际改造可能还要重新设计个好几版才能最终确定。

4. 总结

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

参考资料