JavaScript对象与数据结构:封装与抽象的艺术

JavaScript对象与数据结构:封装与抽象的艺术

【免费下载链接】clean-code-javascript :bathtub: Clean Code concepts adapted for JavaScript 【免费下载链接】clean-code-javascript 项目地址: https://gitcode.com/GitHub_Trending/cl/clean-code-javascript

本文深入探讨了JavaScript中对象与数据结构的封装与抽象艺术,重点分析了getter和setter在数据封装中的核心作用,比较了对象与原始数据结构的优劣,强调了避免创建不必要对象属性的重要性,并详细阐述了组合优于继承的设计原则。通过丰富的代码示例和实际应用场景,展示了如何构建健壮、可维护的JavaScript应用程序。

使用getter和setter进行数据封装

在现代JavaScript开发中,数据封装是构建健壮、可维护应用程序的核心原则。getter和setter作为实现封装的重要工具,为开发者提供了对对象属性访问的精细控制能力。通过合理使用这些访问器方法,我们可以在保持代码简洁性的同时,确保数据的安全性和一致性。

getter和setter的基本语法

JavaScript中的getter和setter使用特殊的语法定义,它们看起来像属性但实际上是有特殊行为的方法:

class BankAccount {
  constructor(balance) {
    this._balance = balance;
  }

  // Getter方法
  get balance() {
    console.log('余额被访问');
    return this._balance;
  }

  // Setter方法
  set balance(newBalance) {
    if (newBalance < 0) {
      throw new Error('余额不能为负数');
    }
    console.log('余额被修改');
    this._balance = newBalance;
  }
}

const account = new BankAccount(1000);
console.log(account.balance); // 输出: 余额被访问 1000
account.balance = 1500;       // 输出: 余额被修改

数据验证与业务逻辑封装

getter和setter最强大的功能之一是能够在属性访问时执行验证逻辑:

class User {
  constructor() {
    this._email = '';
    this._age = 0;
  }

  get email() {
    return this._email;
  }

  set email(value) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      throw new Error('无效的邮箱格式');
    }
    this._email = value;
  }

  get age() {
    return this._age;
  }

  set age(value) {
    if (value < 0 || value > 150) {
      throw new Error('年龄必须在0-150之间');
    }
    this._age = value;
  }
}

计算属性与惰性求值

getter非常适合用于创建计算属性,这些属性的值在访问时动态计算:

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  get area() {
    return this.width * this.height;
  }

  get perimeter() {
    return 2 * (this.width + this.height);
  }

  get isSquare() {
    return this.width === this.height;
  }
}

const rect = new Rectangle(10, 5);
console.log(rect.area);      // 50
console.log(rect.perimeter); // 30
console.log(rect.isSquare);  // false

访问控制与权限管理

通过getter和setter可以实现精细的访问控制:

class SecureData {
  constructor(secret, accessLevel) {
    this._secret = secret;
    this._accessLevel = accessLevel;
  }

  get secret() {
    if (this._accessLevel < 2) {
      throw new Error('权限不足');
    }
    return this._secret;
  }

  set secret(value) {
    if (this._accessLevel < 3) {
      throw new Error('无修改权限');
    }
    this._secret = value;
  }
}

性能优化与缓存机制

getter可以用于实现智能缓存,避免重复计算:

class ExpensiveCalculation {
  constructor() {
    this._data = null;
    this._cachedResult = null;
    this._isDirty = true;
  }

  set data(value) {
    this._data = value;
    this._isDirty = true; // 标记数据已更新,需要重新计算
  }

  get result() {
    if (this._isDirty) {
      console.log('执行复杂计算...');
      this._cachedResult = this._performExpensiveCalculation();
      this._isDirty = false;
    }
    return this._cachedResult;
  }

  _performExpensiveCalculation() {
    // 模拟复杂计算
    return this._data * Math.random();
  }
}

接口兼容性与重构友好性

使用getter和setter可以保持接口的稳定性,即使内部实现发生变化:

// 初始实现
class ProductV1 {
  constructor(price) {
    this.price = price;
  }
}

// 重构后的实现 - 添加了货币单位
class ProductV2 {
  constructor(price) {
    this._priceInCents = price * 100; // 内部存储单位为分
  }

  get price() {
    return this._priceInCents / 100; // 对外接口保持不变
  }

  set price(value) {
    this._priceInCents = Math.round(value * 100);
  }
}

