决定一个类的属性(成员变量)是公共(public)还是私有(private) ,是面向对象设计中的核心问题之一。这不仅关乎代码封装性,还直接影响系统的可维护性、可扩展性和健壮性。
以下是系统化的思考框架和实用原则,帮助你做出合理决策:
🔑 核心原则:最小暴露原则(Principle of Least Exposure)
"只暴露必须暴露的内容,其余一律隐藏。"
换句话说:
✅ 默认私有 ,只有当确实需要外部访问时,才设为公共。
一、判断标准:问自己这几个问题
1. 外部是否需要直接读取这个值?
- ✅ 是 → 考虑
public或提供 getter - ❌ 否 →
private
📌 示例:
typescriptclass BankAccount { private balance: number; // 外部不应直接读余额(需鉴权/日志) getBalance(): number { /* ... */ } // 通过方法控制访问 }
2. 外部是否需要直接修改这个值?
- ✅ 是 → 考虑
public或提供 setter(但要谨慎!) - ❌ 否 →
private
⚠️ 直接暴露可变状态容易导致 bug:
ini// 危险! user.profile.settings.darkMode = true; // 绕过校验/事件通知更好的方式:
perluser.setTheme('dark'); // 内部可触发 re-render / save / log
3. 这个属性是否属于"内部实现细节"?
- 如果未来可能重构、重命名或删除 它 → 必须私有
- 如果它是稳定契约的一部分(如 API 返回结构)→ 可 public
💡 例子:
- 缓存字段(
private cache: Map<...>)→ 私有- 用户 ID(
public id: string)→ 公共(业务标识)
4. 是否需要保持对象的"不变性"(Invariants)?
如果属性参与维持对象的内部一致性,则必须私有,并通过方法控制变更。
📌 示例:矩形的宽高不能为负数
typescriptclass Rectangle { private _width: number; private _height: number; setWidth(w: number) { if (w < 0) throw new Error('Width must be positive'); this._width = w; } }
二、优先使用 方法(Method)而非公共属性
即使需要"读取"或"设置",也优先提供方法而非直接暴露属性:
| 场景 | 推荐做法 |
|---|---|
| 读取计算值 | getFullName() 而非 fullName(除非是简单数据) |
| 设置需校验 | setEmail(email) 而非 email = ... |
| 触发副作用 | activate() 而非 isActive = true |
✅ 好处:
- 未来可加日志、权限、缓存、事件通知等逻辑
- 避免"属性被意外覆盖"导致状态不一致
三、特殊情况处理
✅ 可以公开的属性类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 不可变数据 | 初始化后永不改变 | public readonly id: string |
| 纯数据载体(DTO/POJO) | 仅用于传输,无行为 | interface UserDTO { name: string; email: string } |
| 配置对象 | 明确设计为可读写的配置 | public config: RenderConfig(但建议用 getter/setter 封装) |
❌ 应避免公开的属性
- 内部状态(如
isLoading,retryCount) - 依赖其他属性的派生值(如
fullName = firstName + lastName→ 应用 getter) - 敏感数据(密码、token、余额)
- 复杂对象引用(如
private domElement: HTMLElement)
四、TypeScript / JavaScript 中的具体实践
方案 1:使用 # 私有字段(推荐,ES2022+)
kotlin
class Timer {
#startTime: number;
#isRunning = false;
start() {
this.#startTime = Date.now();
this.#isRunning = true;
}
get elapsed() {
return this.#isRunning ? Date.now() - this.#startTime : 0;
}
}
✅ 真正私有,运行时安全
方案 2:TypeScript private(仅开发时保护)
typescript
class Logger {
private logs: string[] = [];
log(msg: string) { this.logs.push(msg); }
}
⚠️ 注意:编译后仍可被外部访问,仅防"手误"
方案 3:readonly + 公共(用于不可变数据)
typescript
class Point {
constructor(
public readonly x: number,
public readonly y: number
) {}
}
✅ 安全暴露,且不可修改
五、团队协作建议
- 约定优于配置:团队统一规则,如"所有状态属性默认私有"
- 代码审查重点 :检查是否有不必要的
public属性 - 文档说明:对 public 属性明确其用途和约束
✅ 快速决策流程图
csharp
这个属性需要被外部访问吗?
│
├─ 否 → private / #
│
└─ 是 →
├─ 是否需要修改?
│ ├─ 是 → 提供 setter 方法(而非直接 public)
│ └─ 否 →
│ ├─ 是否不可变? → public readonly
│ └─ 是否计算值? → 提供 getter 方法
│
└─ 是否属于稳定数据契约? → 可 public(如 DTO)
🎯 总结:黄金法则
"属性代表状态,状态应受控。
暴露行为(方法),而非状态(属性)。"
- 默认 私有 (
private或#) - 仅在必要且安全时暴露为公共
- 优先通过 方法 控制访问,而非直接暴露字段
- 对于纯数据对象(如 API 响应),可适当放宽
这样做,你的类将更健壮、更易测试、更易演进。