JS `Composition over Inheritance`:通过组合函数和对象来构建复杂功能

各位靓仔靓女,大家好!今天咱们不聊风花雪月,也不谈人生理想,就来唠唠嗑,关于JavaScript里一个既重要又有点让人头大的话题:组合优于继承(Composition over Inheritance)。

别看这名字听起来高大上,其实它说白了就是告诉你,别老想着靠“血缘关系”(继承)来解决问题,多想想怎么把不同的“零件”(函数或对象)拼装起来,搭积木一样。

一、继承的诱惑与陷阱

话说回来,继承这玩意儿,一开始确实挺吸引人的。 想象一下,你有一个Animal(动物)类,它有eat()(吃)和sleep()(睡)方法。然后你想创建一个Dog(狗)类,狗也是动物啊,那直接继承Animal,省事!

class Animal {
  constructor(name) {
    this.name = name;
  }
  eat() {
    console.log(`${this.name} is eating.`);
  }
  sleep() {
    console.log(`${this.name} is sleeping.`);
  }
}

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

const dog = new Dog('Buddy');
dog.eat(); // Buddy is eating.
dog.bark(); // Woof!

看起来很完美,对不对? 代码简洁,逻辑清晰。 但问题来了,如果有一天,你想创建一个Bird(鸟)类,它也需要eat()sleep()方法,你又想让鸟会飞fly()

你可能会想: “那简单,再继承一次Animal就好了!”

class Bird extends Animal {
  fly() {
    console.log(`${this.name} is flying.`);
  }
}

const bird = new Bird('Tweety');
bird.eat(); // Tweety is eating.
bird.fly(); // Tweety is flying.

到目前为止,一切还算顺利。 但是,如果有一天,你突然想创建一个FlyingDog(会飞的狗)类呢? 狗会叫,也会飞,这可咋办?

  • 多重继承的痛苦: 某些语言支持多重继承,你可以让FlyingDog同时继承DogFlying类。 但JavaScript不支持多重继承(虽然可以通过一些方式模拟,但会增加复杂度)。
  • 继承层次的混乱: 如果你强行使用继承,可能会导致继承链越来越深,代码越来越复杂,难以维护。 想象一下,FlyingDog继承DogDog继承Animal,然后Animal又继承LivingThing… 这棵继承树会变得异常臃肿。
  • 代码复用的局限性: 继承是一种“强耦合”的关系。子类必须完全依赖于父类的实现。如果父类的实现发生改变,可能会影响到所有的子类。

二、组合的魅力

组合的核心思想是: 将对象的功能分解成更小的、独立的单元,然后通过组合这些单元来构建更复杂的功能。 就像搭积木一样,你可以选择不同的积木块,按照不同的方式组装,从而创造出不同的形状。

回到刚才的例子,我们可以把fly()方法提取出来,作为一个独立的函数(或者对象),然后让BirdFlyingDog都“拥有”这个功能。

// 定义一个飞行行为
const canFly = (state) => ({
  fly: () => console.log(`${state.name} is flying.`)
});

const canEat = (state) => ({
  eat: () => console.log(`${state.name} is eating.`)
});

const canSleep = (state) => ({
  sleep: () => console.log(`${state.name} is sleeping.`)
});

const canBark = (state) => ({
  bark: () => console.log('Woof!')
});

// 创建Bird类
const createBird = (name) => {
  let state = {
    name: name
  };
  return Object.assign({},
    canEat(state),
    canSleep(state),
    canFly(state)
  );
};

// 创建Dog类
const createDog = (name) => {
  let state = {
    name: name
  };
  return Object.assign({},
    canEat(state),
    canSleep(state),
    canBark(state)
  );
};

// 创建FlyingDog类
const createFlyingDog = (name) => {
  let state = {
    name: name
  };
  return Object.assign({},
    canEat(state),
    canSleep(state),
    canBark(state),
    canFly(state)
  );
};

const bird = createBird('Tweety');
bird.eat(); // Tweety is eating.
bird.fly(); // Tweety is flying.

const dog = createDog('Buddy');
dog.eat(); // Buddy is eating.
dog.bark(); // Woof!