与普通方法的对比

下表展示了getter/setter与普通方法的主要区别:

特性Getter/Setter普通方法
语法obj.propertyobj.method()
调用方式属性访问语法方法调用语法
可读性更自然,像属性明确的方法调用
链式调用不支持支持
参数传递setter可接受一个参数可接受多个参数
使用场景简单的属性访问控制复杂的业务逻辑

实际应用场景示例

class ShoppingCart {
  constructor() {
    this._items = [];
    this._discount = 0;
  }

  get items() {
    return [...this._items]; // 返回副本防止外部修改
  }

  get total() {
    return this._calculateTotal();
  }

  get discountedTotal() {
    return this.total * (1 - this._discount);
  }

  set discount(value) {
    if (value < 0 || value > 1) {
      throw new Error('折扣必须在0-1之间');
    }
    this._discount = value;
  }

  addItem(product, quantity = 1) {
    this._items.push({ product, quantity });
  }

  _calculateTotal() {
    return this._items.reduce((sum, item) => {
      return sum + (item.product.price * item.quantity);
    }, 0);
  }
}

最佳实践指南

  1. 保持一致性:在整个项目中统一使用getter/setter或直接属性访问
  2. 避免过度使用:只在需要验证、转换或副作用时使用
  3. 性能考虑:复杂的getter可能影响性能,必要时添加缓存
  4. 错误处理:在setter中提供清晰的错误信息
  5. 文档化:为复杂的getter/setter行为添加注释

mermaid

通过合理运用getter和setter,开发者可以构建出更加健壮、可维护的JavaScript应用程序,同时在保持代码简洁性的前提下实现复杂的数据管理需求。

对象优先于原始数据结构的优势

在现代JavaScript开发中,对象与原始数据结构的选择往往决定了代码的可维护性、扩展性和健壮性。对象提供了强大的封装能力,而原始数据结构虽然简单直接,但在复杂业务场景下往往显得力不从心。让我们深入探讨对象相比原始数据结构的显著优势。

封装与数据保护

对象通过getter和setter方法提供了强大的数据封装能力,这是原始数据结构无法比拟的。封装不仅仅是隐藏实现细节,更重要的是保护数据的完整性和一致性。

// 原始数据结构 - 脆弱的数据访问
const user = {
  name: "John",
  age: 30,
  email: "john@example.com"
};

// 直接修改可能导致数据不一致
user.age = -5; // 无效的年龄值
user.email = "invalid-email"; // 格式错误的邮箱

// 对象封装 - 健壮的数据访问
function createUser(name, age, email) {
  // 私有变量,外部无法直接访问
  let _name = name;
  let _age = age;
  let _email = email;

  return {
    getName() {
      return _name;
    },
    
    setName(newName) {
      if (typeof newName === 'string' && newName.length > 0) {
        _name = newName;
      } else {
        throw new Error('Invalid name');
      }
    },
    
    getAge() {
      return _age;
    },
    
    setAge(newAge) {
      if (typeof newAge === 'number' && newAge >= 0 && newAge <= 150) {
        _age = newAge;
      } else {
        throw new Error('Invalid age');
      }
    },
    
    getEmail() {
      return _email;
    },
    
    setEmail(newEmail) {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (emailRegex.test(newEmail)) {
        _email = newEmail;
      } else {
        throw new Error('Invalid email format');
      }
    }
  };
}

const userObj = createUser("John", 30, "john@example.com");
userObj.setAge(-5); // 抛出错误:Invalid age

行为与数据的统一

对象将数据和行为紧密结合,形成完整的业务实体,而原始数据结构只是数据的简单容器。

// 原始数据结构 + 分散的函数
const bankAccount = {
  balance: 1000
};

function deposit(account, amount) {
  account.balance += amount;
}

function withdraw(account, amount) {
  if (account.balance >= amount) {
    account.balance -= amount;
  } else {
    throw new Error('Insufficient funds');
  }
}

function getBalance(account) {
  return account.balance;
}

