样板代码是开发人员在每个新模块或项目中以最小改动编写的模板化代码。它不包含独特的业务逻辑,而只是准备基础设施:配置、库的接入、标准处理程序和DTO类。根据CodeScene工程生产力报告(2025)的数据,样板代码占典型商业应用程序总代码的20%到40%。这类代码的主要问题不在于它重复,而在于每次重复都是一个故障点:一个副本中的错误不会与其他副本同步,导致bug在项目中扩散。通过代码生成、注解和宏来自动化样板代码的生成,是在不损失质量的情况下加速开发的最有效方法之一。
要点
样板代码(boilerplate code)是在项目的不同部分以最小变化重复的源代码片段。该术语来自印刷行业,其中boilerplate指的是为报纸准备的无需重写的文本模板。在编程中,这是指为了满足框架、语言或架构的要求而不得不反复编写的任何代码。
样板代码不是传统意义上的技术债务——它不包含bug,也不违反SOLID原则。然而,它增加了需要维护、测试和阅读的代码量。样板代码的每一行都是一个潜在的错误点,编译器并不总能捕捉到。
根据JetBrains开发者生态系统报告(2025)的数据,67%的开发人员认为样板代码是生产力下降的主要原因。在移动开发中,这个数字更高:基于Java的Android项目包含大量模板代码,用于findViewById、Intent、RecyclerView适配器和ContentProvider。Kotlin和Swift通过语法手段解决了部分问题,但样板代码并未完全消失。
在设计架构时,尽量选择能最小化模板代码的解决方案。例如,在Kotlin中使用@Parcelize代替手动编写Parcelable实现,使用带@HiltViewModel的Hilt代替ViewModel工厂。每一次这样的优化都能在项目规模上节省数小时的开发时间。
Android开发中最知名的样板代码示例是RecyclerView.Adapter。在Kotlin和ViewBinding出现之前,每个适配器需要大约80-100行模板代码:onCreateViewHolder、onBindViewHolder、getItemCount、内部ViewHolder类、构造函数、字段绑定。有了ViewBinding,代码减少了,但并未完全消失。
class UserAdapter(
private val users: List<User>
) : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
holder.bind(users[position])
}
override fun getItemCount(): Int = users.size
class ViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun bind(user: User) {
Glide.with(itemView)
.load(user.avatarUrl)
.into(itemView.avatar)
}
}
}
另一个典型示例是在没有库的情况下的Java JSON映射。手动解析API响应需要编写数十个方法,每个方法检查键是否存在、获取值并将其分配给字段。有了像Gson、Moshi或Kotlin Serialization这样的库——只需要一个@Serializable注解。
在iOS开发中,经典的样板代码是为每个API响应实现CodingKey和Decodable,特别是当JSON键与camelCase属性名称不同时。尽管Codable是自动生成的,但手动列举CodingKeys仍然是模板代码的来源。
在构建阶段使用代码生成来创建样板代码。在Android中——使用Annotation Processing(KSP)用于Room、Dagger、Moshi。在iOS中——使用Sourcery用于Codable和AutoMockable。每花一小时配置生成,就能节省数天的手动复制时间。
样板代码以三种方式损害项目:拖慢新功能的编写、使现有代码的阅读复杂化、并在变更时产生不同步点。
开发速度的降低是显而易见的:开发人员花费时间编写不包含业务逻辑的代码。而不是实现一个新功能(例如,在用户个人资料中添加一个字段),他要编写数据库迁移、DTO类、到领域实体的映射器、带有输入字段的屏幕、验证和每一层的测试。这项工作的大部分是机械性的。
不同步是一个更隐蔽的问题。当在一个地方更改数据结构时(例如,在API响应中添加一个字段),开发人员必须更新DTO、映射器、模型、屏幕和测试。如果遗漏了一个地方,应用程序可以编译,但在运行时崩溃——或者更糟——显示不正确的数据而没有错误。样板代码的层数越多,这种不同步的可能性就越大。
分析项目中重复的模式。如果你看到三个同名不同类的类——这就是生成的候选对象。将代码生成作为架构决策的一部分来实施,而不是作为一次性的优化。这在每个新模块上都会得到回报。
代码生成是应对样板代码最可靠的方式。开发人员不是手动编写模板代码,而是描述元数据(注解、模式、配置),然后生成器在编译阶段创建现成的代码。
在Android生态系统中,标准的代码生成工具是KSP(Kotlin符号处理)。它取代了过时的KAPT,并且由于直接访问Kotlin AST而无需生成Java桩,从而运行得更快。KSP被Room(生成DAO实现)、Moshi(生成JsonAdapter)、Glide(生成目标加载类)和Dagger(生成DI图)使用。
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: Long,
@ColumnInfo(name = "full_name") val name: String,
@ColumnInfo(name = "avatar_url") val avatarUrl: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getById(@Param("id") id: Long): UserEntity?
}
在iOS开发中,代码生成的角色由Sourcery承担——这是一个处理Stencil模板并根据注释中的注解生成Swift代码的工具。典型场景包括:AutoMockable(生成测试用的mock)、AutoCodable(无需CodingKeys生成Decodable实现)、AutoEquatable和AutoLenses。
对于Flutter项目,通过build_runner的生成器减少样板代码:json_serializable用于JSON映射,freezed用于带copyWith的不可变模型,retrofit_generator用于API客户端,injectable_generator用于DI。这些生成器中的每一个都将10-20行注解转化为数百行现成的代码。
注解和宏是一种声明式方式,指示编译器或预处理器需要生成什么代码。开发人员不编写实现,只标记意图,然后生成器将标记转化为现成的代码。
最典型的例子是Java中的Lombok(历史上)和Kotlin data class。Kotlin中的data class自动生成equals、hashCode、toString、componentN和copy——而在Java中,这需要手写大约80行代码或使用带@Data的Lombok。Kotlin在语言级别解决了这个问题,使样板代码变得隐式。
在Swift中,宏(Swift Macros,在Swift 5.9中引入)扮演着类似的角色。开发人员不需要手动编写Codable实现,而是用@Codable标记结构——然后编译器自己生成必要的代码。其他内置宏包括:@Observable(可观察状态)、@ResultBuilder(结果构建器)和@MainActor(主线程调度)。
@Codable
struct UserProfile {
let id: Int
let displayName: String
let avatarURL: URL
let bio: String?
}
// @Codable 宏会生成:
// extension UserProfile: Codable { }
// private enum CodingKeys: String, CodingKey {
// case id, displayName, avatarURL, bio
// }
在代码生成和宏之间进行选择时,如果语言支持宏,应优先选择宏。宏在编译器级别工作,不需要配置构建脚本,不会减慢编译速度(与注解处理不同),并且始终与源代码同步。如果宏不可用——则通过KSP、Sourcery或build_runner使用外部生成器。
每种语言和平台都提供自己的工具来最小化样板代码。以下是针对主要移动开发栈的具体实践。
| 平台 | 工具/技巧 | 替代什么 |
|---|---|---|
| Android / Kotlin | data class | equals, hashCode, toString, copy, componentN |
| Android / Kotlin | @Parcelize | Parcelable实现 |
| Android / Kotlin | ViewBinding / DataBinding | findViewById, ButterKnife |
| iOS / Swift | Codable + 宏 | 手动JSON解析, CodingKeys |
| iOS / Swift | Sourcery | AutoMockable, AutoEquatable, AutoLenses |
| Flutter / Dart | freezed + json_serializable | copyWith, 密封类, equals/hashCode, JSON |
| Flutter / Dart | retrofit_generator | 带请求和响应类型的API客户端 |
对于Web前端(React Native / TypeScript),主要工具是从OpenAPI规范生成类型(openapi-typescript、swagger-codegen)。每个端点自动获得类型化的请求和响应——开发人员不需要为数百个API调用手动描述接口。
在项目的早期阶段就引入代码生成。将现有项目迁移到生成器比从一开始就使用它们进行设计更难。如果项目已经编写完成——从最痛点开始:Java → Kotlin(data class)、手动适配器 → 带DiffUtil的ListAdapter、手动JSON映射 → Moshi / Kotlin Serialization。
常见问题
样板代码不是债务,而是冗余:代码是正确的,但太多了。技术债务是有意识的折衷决策,以后必须修复。样板代码不需要修复——它需要自动化。
不,在小型项目中,样板代码可以通过简单性来证明其合理性:它立即可见且易于更改。问题出现在规模上——当同类模块超过十个时,手动复制就不再高效,是时候引入生成了。
依赖于具有非标准逻辑的外部服务(自定义SDK、专有协议)的代码很难生成。在这种情况下,样板代码是手动编写的,但会隔离到单独的模块中以最小化项目中的分散。
对于新项目,最好直接切换到Kotlin,其中data class在语言级别解决同样的问题。如果项目继续使用Java——Lombok仍然是事实上的标准,但请注意它需要IDE插件,并可能与新版本的Java冲突。
是的,代码生成会为编译增加时间。KSP比KAPT运行得更快,但仍会为完整编译增加数秒或数分钟。优化:使用增量编译和构建之间生成结果的缓存。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。