const flyingDog = createFlyingDog('AirBuddy');
flyingDog.eat(); // AirBuddy is eating.
flyingDog.bark(); // Woof!
flyingDog.fly(); // AirBuddy is flying.

在这个例子中,我们使用Object.assign()方法将不同的功能“混合”到对象中。 canFlycanEatcanSleepcanBark 这些都是可复用的行为模块。 FlyingDog 可以同时拥有 canBarkcanFly 的能力,而不需要复杂的继承关系。

组合的优势:

  • 灵活性: 可以根据需要选择不同的功能模块进行组合,创建出各种不同的对象。
  • 可复用性: 功能模块可以被多个对象复用,避免代码重复。
  • 解耦性: 对象之间的耦合度较低,修改一个功能模块不会影响到其他对象。
  • 可测试性: 每个功能模块都是独立的,可以单独进行测试。

三、组合的几种常见姿势

组合的方式有很多种,以下是一些常见的姿势:

  1. 函数组合(Function Composition)

    函数组合是指将多个函数组合成一个函数,然后依次执行这些函数。 就像流水线一样,每个函数负责处理一个环节。

    const add = (x) => x + 2;
    const multiply = (x) => x * 3;
    
    // 组合函数
    const compose = (f, g) => (x) => f(g(x));
    
    // 先乘以3,再加2
    const addThenMultiply = compose(multiply, add);
    
    console.log(addThenMultiply(5)); // (5 + 2) * 3 = 21

    在这个例子中,compose函数接受两个函数fg作为参数,返回一个新的函数,该函数先执行g函数,然后将结果传递给f函数。

    你可以使用 Lodash 或 Ramda 等库提供的 composepipe 函数来进行更简洁的函数组合。 例如:

    import { compose } from 'lodash';
    
    const toUpperCase = (str) => str.toUpperCase();
    const exclaim = (str) => str + '!';
    
    const excited = compose(exclaim, toUpperCase);
    
    console.log(excited('hello')); // HELLO!
  2. 混入(Mixins)

    混入是指将一个对象的功能“混入”到另一个对象中。 这可以通过Object.assign()...扩展运算符来实现。 上面创建 BirdDogFlyingDog 的例子就使用了混入。

    const barker = {
      bark: function() {
        console.log('Woof!');
      }
    };
    
    const driver = {
      drive: function() {
        console.log('Vroom!');
      }
    };
    
    const robotDog = Object.assign({}, barker, driver);
    
    robotDog.bark(); // Woof!
    robotDog.drive(); // Vroom!
  3. 策略模式(Strategy Pattern)

    策略模式是指定义一系列的算法,并将每个算法封装成一个独立的类,然后让客户端可以选择使用哪个算法。 这可以提高代码的灵活性和可扩展性。

    // 定义策略接口
    class PaymentStrategy {
      pay(amount) {
        throw new Error('pay() method must be implemented.');
      }
    }
    
    // 实现信用卡支付策略
    class CreditCardPayment extends PaymentStrategy {
      constructor(cardNumber, expiryDate, cvv) {
        super();
        this.cardNumber = cardNumber;
        this.expiryDate = expiryDate;
        this.cvv = cvv;
      }
    
      pay(amount) {
        console.log(`Paying ${amount} using Credit Card: ${this.cardNumber}`);
      }
    }
    
    // 实现 PayPal 支付策略
    class PayPalPayment extends PaymentStrategy {
      constructor(email) {
        super();
        this.email = email;
      }
    
      pay(amount) {
        console.log(`Paying ${amount} using PayPal: ${this.email}`);
      }
    }
    
    // 上下文对象
    class ShoppingCart {
      constructor(paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
      }
    
      setPaymentStrategy(paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
      }
    
      checkout(amount) {
        this.paymentStrategy.pay(amount);
      }
    }
    
    // 使用示例
    const cart = new ShoppingCart(new CreditCardPayment('1234-5678-9012-3456', '12/24', '123'));
    cart.checkout(100); // Paying 100 using Credit Card: 1234-5678-9012-3456
    
    cart.setPaymentStrategy(new PayPalPayment('[email protected]'));
    cart.checkout(50); // Paying 50 using PayPal: [email protected]

    在这个例子中,PaymentStrategy是一个抽象类,定义了支付接口。 CreditCardPaymentPayPalPayment是具体的策略类,分别实现了信用卡支付和PayPal支付。 ShoppingCart是一个上下文对象,它持有一个PaymentStrategy对象,并根据客户端的选择来执行不同的支付策略。

  4. 高阶组件(Higher-Order Components – React)

    在高阶组件(HOC)中,一个组件接受另一个组件作为参数,并返回一个新的组件。 这是一种常见的在 React 中复用组件逻辑的方式。

    import React from 'react';
    
    // 高阶组件:增强组件的功能
    const withLoading = (WrappedComponent) => {
      return class WithLoading extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            isLoading: true
          };
        }
    
        componentDidMount() {
          // 模拟数据加载
          setTimeout(() => {
            this.setState({ isLoading: false });
          }, 1500);
        }
    
        render() {
          if (this.state.isLoading) {
            return <div>Loading...</div>;
          }
          return <WrappedComponent {...this.props} />;
        }
      };
    };
    
    // 原始组件
    const MyComponent = (props) => {
      return <div>Hello, {props.name}!</div>;
    };
    
    // 使用高阶组件增强后的组件
    const EnhancedComponent = withLoading(MyComponent);
    
    // 渲染
    ReactDOM.render(<EnhancedComponent name="World" />, document.getElementById('root'));

    在这个例子中,withLoading是一个高阶组件,它接受一个组件WrappedComponent作为参数,并返回一个新的组件WithLoadingWithLoading组件在渲染WrappedComponent之前,会显示一个加载指示器,直到数据加载完成。

  5. Hooks (React)

    React Hooks 允许你在不编写 class 的情况下使用 state 以及其他的 React 特性。 自定义 Hooks 是一种组合逻辑的有效方式。

    import { useState, useEffect } from 'react';
    
    // 自定义 Hook:获取窗口大小
    function useWindowSize() {
      const [windowSize, setWindowSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    
      useEffect(() => {
        function handleResize() {
          setWindowSize({
            width: window.innerWidth,
            height: window.innerHeight,
          });
        }
    
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
      }, []); // Empty array ensures that effect is only run on mount
    
      return windowSize;
    }
    
    function MyComponent() {
      const windowSize = useWindowSize();
    
      return (
        <div>
          Window size: {windowSize.width} x {windowSize.height}
        </div>
      );
    }

    在这个例子中,useWindowSize 是一个自定义 Hook,它返回当前窗口的大小。 MyComponent 使用 useWindowSize Hook 来获取窗口大小,并在屏幕上显示。

四、 总结:选择合适的姿势

组合优于继承并不是说继承完全没有用武之地。 在某些情况下,继承仍然是一种有效的解决方案。 关键在于选择合适的工具来解决问题。

特性 继承 组合
代码复用 通过继承父类的属性和方法 通过组合独立的函数和对象
灵活性 较低,子类必须依赖于父类的实现 较高,可以根据需要选择不同的功能模块进行组合
耦合度 较高,子类和父类之间存在强耦合关系 较低,对象之间的耦合度较低
可维护性 随着继承层次的加深,代码会变得越来越复杂,难以维护 功能模块独立,易于维护和测试
适用场景 当存在明显的“is-a”关系,且继承层次结构稳定时 当需要灵活地组合不同的功能,且避免继承带来的复杂性时
典型应用 创建用户界面组件库,其中某些组件具有共同的基类 创建游戏引擎,其中不同的游戏对象可以组合不同的行为

什么时候选择组合?

  • 当你需要灵活地组合不同的功能时。
  • 当你希望避免继承带来的复杂性时。
  • 当你需要提高代码的可复用性和可维护性时。

什么时候选择继承?

  • 当存在明显的“is-a”关系时。
  • 当继承层次结构稳定时。
  • 当你需要利用继承来实现多态性时。

总而言之,组合优于继承是一种重要的设计思想,它可以帮助你编写更加灵活、可复用、可维护的代码。 在实际开发中,你应该根据具体情况选择合适的组合方式。

希望今天的唠嗑对大家有所帮助! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注