// 对象封装 - 数据和行为统一
function createBankAccount(initialBalance = 0) {
  let balance = initialBalance;
  let transactionHistory = [];

  return {
    deposit(amount) {
      if (amount > 0) {
        balance += amount;
        transactionHistory.push({
          type: 'deposit',
          amount,
          timestamp: new Date(),
          newBalance: balance
        });
      }
    },
    
    withdraw(amount) {
      if (amount > 0 && balance >= amount) {
        balance -= amount;
        transactionHistory.push({
          type: 'withdraw',
          amount,
          timestamp: new Date(),
          newBalance: balance
        });
      } else {
        throw new Error('Insufficient funds or invalid amount');
      }
    },
    
    getBalance() {
      return balance;
    },
    
    getTransactionHistory() {
      return [...transactionHistory]; // 返回副本保护原始数据
    },
    
    transfer(toAccount, amount) {
      this.withdraw(amount);
      toAccount.deposit(amount);
    }
  };
}

const account1 = createBankAccount(1000);
const account2 = createBankAccount(500);
account1.transfer(account2, 200);

扩展性与维护性

对象提供了更好的扩展机制,可以通过继承、组合等方式轻松添加新功能,而原始数据结构的扩展往往需要重写大量代码。

mermaid

类型安全与接口契约

对象通过明确定义的接口提供了类型安全性,编译器或运行时可以验证调用的正确性。

// 接口契约示例
class UserInterface {
  constructor() {
    if (this.constructor === UserInterface) {
      throw new Error('Cannot instantiate interface');
    }
  }
  
  getName() {
    throw new Error('Method not implemented');
  }
  
  setName(name) {
    throw new Error('Method not implemented');
  }
  
  validate() {
    throw new Error('Method not implemented');
  }
}

// 具体实现
class User extends UserInterface {
  constructor(name, email) {
    super();
    this._name = name;
    this._email = email;
  }
  
  getName() {
    return this._name;
  }
  
  setName(name) {
    if (typeof name === 'string' && name.trim().length > 0) {
      this._name = name.trim();
    }
  }
  
  validate() {
    const errors = [];
    if (!this._name || this._name.trim().length === 0) {
      errors.push('Name is required');
    }
    if (!this._email || !this._email.includes('@')) {
      errors.push('Valid email is required');
    }
    return errors;
  }
}

// 使用
const user = new User('John Doe', 'john@example.com');
const validationErrors = user.validate();
if (validationErrors.length === 0) {
  console.log('User is valid');
}

性能优化的灵活性

对象封装提供了性能优化的空间,可以实现延迟加载、缓存等高级优化策略。

class DataLoader {
  constructor(dataSource) {
    this.dataSource = dataSource;
    this._cache = null;
    this._lastLoadTime = null;
    this.cacheDuration = 300000; // 5分钟缓存
  }
  
  async loadData() {
    const now = Date.now();
    
    // 检查缓存是否有效
    if (this._cache && this._lastLoadTime && 
        (now - this._lastLoadTime) < this.cacheDuration) {
      console.log('Returning cached data');
      return this._cache;
    }
    
    console.log('Loading fresh data from source');
    try {
      // 模拟异步数据加载
      const data = await this.fetchFromSource();
      this._cache = data;
      this._lastLoadTime = now;
      return data;
    } catch (error) {
      console.error('Failed to load data:', error);
      // 即使加载失败,也返回缓存数据(如果存在)
      return this._cache || [];
    }
  }
  
  async fetchFromSource() {
    // 模拟网络请求
    return new Promise(resolve => {
      setTimeout(() => {
        resolve([{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }]);
      }, 1000);
    });
  }
  
  invalidateCache() {
    this._cache = null;
    this._lastLoadTime = null;
  }
}

// 使用
const loader = new DataLoader();
async function getData() {
  const data = await loader.loadData(); // 第一次加载
  console.log(data);
  
  // 短时间内再次调用,返回缓存
  setTimeout(async () => {
    const cachedData = await loader.loadData(); // 返回缓存
    console.log(cachedData);
  }, 1000);
}

测试友好性

对象封装使得单元测试更加容易,可以轻松模拟依赖和验证行为。

// 可测试的对象设计
class PaymentProcessor {
  constructor(paymentGateway) {
    this.paymentGateway = paymentGateway;
  }
  
  async processPayment(amount, cardDetails) {
    // 验证输入
    if (amount <= 0) {
      throw new Error('Invalid amount');
    }
    
    if (!this.validateCardDetails(cardDetails)) {
      throw new Error('Invalid card details');
    }
    
    // 处理支付
    try {
      const result = await this.paymentGateway.charge(amount, cardDetails);
      
      if (result.success) {
        return {
          success: true,
          transactionId: result.transactionId,
          amount: amount
        };
      } else {
        throw new Error(`Payment failed: ${result.error}`);
      }
    } catch (error) {
      console.error('Payment processing error:', error);
      throw new Error('Payment processing failed');
    }
  }
  
