Fork me on GitHub

如何打造一个让人愉悦的框架

API设计

考虑提供给开发者的内容

你标记为 Public 的内容都是非常重要的,它将是框架的使用者能看到的内容,提供什么样的API一定程度上决定了其他开发者如何使用你的框架。

尽可能小的访问权限

在API设计的时候,从原则上讲我应该尽可能提供较少的接口来完成必要的任务,这有助于在框架的初期控制住框架的复杂度,而之后随着逐步开发和框架使用场景的扩展,我们可以添加一些公共的接口啊,或者将原来标记为 Private 的内容标记为 Public,供外界来使用。

1
2
3
4
5
6
7
8
9
// Do this
public func mustMethod() { ... }
func onlyUsedInFramework() { ... }
private func onlyUsedInFile() { ... }

// Don't do this
public func mustMethod() { ... }
public func onlyUsedInFramework() { ... }
public func onlyUsedInFile() { ... }

命名,是否清晰易懂完整

在 Cocoa 的世界里,精确比简短更有吸引力。

举个例子,可能相较于简短的 remove,可能 removeAt 更能表达出从一个集合中移除某个元素的方法,而 remove 可能导致误解,是移除特定的 Int 呢?还是从某个 Index 去移除呢?

1
2
3
4
5
// Do this
public mutating func removeAt(position: Index) -> Element

// Don't do this
public mutating func remove(i: Int) -> Element // <- index or element?

同样的 recursivelyFetch 表达了递归的获取全部内容,而 fetch 可能会被理解为仅获取当前的输入。

1
2
3
4
5
// Do this
public func recursivelyFetch(urls: [(String, Range<Version>)]) throws -> [T]

// Don't do this
public func fetch(urls: [(String, Range<Version>)]) throws -> [T] // <- how?

另外需要注意的是,方法名应该是动词或者动词开头的短语,而属性名应该是名词,当遇到冲突时,比方说这里的 displayName 既可以是名词也可以是动词短语,这个时候就应该特别注意属性和方法的上下文造成的理解不同,更好的一种方式是避免这种名动皆可的词语,比如说把 displayName 换成 screenName 可能是更好的选择。

1
2
3
4
5
public var displayName: String
public var screenName: String // <- Better

// Don't do this
public func displayName() -> String // <- none or verb?

注释文档

在命名 API 的时候一个有用的诀窍是为你的 API 编写文档,如果你不能用一句话表述清楚这个方法具体做了什么,这往往意味着你的这个 API 的名字是有改进的余地的。好的 API 呢就是可以上有经验的开发者猜的八九不离十。

理想状态:代码不需要文档就能被看懂

Swift API Design Guidelines

关于API的命名,苹果官方给出了Swift API 开发指南,Swift API Design Guidelines,遵守这个准则才能和其他开发者一道用约定俗成的一种方式来进行交流,这对提高框架的质量非常、非常、非常、重要!

优先测试,测试驱动开发

你应该是你开发的框架的第一个使用者,其实最简单的使用你的框架就是编写测试,在APP开发中,单元测试很多时候被我们忽略掉了,但是在框架开发里这是一个非常重要的细节,恐怕没有人敢使用没有经过测试的框架,除了保证功能正确以外,通过测试你还能第一时间发现框架里不合理的地方,并进行改善。

命名冲突

1
2
3
4
5
6
7
8
// F1.framework
extension UIImage {
public method() { print("F1") }
}
// F2.framework
extension UIImage {
public method() { print("F2") }
}

在OC中,另一个特别突出的问题就是,静态库里一个常见的同样的符号在链接的时候导致冲突,在 Swift 中我们可以通过 model 和 import 来提供一个像类似于命名空间这样的代码隔离,从而避免这种符号冲突,但是对系统已有的类,添加 Extension 的时候还是要特别注意,比如说我们在这里,框架F1和框架F2,都是UIImage 定义的一个 method 的方法,然后分别输出自己来自哪一个框架。

1
2
3
4
5
// app
import F1
import F2
UIImage().method()
// Ambiguous use of 'method()'

如果我们需要在同一个文件里同时引用F1和F2的话,编译器会很不高兴,因为它不知道具体要执行哪个方法。

1
2
3
4
// app
import F1
UIImage().method()
// 输出 F2 (结果不确定)

虽然命名空间已经被隔离了,但是这里 UIImage 是一个 NSObject,它的 Instance 实际上还是依赖于 OC 的 Runtime,这两个框架F1和F2都在 App 启动的时候被加载,运行时究竟调用了哪个,是以加载顺序为依据的,并不能确定,所以这种问题如果实际遇到的话就会非常难以调试,尽量避免。

对于 Cocoa 类型的 Extension 必须添加前缀

1
2
3
4
5
6
7
8
9
// Do this
// F1.framework
extension UIImage {
public f1_method() { print("F1") }
}
// F2.framework
extension UIImage {
public f2_method() { print("F2") }
}

发布框架

CocoaPods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pod spec create MyFramework 

