第7章:封装(Encapsulation)
一、导读
1.1 本章学习目标
第7章聚焦于封装相关的重构手法。封装是面向对象设计的核心原则,它通过隐藏实现细节来降低系统的复杂度。本章介绍如何通过重构来改善代码的封装性。
本章的核心学习目标包括:
- 掌握 Encapsulate Record:用函数替代直接访问字段
- 理解 Encapsulate Collection:控制集合的访问和修改方式
- 学会 Replace Primitive with Object:用对象封装基本类型
- 掌握 Replace Temp with Query:用函数调用替代临时变量
1.2 为什么本章如此重要
封装的价值在于降低耦合。当实现细节被隐藏后,你可以自由地修改内部实现而不影响外部调用者。这种"信息隐藏"是管理复杂性的关键手段。
本章之所以重要,是因为很多代码的坏味道都源于封装不足——直接暴露内部数据、直接修改集合、用基本类型表示复杂概念。通过本章的重构手法,你可以逐步改善代码的封装性,使其更加健壮和可维护。
1.3 核心问题引导
为什么要封装记录(对象)?直接访问字段有什么问题?
封装集合的关键是什么?为什么不能直接返回集合引用?
什么时候应该用对象替代基本类型?
Replace Temp with Query 的好处是什么?
二、核心概念详解
2.1 Encapsulate Record(封装记录)
动机:直接访问数据结构的字段会导致调用者与数据结构的具体实现紧密耦合。通过封装,你可以用函数来控制数据的访问和修改。
// 重构前:直接访问字段
const organization = { name: "Acme", country: "GB" };
// 调用者直接依赖数据结构
function adjustCountry(org) {
org.country = org.country.toUpperCase();
}
// 重构后:封装为类
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() { return this._name; }
set name(value) { this._name = value; }
get country() { return this._country; }
set country(value) { this._country = value; }
}
// 调用者通过接口访问
function adjustCountry(org) {
org.country = org.country.toUpperCase();
}2.2 Encapsulate Collection(封装集合)
动机:直接暴露集合的内部引用会导致外部代码随意修改集合,破坏封装性。应该提供添加/移除方法,而不是直接返回集合。
// 重构前:直接暴露集合
class Course {
constructor() {
this.students = []; // 直接暴露内部数组
}
}
// 外部代码可以随意修改
course.students.push(student);
course.students.splice(0, 1);
course.students = []; // 甚至可以替换整个集合
// 重构后:封装集合访问
class Course {
constructor() {
this._students = [];
}
addStudent(student) {
if (!this._students.includes(student)) {
this._students.push(student);
}
}
removeStudent(student) {
this._students = this._students.filter(s => s !== student);
}
get students() {
return [...this._students]; // 返回副本,防止外部修改
}
get studentCount() {
return this._students.length;
}
}2.3 Replace Primitive with Object(以对象取代基本类型)
动机:当基本类型(字符串、数字)开始承载额外的行为或约束时,应该用对象来封装它。
// 重构前:用字符串表示电话号码
const phoneNumber = "123-456-7890";
// 无法验证格式
// 无法格式化显示
// 无法比较
// 重构后:用对象封装
class PhoneNumber {
constructor(value) {
if (!this.isValid(value)) {
throw new Error(`Invalid phone number: ${value}`);
}
this._value = value;
}
isValid(value) {
return /^\d{3}-\d{3}-\d{4}$/.test(value);
}
toString() {
return this._value;
}
get areaCode() {
return this._value.split('-')[0];
}
equals(other) {
return this._value === other._value;
}
}
const phoneNumber = new PhoneNumber("123-456-7890");
phoneNumber.areaCode; // "123"2.4 Replace Temp with Query(以查询取代临时变量)
动机:临时变量只在所属函数的上下文中才有意义。如果将临时变量的计算逻辑提取为函数,其他函数也可以访问这个值。
// 重构前:使用临时变量
function calculateOrderTotal(order) {
const basePrice = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const discount = basePrice > 100 ? 0.1 : 0;
const finalPrice = basePrice * (1 - discount);
return finalPrice;
}
// 重构后:以查询取代临时变量
function calculateOrderTotal(order) {
return this.basePrice(order) * (1 - this.discountRate(order));
}
function basePrice(order) {
return order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function discountRate(order) {
return this.basePrice(order) > 100 ? 0.1 : 0;
}三、实战案例
3.1 案例:封装一个配置管理模块
// 重构前:直接暴露配置对象
const config = {
database: { host: 'localhost', port: 5432 },
cache: { ttl: 3600 }
};
// 任何地方都可以修改
config.database.port = 3306;
config.cache.ttl = 'invalid'; // 没有验证
// 重构后:封装配置管理
class ConfigManager {
constructor(initialConfig) {
this._config = this.validateAndClone(initialConfig);
}
get databaseHost() { return this._config.database.host; }
get databasePort() { return this._config.database.port; }
get cacheTTL() { return this._config.cache.ttl; }
set(key, value) {
const validated = this.validate(key, value);
this._setNestedValue(key, validated);
}
validate(key, value) {
if (key === 'database.port') {
if (typeof value !== 'number' || value < 1 || value > 65535) {
throw new Error('Invalid port number');
}
}
if (key === 'cache.ttl') {
if (typeof value !== 'number' || value < 0) {
throw new Error('Invalid TTL');
}
}
return value;
}
_setNestedValue(key, value) {
const keys = key.split('.');
let obj = this._config;
for (let i = 0; i < keys.length - 1; i++) {
obj = obj[keys[i]];
}
obj[keys[keys.length - 1]] = value;
}
validateAndClone(config) {
return JSON.parse(JSON.stringify(config));
}
toJSON() {
return JSON.parse(JSON.stringify(this._config));
}
}四、本章小结
本章介绍了封装相关的重构手法:
Encapsulate Record:用函数控制数据的访问和修改
Encapsulate Collection:提供添加/移除方法,不直接暴露集合
Replace Primitive with Object:用对象封装基本类型,增加行为
Replace Temp with Query:用函数调用替代临时变量,提高复用性
封装的核心思想是:隐藏实现细节,暴露稳定的接口。
五、思考与练习
思考题
封装是否会降低性能?如何权衡封装与性能?
在函数式编程中,封装的概念有什么不同?
如何判断封装的粒度是否合适?
封装与 API 设计有什么关系?
编程练习
找到一段直接访问字段的代码,使用 Encapsulate Record 进行改善。
找到一段直接暴露集合的代码,使用 Encapsulate Collection 进行封装。
找到一个用基本类型表示的领域概念,使用 Replace Primitive with Object 进行封装。