  validateCardDetails(cardDetails) {
    // 简单的卡号验证逻辑
    const { cardNumber, expiryDate, cvv } = cardDetails;
    return cardNumber && cardNumber.replace(/\s/g, '').length === 16 &&
           expiryDate && /^\d{2}\/\d{2}$/.test(expiryDate) &&
           cvv && /^\d{3,4}$/.test(cvv);
  }
}

// 测试用例
describe('PaymentProcessor', () => {
  it('should process valid payment', async () => {
    const mockGateway = {
      charge: jest.fn().mockResolvedValue({
        success: true,
        transactionId: 'txn_123'
      })
    };
    
    const processor = new PaymentProcessor(mockGateway);
    const result = await processor.processPayment(100, {
      cardNumber: '4111111111111111',
      expiryDate: '12/25',
      cvv: '123'
    });
    
    expect(result.success).toBe(true);
    expect(mockGateway.charge).toHaveBeenCalledWith(100, expect.any(Object));
  });
});

设计模式的应用

对象封装使得各种设计模式的应用成为可能,大大提升了代码的灵活性和可重用性。

设计模式原始数据结构对象封装优势
策略模式难以实现轻松实现算法可替换
观察者模式无法实现完整支持事件驱动架构
装饰器模式不支持完全支持动态添加功能
工厂模式有限支持完整实现对象创建抽象
// 策略模式示例
class PricingStrategy {
  calculatePrice(quantity, unitPrice) {
    throw new Error('Method not implemented');
  }
}

class RegularPricing extends PricingStrategy {
  calculatePrice(quantity, unitPrice) {
    return quantity * unitPrice;
  }
}

class BulkDiscountPricing extends PricingStrategy {
  calculatePrice(quantity, unitPrice) {
    if (quantity > 100) {
      return quantity * unitPrice * 0.9; // 10%折扣
    }
    return quantity * unitPrice;
  }
}

class SeasonalPricing extends PricingStrategy {
  calculatePrice(quantity, unitPrice) {
    const month = new Date().getMonth();
    // 假设6-8月是旺季,加价20%
    if (month >= 5 && month <= 7) {
      return quantity * unitPrice * 1.2;
    }
    return quantity * unitPrice;
  }
}

// 使用策略模式
class Product {
  constructor(name, unitPrice, pricingStrategy = new RegularPricing()) {
    this.name = name;
    this.unitPrice = unitPrice;
    this.pricingStrategy = pricingStrategy;
  }
  
  setPricingStrategy(strategy) {
    this.pricingStrategy = strategy;
  }
  
  calculateTotalPrice(quantity) {
    return this.pricingStrategy.calculatePrice(quantity, this.unitPrice);
  }
}

// 使用示例
const product = new Product('Widget', 10);
console.log('Regular price:', product.calculateTotalPrice(50)); // 500

product.setPricingStrategy(new BulkDiscountPricing());
console.log('Bulk price:', product.calculateTotalPrice(150)); // 1350

product.setPricingStrategy(new SeasonalPricing());
console.log('Seasonal price:', product.calculateTotalPrice(50)); // 旺季600,淡季500

通过以上对比分析,我们可以清晰地看到对象相比原始数据结构的巨大优势。对象提供了完整的封装、更好的抽象、更强的扩展性和更优秀的可维护性。在复杂的业务场景中,选择对象封装而不是原始数据结构,是编写高质量、可维护JavaScript代码的关键决策。

避免创建不必要的对象属性

在JavaScript对象与数据结构的封装与抽象艺术中,避免创建不必要的对象属性是编写高质量代码的关键原则之一。不必要的属性不仅会增加内存占用,还会降低代码的可读性和维护性,甚至可能引入潜在的错误和安全隐患。

为什么需要避免不必要的属性

对象属性的过度设计会带来多重负面影响:

mermaid

识别不必要的属性模式

在实际开发中,以下几种情况通常表明存在不必要的属性:

1. 冗余的属性命名
// 不良实践:属性名重复对象上下文
const user = {
  userName: "john_doe",
  userEmail: "john@example.com",
  userAge: 30,
  userAddress: "123 Main St"
};