Pod::Spec.new do |s|
s.name = "MyFramework"
s.version = "1.0.2"
s.summary = "My first framework"
s.description = <<-DESC
It's my first framework.
DESC
s.ios.deployment_target = "8.0"
s.source = { :git => "https://github.com/onevcat/myframework.git",
:tag => s.version }
s.source_files = "Class/*.{h,swift}"
s.public_header_files = ["MyFramework/MyFramework.h"]
end

提交到 CocoaPods

1
2
3
4
5
6
7
8
# 打 tag
git tag 1.0.2 && git push origin --tags

# podspec 文法检查
pod spec lint MyFramework.podspec

# 提交到 CocoaPods 中心仓库
pod trunk push MyFramework.podspec

Carthage

很简单,将TargetScheme设置为shared

版本管理

1
2
3
4
5
6
7
8
9
10
11
# Podfile
pod 'AFNetworking', '~> 2.6.1'
# 2.6.x 兼容 (2.6.1, 2.6.2, 2.6.9 等,不包含 2.7)

# Podfile
pod 'AFNetworking', '~> 2.6'
# 2.x 兼容 (2.6.1, 2.7, 2.8 等,不包含 3.0)

# Cartfile
github "Mantle/Mantle" >= 1.1
# 大于等于 1.1 (1.1,1.1.4, 1.3, 2.1 等)

版本兼容

1
x(major).y(minor).z(patch)
  • major - 公共 API 改动或者删减,用户必须要修改自己的代码才能够继续使用你的框架。
  • minor - 新添加了公共 API,但是现有用户不需要修改代码就可以继续使用。
  • patch - bug 修正等,API 没有变更。

0.x.y 只遵守最后一条,还未到达1.0.0的框架,表示还在早起开发,没有正式发布,API 在调整中,开发者想干什么就干什么。

使用Git tag 进行版本标记,框架的版本应该是和Git的tag相对应的,你的用户/包管理系统期望合理的兼容版本。

Version和Build

1
2
3
4
5
6
7
8
9
10
// MyFramework.h
//! Project version string for MyFramework.
FOUNDATION_EXPORT const unsigned char MyFrameworkVersionString[]; // 1.8.3
//! Project version number for MyFramework.
FOUNDATION_EXPORT double MyFrameworkVersionNumber; // 347

// Exported module map
//! Project version number for MyFramework.
public var MyFrameworkVersionNumber: Double
// 并没有导出 MyFrameworkVersionString

框架的用户如果希望在运行时知道使用框架的版本号的话,他们会使用上面的两个属性进行访问,如果提供了的话,那么在框架迁移的时候比较有用,作为开发者,应该顺便维护下这两个值来帮助用户确定所使用的框架版本。

持续集成

在框架开发中,一个优秀的集成环境也是至关重要的,CI 可以保证潜在的贡献者在有保证的情况下对代码进行修改,可以大幅减小我们维护框架的压力。

TRAVIS CI, CIRCLE CI, COVERALLS, CODECOV…

自动化的发布流程

FASTLANE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Fastfile
desc "Release new version"
lane :release do |options|
target_version = options[:version]
raise "The version is missed." if target_version.nil?
ensure_git_branch # 确认 master 分支
ensure_git_status_clean # 确认没有未提交的文件
scan # 运行测试

sync_build_number_to_git # 将 build 号设为 git commit 数
increment_version_number(version_number: target_version) # 设置版本号
version_bump_podspec(path: "Kingfisher.podspec",
version_number: target_version) # 更新 podspec
git_commit_all(message: "Bump version to #{target_version}") # 提交版本号修改
add_git_tag tag: target_version # 设置 tag
push_to_git_remote # 推送到 git 仓库
pod_push # 提交到 CocoaPods
end

$ fastlane release version:1.8.4

值得参考的例子:AFNETWORKING/FASTLANE

优秀框架的特征

  • 详尽的文档说明
  • 可以指导后来开发者和协作者快速上手的注释
  • 完善的测试保证
  • 代码质量
  • 让使用者了解更新变化的更新日志
  • issue的相应速度

COCOAPODS QUALITY

这是一个为开源框架打分的索引类的项目,它会按照项目的受欢迎程度和上面提到的标准来为开源框架进行一个初步的评判。

COCOAPODS QUALITY

QUALITY INDEXES GUIDE

可能的问题

兼容性保证

这里指的是逻辑上的兼容性,最可能出现的问题就是在不同版本中对数据持久化部分的处理是否兼容,包括数据库,属性增添,Key-archiving等等,比如说从老版本过来,添了一个属性,如何把旧版本的数据迁移过来,处理不当的话就可能导致 Crash。

重复依赖

  • EMBEDDED_CONTENT_CONTAINS_SWIFT设置为 NO

  • 不要将依赖的 framework copy 到 你自己的 Framework 中,而是要使用如 PodFile 的方式来管理依赖

不同的框架依赖可能无法兼容

错误依赖

在开发框架的时候,如果要依赖别的框架,最好使用最宽松的依赖条件。

正确依赖

-------------本文结束感谢您的阅读-------------

本文作者:乔羽 / FightingJoey

发布时间:2018年12月30日 - 20:19

最后更新:2018年12月30日 - 22:36

原始链接:https://fightingjoey.github.io/2018/12/30/私密/如何打造一个让人愉悦的框架/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!