在优步,我们的Android工程师已经使用了几年的注释支持的代码生成。在iOS上,我们首先在2016年秋季调整代码生成,我们开始在我们的新工作骑手应用程序使用肋骨结构.我们新的移动架构的经济架构之一 - 驱动我们的可靠性 - 推动我们来调查我们是否可以使用代码生成来增强我们的移动客户端的可靠性,并通过删除手动编写可以自动生成的代码来提高开发人员体验来自现有数据源。
但是,需要注意的是,代码生成并不是万能的解决方案。如果您正在使用代码生成来解决一个问题,而这个问题可以通过使您的代码更通用来更好地解决,那么代码生成可能会对代码库的健康和可维护性产生退化影响。然而,对于某些用例,代码生成是提高应用程序可靠性和提高开发人员生产力的有效方法。
在本文中,我们讨论了两个常见的代码生成用例,生成嵌入式资源访问器和测试模型 - 突出该技术如何用于使应用更可靠,工程师更加富有成效。
资源访问者
iOS没有相当于Android的R类,一个类是生成的代码,以表示应用程序的静态资源。在运行时通常会在运行时动态评估像图像或字符串等静态资源的所有访问:
让翻译= nslocalizedstring.( “email.unreadMessages” ,评论: “你有%d未读消息” )
这有两个主要问题。首先,由于返回的图像和本地化字符串都是可选的,因此代码变得更加复杂,必须安全地处理这种可选性,以避免图像从资源包中丢失时出现崩溃。
其次,没有编译时间步骤会捕获任何这些资源的意外删除,为必须编写单元测试的工程师添加大量开销,以确保实际存在预期资源。
资源访问器代码生成
通过创建检查项目并生成包含所有可用资源的静态结构的工具,我们解决了这两个问题。
对于图像,该工具将通过与每个项目目标相关联的资产目录,查找相关图像,并为所有图像生成带有非NULL访问器的静态结构。连续集成也会运行该工具,确保如果有人意外地从任何资产目录中删除了图像,则修订将无法构建,错误的变化不会降落。
对于本地化的字符串,将构造一个类似的结构。此外,该工具将识别需要输入变量的本地化字符串,并生成API,以确保仅使用正确的参数访问该字符串。
例如,字符串文件中的以下格式化字符串:
unreadMessages = "你有%d未读消息“
会生成一个API来确保工程师使用正确的整数类型来格式化字符串:
//返回“未读消息计数”的本地化字符串。
//
// -参数值:用于格式化字符串的值。
// - 返回:本地化字符串。
上市 静态 fun未读物(_ 价值: ㈡ ) - &GT.; 字符串 {
返回StringLoader。FormattedStringWithKey. ( “email.unreadMessages” ,碰碰:
classBundle,Intablename.:的表,价值:价值)
}
}
这些代码生成工具最终也将两种方式进行。它们将能够检查所有源代码,并确保在代码中实际引用构建中包含的每个图像和本地化字符串,并将我们从其应用程序包中的运输不必要的字节保持在重构中删除对这些资源的引用时。
模拟
优步是新的应用程序体系结构(肋骨)广泛使用协议以防止其各种组件去耦和可测试。我们在我们的第一次使用此架构新骑手应用程序并将我们的主要语言从Object-C移至Swift。由于SWIFT是一种非常静态的语言,因此单元测试变得有问题。动态语言具有良好的框架,可以通过动态创建或修改现有的具体类来构建测试模型,存根或支架。
具有静态语言,如SWIFT,创建专门的测试模型,允许您计算调用特定功能的次数(例如)要求您手动创建符合该协议的类。创建这些测试模型可能导致大量代码。通常,在更新原始协议时,需要手动修改所有此代码。
毋庸置疑,我们对手动编写和维护了我们数千项协议中的每一个的额外复杂性并不是很兴奋。
代码生成模拟
生成模拟类所需的信息在Swift协议中已经存在。对于Uber的用例,我们开始创建工具,让工程师可以通过简单的注释来自动生成任何协议的测试模拟。
我们的工具将解析每个目标中的每个Swift文件的抽象语法树,找到任何已被注释的协议@CreateMock评论,并为每个人生成符合课程。
考虑以下协议:
协议UserPresentable{
var.侦听器:UserPresentableListener{ 得到 }
fun更新(withuserinformation用户信息:UserInformation) - &GT.; BOOL.
}
在这个协议中,具体的呈现器类将使用listener属性来显示用户信息并向其父类报告用户操作,该属性的类型是另一个协议。在包含上述协议的代码基上运行模拟生成工具将生成以下模拟类:
///用于测试的UserPresentableMock类。
班级UserPresentableMock:UserPresentable{
/ /变量
var.listener = userpresentablelistenermock.( ) {
did {listorereetcallcount.+ = 1 }
}
var.listenerSetCallCount =0
//函数处理程序
var.updateHandler: ( (_ withUserInformation:UserInformation) - &GT.; ( ) )?
var.updatecallcount.: ㈡=0
在里面 ( ) {
}
fun更新(withuserinformation用户信息:UserInformation) - &GT.; BOOL. {
updatecallcount.+ = 1
如果 让updateHandler = UpdateHandler.{
返回updateHandler(项目)
}
//默认返回值
返回 真的
}
}
侦听器属性和更新函数都包含可在测试中用于验证侦听器属性设置器或更新函数是否已被调用一定次数的计数器。生成的类符合UserPresentable协议通过实现协议所需的属性和方法。结果,它可以用作单元测试中的具体实现的待机。生成的代码有一些有趣的功能,概述如下:
- 一个updateHandler已生成属性,使工程师能够实现验证调用更新函数的参数的测试。
- 原始协议中的侦听器属性是非可选的。模拟生成能够通过构造它已经递归生成的另一协议模拟的实例来满足此合同。
- 类似地,将生成更新处理程序的默认返回值。模拟生成可以为所有原语、协议、模型对象以及许多Foundation和UIKit类生成默认值。对于许多单元测试,返回值并不重要,因为您只测试是否实际调用了某些函数。使用默认的返回值可以提高开发人员的工作效率,因为他们不再需要为测试中涉及的所有函数实现处理程序。
我们的骑手应用程序的iOS代码库包含了大约1500个这些生成的模拟。如果没有我们的代码生成工具,所有这些都将不得不手工编写和维护,这将使测试更加费时。自动生成的模拟为我们今天所拥有的单元测试覆盖率做出了很大的贡献。
前进
我们希望本文展示了支出工程时间写作工具的实际价值,用于代码生成。
除了讨论的用例外,我们还利用代码生成生成iOS和Android应用程序中使用的所有REST端点和模型。事实上,由这些工具产生的代码所占的比例与20%我们iOS代码库中的所有代码。
正确利用,代码生成可以使您的代码更加可靠,您的工程师更富有成效。在优步,这归结为两个主要用例:
- 在代码中表示现有资源。例如,我们的后端使用节俭描述我们的REST API,包括请求和响应模型。服务端点和模型必须在代码中表示,因此您可以手动维护这些模型(以及风险错误),或者您可以自动生成它们。
- 增加可靠性。我们的资源访问器代码生成就是一个很好的例子。如果没有代码生成,访问资源的潜在错误将在运行时引发,而通过代码生成,我们将获得编译时安全性。
我们以多种原因建立了这些代码生成工具,包括在我们开始努力时没有许多开源工具。今天,有一些很棒的开源工具来生成资源访问器,如SwiftGen.和Sourcery可以帮助您生成需要的泛型代码。
如果建立同时增强开发人员生产力和代码可靠性的建筑工具和系统,请考虑申请职务在我们的团队!
Tuomas Artman是一家位于旧金山的优步开发者体验团队的软件工程师。