// 良好实践:简洁的属性命名
const user = {
  name: "john_doe",
  email: "john@example.com", 
  age: 30,
  address: "123 Main St"
};
2. 临时或计算属性
// 不良实践:存储可计算的数据
const product = {
  price: 100,
  taxRate: 0.08,
  priceWithTax: 108 // 不必要的存储
};

// 良好实践:使用getter方法
const product = {
  price: 100,
  taxRate: 0.08,
  get priceWithTax() {
    return this.price * (1 + this.taxRate);
  }
};
3. 过度嵌套的结构
// 不良实践:过度嵌套导致属性冗余
const order = {
  orderDetails: {
    orderItems: {
      itemList: [
        { productName: "Book", productPrice: 20 }
      ]
    }
  }
};

// 良好实践:扁平化结构
const order = {
  items: [
    { name: "Book", price: 20 }
  ]
};

最佳实践与设计模式

使用工厂函数控制属性创建
// 使用工厂模式精确控制属性
function createUser(name, email, age) {
  // 只创建必要的属性
  return {
    name: name.trim(),
    email: email.toLowerCase(),
    age: Math.max(0, age),
    // 计算属性而不是存储
    get isAdult() {
      return this.age >= 18;
    }
  };
}

const user = createUser(" John Doe ", "JOHN@EXAMPLE.COM", 25);
console.log(user.isAdult); // true
应用享元模式减少属性
// 享元模式:共享不变的部分
const sharedUserProperties = {
  country: "USA",
  currency: "USD",
  language: "en"
};

function createUserWithSharedProps(name, email) {
  return {
    name,
    email,
    ...sharedUserProperties // 共享不变属性
  };
}
使用Proxy控制属性访问
// 使用Proxy防止意外属性创建
const user = new Proxy({ name: "John" }, {
  set(target, property, value) {
    const allowedProperties = ["name", "email", "age"];
    if (!allowedProperties.includes(property)) {
      throw new Error(`不允许创建属性: ${property}`);
    }
    target[property] = value;
    return true;
  }
});

user.name = "Jane"; // 允许
user.undefinedProp = "test"; // 抛出错误

性能优化考虑

不必要的属性会对性能产生显著影响,特别是在大规模数据处理时:

属性数量内存占用序列化时间反序列化时间
10个属性1x基准1x基准1x基准
20个属性1.8x1.5x1.6x
50个属性4.2x3.1x3.4x
// 性能对比示例
function createDataWithManyProps() {
  const data = {};
  for (let i = 0; i < 50; i++) {
    data[`prop${i}`] = i; // 创建50个属性
  }
  return data;
}

function createDataWithEssentialProps() {
  return { essentialData: "value" }; // 只包含必要属性
}

安全性与数据完整性

不必要的属性可能成为安全漏洞的来源:

// 安全风险:不必要的属性可能包含敏感数据
const user = {
  name: "John",
  email: "john@example.com",
  password: "plaintext_password", // 不应该存储
  ssn: "123-45-6789" // 不必要的敏感信息
};

// 安全实践:最小化数据原则
const user = {
  name: "John",
  email: "john@example.com",
  // 不存储敏感信息
  authenticate(password) {
    // 验证逻辑
  }
};

重构技巧与工具

ESLint规则检测

配置ESLint来检测不必要的属性:

{
  "rules": {
    "no-unused-properties": "error",
    "no-extra-parens": ["error", "all", {
      "nestedBinaryExpressions": false
    }]
  }
}
使用对象解构过滤属性
// 使用解构过滤不必要的属性
const rawData = {
  name: "John",
  email: "john@example.com",
  internalId: "123",
  timestamp: "2023-01-01",
  // ...其他元数据
};

// 只提取需要的属性
const { name, email, ...cleanData } = rawData;
// cleanData 只包含 name 和 email
模式验证与规范化
// 使用Schema验证确保属性必要性
const userSchema = {
  type: "object",
  required: ["name", "email"],
  properties: {
    name: { type: "string" },
    email: { type: "string", format: "email" },
    age: { type: "number", minimum: 0 }
  },
  additionalProperties: false // 禁止额外属性
};

实际应用场景

