Gemini 解惑:以类为中心的 OOP(Java)➔ 以函数为中心(Go/Kotlin)的多范式编程思维转变
困扰:
我原是 Java 程序员,在学 Kotlin 和 Golang 后,我有点不知所措,突然不会编程了。 因为在 Java 中类是一等公民,先写类再在类中写方法, 而 Kotlin 和 Golang 中函数和类可以处于同一级别,函数和类都是一等公民, 那我在用它写代码时我不知道该怎样组织代码,不知道是直接写函数还是写类,先写类还是先写函数, 或者两者混合,怎样组合,谁多谁少等,比例原则,什么时候该直接写类,什么时候该直接写函数,很困惑。
Gemini:
你好!我非常理解你的困惑。从一个“万物皆类”的 Java 世界,突然进入到函数可以和类平起平坐的 Kotlin 与 Golang 世界, 会有一种“规则被打破”的不适感。这就像你一直玩的是国际象棋,突然换成了围棋,棋子都一样,但“怎么下”的思路完全变了。
你遇到的不是技术问题,而是 编程范式(Programming Paradigm) 的转变问题。
Java 强制你使用面向对象(OOP)的思维来组织一切,所以你的默认动作是“先建一个类”。 Kotlin 和 Golang 则是多范式语言,它们给了你更多的自由,但也带来了“选择的困惑”。
这里有一份为你量身打造的“代码组织指南”,帮你走出这个困惑区。
核心原则是:优先考虑数据和行为。如果行为需要绑定和修改数据(状态),就用类/结构体;如果行为是独立的、无状态的,就用函数。
1. 针对 Kotlin:更优雅的 Java
把 Kotlin 想象成一个“更现代、更务实”的 Java。它仍然是强面向对象的,但它帮你清理了很多 Java 的“历史包袱”。
在 Java 中,你经常需要创建 XxxUtils
或 Helper
这样的“工具类”,里面全都是 static
方法。
你心里清楚,这些类根本不是真正的“对象”,它们只是一个函数的容器,因为 Java 语法强制你必须把函数放在类里。
Kotlin 说:“别装了,我允许你直接写函数。”
什么时候在 Kotlin 中写“类” (Class)?
答案:当你需要封装状态(State)和与该状态紧密相关的行为时。
这和你在 Java 中的直觉完全一样。如果一个东西有属性(成员变量)和需要操作这些属性的方法(成员函数),那它就应该是一个类。
- 模型/实体(Data Models):
data class User(val id: String, val name: String)
- 服务(Services):
class UserService(private val db: Database) { fun findUser(id: String): User { ... } }
- 状态管理器(State Holders):
class ShoppingCart { private val items = mutableListOf<Item>(); fun addItem(item: Item) { ... } }
简单说:但凡在 Java 里你觉得理应写个类的地方,在 Kotlin 里一样写类。
什么时候在 Kotlin 中写“顶级函数” (Top-level Function)?
答案:当你发现这个函数不依赖任何特定类的内部状态时。
在 Java 中,这些就是你被迫写成 public static
方法的那些函数。
- 工具/辅助函数:
- Java:
class StringUtils { public static boolean isEmail(String s) { ... } }
- Kotlin: (在
StringUtils.kt
文件里)fun isEmail(s: String): Boolean { ... }
- Java:
- 转换函数:
fun userDtoToDomain(dto: UserDto): User { ... }
- 扩展函数(Extension Functions): 这是 Kotlin 的一大特色,它允许你“假装”把一个顶级函数“塞进”一个已有的类里。
- 你想给
String
类加一个isEmail
的方法,但你不能修改String
源码。 - 你可以写一个顶级扩展函数:
fun String.isEmail(): Boolean { ... }
- 然后你就可以像调用成员方法一样使用它:
"test@example.com".isEmail()
- 你想给
Kotlin 代码组织建议
-
按功能组织文件,而非按类。
- Java 强制一个
public class
一个文件。 - Kotlin 允许你把多个相关的小类、接口和顶级函数放在同一个
.kt
文件里。 - 例如,你可以有一个
UserRepository.kt
文件,里面同时包含:interface UserRepository
class DbUserRepository(private val db: Database) : UserRepository
data class UserQuery(val name: String)
fun defaultUserQuery(): UserQuery = UserQuery("default")
(一个辅助函数)
- Java 强制一个
-
你的思维转变: 从“我需要一个工具类来放这个方法”转变为“我需要一个函数,它应该和 Xxx 功能放在一个文件里”。
2. 针对 Golang:一种全新的思维
如果说 Kotlin 是对 Java 的“改良”,那么 Go 就是“另起炉灶”。你必须暂时忘掉 Java 的 OOP 思想,尤其是“类”、“继承”和“异常”。
Go 的核心组织单位是 包 (Package),而不是类。
在 Go 中,你的默认动作应该是:“我这个功能属于哪个包?”
Go 没有“类”,它有“结构体” (Struct) 和“方法” (Method)
- Struct (结构体): 只是一个数据的集合体。它没有方法,就是纯数据。
type User struct { ID string; Name string; }
- Method (方法): 是一个“绑定”到某个特定类型(比如 Struct)的函数。它通过“接收器”(receiver)来实现。
// 这是一个 Struct (纯数据)
type Rectangle struct {
Width float64
Height float64
}
// 这是一个 "方法" (Method)
// 它绑定到了 *Rectangle 类型上,可以访问 Rectangle 的数据
func (r *Rectangle) Area() float64 {
return r.Width * r.Height
}
什么时候在 Go 中写“结构体 + 方法”?
答案:和 Kotlin/Java 一样:当你需要封装状态(数据)和操作该状态的行为时。
Rectangle
例子就是最好的证明。Area()
这个行为逻辑上就属于 Rectangle
的数据。
在 Java 中你会写:
class Rectangle {
double width;
double height;
public double area() {
return this.width * this.height;
}
}
在 Go 中,你只是把它们“分开定义,但逻辑上绑定”而已。
什么时候在 Go 中写“包级函数” (Package-level Function)?
答案:所有其他情况。
这包括了 Java 里的 static
方法、构造函数以及 Kotlin 里的顶级函数。
- 工具/辅助函数:
- 在
strings
包里,有一个ToUpper(s string) string
函数。它不需要一个StringUtil
结构体,它就是一个纯粹的转换函数。
- 在
- “构造函数” (Constructors): Go 没有构造函数,而是用一个包级函数来返回一个实例。
func NewUser(name string) *User { return &User{ID: generateId(), Name: name} }
- 不依赖特定实例状态的服务逻辑:
func SendEmail(to string, body string) error { ... }
Golang 代码组织建议
-
忘掉“层”,拥抱“域”。
- Java 项目常按“层”组织:
com.myapp.controller
、com.myapp.service
、com.myapp.repository
。 - Go 项目常按“功能域”组织:
user
、order
、payment
。
- Java 项目常按“层”组织:
-
一个包(一个目录)就是一个功能单元。
- 比如一个
user
包 (目录user/
):user.go
: 定义type User struct { ... }
和type UserService struct { ... }
service.go
: 定义func (s *UserService) FindUser(id string) (*User, error) { ... }
store.go
: 定义type UserStore struct { ... }
和它的方法(比如FindUserInDB
)http.go
: (可选) 定义func (s *UserService) HandleGetUser(w http.ResponseWriter, r *http.Request) { ... }
- 这个包对外暴露 (大写字母开头) 的可能只有
UserService
和NewUserService(...)
函数。其他的一切(UserStore
、小写的辅助函数)都是包内私有的。
- 比如一个
-
你的思维转变: 从“这个方法属于哪个类?”转变为“这个函数(或方法)应该放在哪个包里?它是否需要绑定到一个结构体上才能工作?”
总结:你的新编程思路
当你再感到不知所措时,按这个流程来思考:
- 我要实现的功能是什么? (例如:“验证一个邮箱地址”)
- 这个功能是否需要依赖或修改某个“对象”的内部数据(状态)?
- 否 (如“验证邮箱”,它不依赖任何对象,只依赖输入的字符串):
- Kotlin: 写一个顶级函数
fun isEmail(s: String): Boolean
。把它放在一个功能相关的文件里,比如Validation.kt
。 - Go: 写一个包级函数
func IsEmail(s string) bool
。把它放在一个功能相关的包里,比如validation
。
- Kotlin: 写一个顶级函数
- 是 (如“给购物车添加商品”,它需要修改购物车里的
items
列表):- Kotlin: 定义一个
class ShoppingCart
,包含private val items
属性,并为它写一个成员方法fun addItem(item: Item)
。 - Go: 定义一个
type ShoppingCart struct { items []Item }
,并为它写一个方法func (c *ShoppingCart) AddItem(item Item)
。
- Kotlin: 定义一个
- 否 (如“验证邮箱”,它不依赖任何对象,只依赖输入的字符串):
你没有“退步”,你只是正在“升级”。从单一的 OOP 范式升级到了更灵活的多范式思维。多写一些代码,这种新的直觉很快就会建立起来。祝你学习愉快!