API响应数据处理
// 处理API响应,只保留必要属性
function sanitizeApiResponse(response) {
  const essentialFields = ["id", "name", "email", "createdAt"];
  return Object.keys(response)
    .filter(key => essentialFields.includes(key))
    .reduce((obj, key) => {
      obj[key] = response[key];
      return obj;
    }, {});
}
表单数据处理
// 表单数据清理
function cleanFormData(formData) {
  const { 
    name, 
    email, 
    // 过滤掉空值和undefined
    ...rest 
  } = formData;
  
  return Object.keys(rest).reduce((cleaned, key) => {
    if (rest[key] !== null && rest[key] !== undefined && rest[key] !== "") {
      cleaned[key] = rest[key];
    }
    return cleaned;
  }, { name, email });
}

通过遵循避免创建不必要对象属性的原则,开发者可以创建出更加健壮、高效且安全的JavaScript应用程序。这一实践不仅提升了代码质量,还为后续的维护和扩展奠定了坚实的基础。

组合优于继承的设计原则

在面向对象编程的世界中,组合与继承的争论由来已久。随着软件工程实践的不断发展,"组合优于继承"这一设计原则已经成为现代JavaScript开发中的重要指导思想。这一原则并非要完全摒弃继承,而是强调在大多数情况下,组合能够提供更灵活、更易于维护的代码结构。

理解组合与继承的本质区别

要真正理解为什么组合往往优于继承,我们首先需要明确两者的根本区别:

// 继承示例:is-a关系
class Animal {
  move() {
    console.log('Moving...');
  }
}

class Dog extends Animal {
  bark() {
    console.log('Woof!');
  }
}

// 组合示例:has-a关系
class Animal {
  move() {
    console.log('Moving...');
  }
}

class Dog {
  constructor() {
    this.animal = new Animal();
  }
  
  bark() {
    console.log('Woof!');
  }
  
  move() {
    this.animal.move();
  }
}

从上面的代码可以看出,继承建立的是"is-a"(是一个)关系,而组合建立的是"has-a"(有一个)关系。这种根本性的区别导致了它们在软件设计中的不同表现。

组合的优势分析

组合模式之所以被广泛推崇,主要基于以下几个核心优势:

1. 更低的耦合度

组合通过对象引用而不是类继承来建立关系,这使得组件之间的依赖更加松散:

// 低耦合的组合示例
class Engine {
  start() {
    return 'Engine started';
  }
}

class Wheels {
  rotate() {
    return 'Wheels rotating';
  }
}

class Car {
  constructor() {
    this.engine = new Engine();
    this.wheels = new Wheels();
  }
  
  drive() {
    return `${this.engine.start()}, ${this.wheels.rotate()}`;
  }
}
2. 更好的灵活性

组合允许在运行时动态改变行为,而继承的关系在编译时就已经确定:

class Logger {
  log(message) {
    console.log(`LOG: ${message}`);
  }
}

class ErrorLogger {
  log(message) {
    console.error(`ERROR: ${message}`);
  }
}

class Application {
  constructor(logger = new Logger()) {
    this.logger = logger;
  }
  
  setLogger(newLogger) {
    this.logger = newLogger;
  }
  
  run() {
    this.logger.log('Application started');
  }
}

// 运行时切换日志器
const app = new Application();
app.run(); // 使用默认日志器

app.setLogger(new ErrorLogger());
app.run(); // 使用错误日志器
3. 避免继承层次过深

过深的继承层次会导致所谓的"脆弱的基类问题"(Fragile Base Class Problem),基类的修改可能会意外破坏所有子类:

mermaid

实际应用场景分析

让我们通过一个具体的业务场景来对比两种方式的差异:

电商系统中的用户权限管理

使用继承的问题方案:

class User {
  constructor(name) {
    this.name = name;
  }
  
  login() {
    return `${this.name} logged in`;
  }
}

class Customer extends User {
  placeOrder() {
    return 'Order placed';
  }
}

class Admin extends User {
  manageUsers() {
    return 'Users managed';
  }
}

class Moderator extends User {
  // 需要同时具备Customer和Admin的部分功能
  // 这里就出现了问题!
}

使用组合的优化方案:

// 定义可组合的功能模块
class Loginable {
  login(user) {
    return `${user.name} logged in`;
  }
}

class OrderPlacer {
  placeOrder() {
    return 'Order placed';
  }
}

class UserManager {
  manageUsers() {
    return 'Users managed';
  }
}

class ContentModerator {
  moderateContent() {
    return 'Content moderated';
  }
}

// 组合不同的功能
class Customer {
  constructor(name) {
    this.name = name;
    this.loginable = new Loginable();
    this.orderPlacer = new OrderPlacer();
  }
  
  login() {
    return this.loginable.login(this);
  }
  
  placeOrder() {
    return this.orderPlacer.placeOrder();
  }
}

class Moderator {
  constructor(name) {
    this.name = name;
    this.loginable = new Loginable();
    this.userManager = new UserManager();
    this.contentModerator = new ContentModerator();
  }
  
  login() {
    return this.loginable.login(this);
  }
  
  manageUsers() {
    return this.userManager.manageUsers();
  }
  
  moderateContent() {
    return this.contentModerator.moderateContent();
  }
}

组合模式的实现技巧

1. 使用对象字面量进行简单组合
const canFly = {
  fly() {
    return 'Flying high!';
  }
};

const canSwim = {
  swim() {
    return 'Swimming deep!';
  }
};

const canRun = {
  run() {
    return 'Running fast!';
  }
};

class Animal {
  constructor(name) {
    this.name = name;
  }
}

// 组合能力
class Bird extends Animal {
  constructor(name) {
    super(name);
    Object.assign(this, canFly);
  }
}

class Duck extends Animal {
  constructor(name) {
    super(name);
    Object.assign(this, { ...canFly, ...canSwim });
  }
}
2. 使用函数式组合
const withLogger = (baseClass) => class extends baseClass {
  log(message) {
    console.log(`[${this.constructor.name}]: ${message}`);
  }
};

const withTiming = (baseClass) => class extends baseClass {
  timedOperation(operationName, operation) {
    const start = Date.now();
    const result = operation();
    const end = Date.now();
    this.log(`${operationName} took ${end - start}ms`);
    return result;
  }
};

class BaseService {
  fetchData() {
    return 'Data fetched';
  }
}

// 组合装饰器
const EnhancedService = withTiming(withLogger(BaseService));
const service = new EnhancedService();
service.timedOperation('Data fetch', () => service.fetchData());

何时使用继承

虽然组合在很多情况下更优,但继承仍然有其适用的场景:

场景类型适合继承适合组合
关系类型is-a关系has-a关系
代码复用白盒复用黑盒复用
灵活性编译时确定运行时可变
耦合度高耦合低耦合
维护性相对困难相对容易

适合使用继承的场景:

  • 真正的"是一个"关系(如:Dog is an Animal)
  • 需要多态行为
  • 希望基类的改变影响所有子类
  • 框架或库的设计中需要提供扩展点

最佳实践建议

  1. 优先考虑组合:在大多数业务场景中,组合都能提供更好的解决方案
  2. 小规模使用继承:只在真正需要is-a关系时使用继承,并保持继承层次扁平
  3. 使用接口定义契约:通过接口或抽象类定义行为契约,具体实现使用组合
  4. 遵循单一职责原则:每个类应该只有一个改变的理由
  5. 保持组件可测试:组合的组件更容易进行单元测试
// 良好的组合实践
class PaymentProcessor {
  constructor(paymentMethod) {
    this.paymentMethod = paymentMethod;
  }
  
  processPayment(amount) {
    return this.paymentMethod.process(amount);
  }
}

class CreditCardPayment {
  process(amount) {
    return `Processed $${amount} via Credit Card`;
  }
}

class PayPalPayment {
  process(amount) {
    return `Processed $${amount} via PayPal`;
  }
}

// 使用
const creditCardProcessor = new PaymentProcessor(new CreditCardPayment());
const paypalProcessor = new PaymentProcessor(new PayPalPayment());

通过遵循"组合优于继承"的原则,我们能够构建出更加灵活、可维护和可测试的JavaScript应用程序。这种设计思维方式不仅适用于传统的面向对象编程,也同样适用于现代的函数式编程和组件化开发模式。

总结

通过本文的全面分析,我们可以看到JavaScript对象与数据结构的封装与抽象是一门需要精心掌握的艺术。getter和setter提供了精细的数据访问控制,对象相比原始数据结构具有明显的优势,避免不必要的属性创建能提升代码质量和性能,而组合优于继承的原则则为构建灵活可维护的系统提供了指导。这些设计原则和实践方法的合理运用,将帮助开发者创建出更加健壮、高效且易于维护的JavaScript应用程序。

【免费下载链接】clean-code-javascript :bathtub: Clean Code concepts adapted for JavaScript 【免费下载链接】clean-code-javascript 项目地址: https://gitcode.com/GitHub_Trending/cl/clean-code-javascript

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值