blog icon indicating copy to clipboard operation
blog copied to clipboard

控制反转和依赖注入

Open wang2lang opened this issue 2 years ago • 0 comments

核心概念

抽象

认知方式对比 人类靠抽象来理解世界,积累知识。但认知的形式不止这一种,计算机可以通过运算来认知世界。人工智能所依赖的机器学习的本质,是拟合。

image

在上图中,通过点拟合出线,那么输入任何 X 都能预测到 Y。

机器学习就是找出过往数据的内在规律,并对未来新数据进行预测的过程。从生物神经网络原理中得到的灵感,用网状结构逐步调整各神经元权重的方法来拟合,从而找到最佳拟合函数。

image image

人类认知世界靠抽象。缩减一个概念或现象的信息量来将其广义化(Generalization)的过程被称为抽象化。例如:

  • 将皮制足球抽象化成一个球,只保留球的属性和行为等信息;
  • 将快乐抽象化成一种情绪,以减少其在情绪中所含的信息量;

image

我们区别于动植物最底层的缘由是我们的大脑具备抽象的能力:

  • 抽象是人类认知的基础;
  • 抽象是人类思维和语言的基础,语言词汇就是人类科学、哲学发展的印证;
  • 抽象是计算机模拟世界运转的基础;

image

// 如果被强光晒,植物会蔫(sleepy);
class Energy {
  mode: string;
  constructor(mode) {
    this.mode = mode;
  }
 
  scorch() {
    console.log(`${this.mode} is sorching.`);
    return true;
  }
}
 
class Plant {
  category: string;
  constructor(category) {
    this.category = category;
  }
 
  sleepy() {
    console.log(`${this.category} is sleepy.`);
  }
}
 
class Frame {
  energy: Energy;
  plant: Plant;
 
  constructor(energy: Energy, plant: Plant) {
    this.energy = energy;
    this.plant = plant;
  }
 
  run() {
    if (this.energy.scorch()) {
      this.plant.sleepy();
    }
  }
}
 
function main() {
  const frame = new Frame(new Energy('sun'), new Plant('grass'));
  frame.run();
}

计算机模拟世界运转要点:

  • 状态的产生和记录;
  • 状态的变更;
  • 通信同步状态(互联网);

在现实生活中我们所说的数字化(数字孪生)和信息化(互联),就是在尽可能的用计算机来动态模拟世界的运转。

比如:人事系统里职工的状态以及状态的变更,同现实中职工在公司里的实际状态和状态变更是保持同步的。现实中发生了离职行为的话,在计算机中可以找到对应的程序方法将员工的状态变更为离职。

封装

封装的更高级和高阶的解释是:将与数据相关的变量、属性与行为方法绑定在一个类或一块代码单元中。

封装是一种实现限制直接访问某些数据结构元素(字段、属性、方法等)的方式。

The more advanced and high-level explanations of Encapsulation is a concept of bunding data related variables and properties with behavioral methods in one class or code unit.

Encapsulation is an approach for restricting direct access to some of the data structure elements (fields, properties, methods,

在 JavaScript 中,实现封装的方式主要有:【类】和【闭包】,下面分别用类和闭包来实现 Car 相关的数据和方法的封装:

  1. Class 实现封装
const TURN = {
  OFF: false,
  ON: true
}
 
class Car {
  constructor(model) {
      this.model = model;
      this.engine = TURN.OFF;
      this.speed = 0;
  }
 
  async drive(speed = 10) {
      this.engine = TURN.ON;
      await this.setSpeed(speed);
      console.log(`Ridding with speed: ${this.speed}`);
      return this.speed;
  }
 
  async stop() {
      await this.setSpeed(0);
      this.engine = TURN.OFF;
      return this.speed;
  }
 
  async setSpeed(speed) {
      return new Promise((resolve, reject) => {
          if (this.engine === TURN.OFF) {
              reject('Turn on engine!');
          } else {
              const interval = setInterval(async () => {
                  console.log('current speed:', this.speed);
                  if (this.speed < speed) {
                      this.speed++;
                  } else if (this.speed > speed) {
                      this.speed--;
                  } else {
                      resolve(this.speed);
                      clearInterval(interval);
                  }
              }, 100);
          }
      });
  }
 
  toString() {
      return `Car: ${this.model}; Engine turned on: ${this.engine}; ` + (this.engine ? `Current speed: ${this.speed}` : '');
  }
}
 
const tesla = new Car('TESLA');
  1. Closure 实现封装
function createCar(model) {
  const TURN = {
    OFF: false,
    ON: true
  }
 
  let engine = TURN.OFF;
  let speed = 0;
 
  function setSpeed(speed) {
    return new Promise((resolve, reject) => {
      if (engine === TURN.OFF) {
        reject('Turn on engine!');
      } else {
        const interval = setInterval(async () => {
          console.log('current speed:', this.speed);
          if (this.speed < speed) {
            this.speed++;
          } else if (this.speed > speed) {
            this.speed--;
          } else {
            resolve(this.speed);
            clearInterval(interval);
          }
        }, 100);
      }
    });
  }
 
  async function drive(speed) {
    engine = TURN.ON;
    await setSpeed(speed);
    console.log(`Ridding with speed: ${this.speed}`);
    return speed;
  }
 
  async function stop() {
    await setSpeed(0);
    engine = TURN.OFF;
    return speed;
  }
 
  function toString() {
    return `Car: ${model}; Engine turned on: ${engine}; ` + (engine ? `Current speed: ${speed}` : '');
  }
 
  return {
    setSpeed,
    drive,
    stop,
    toString
  }
}
 
const tesla = createCar('TESLA');

Goland 通过 struct 实现接口里的方法来实现封装:

package main
 
import (
    "errors"
    "fmt"
)
 
type TurnType bool
 
const (
    ON  TurnType = true
    OFF TurnType = false
)
 
type Car struct {
    model  string
    engine TurnType
    speed  int
}
 
func NewCar(model string) *Car {
    return &Car{
        model:  model,
        engine: OFF,
        speed:  0,
    }
}
 
func (c *Car) SetSpeed(speed int) (int, error) {
    if c.engine == OFF {
        return 0, errors.New("Turn on engine!")
    }
 
    for c.speed != speed {
        fmt.Println("current speed:", c.speed)
        if c.speed < speed {
            c.speed = c.speed + 1
        } else {
            c.speed = c.speed - 1
        }
    }
    return speed, nil
}
 
func (c *Car) Drive(speed int) {
    c.engine = ON
    c.SetSpeed(speed)
    fmt.Println("Ridding with speed:", speed)
}
 
func (c *Car) Stop() {
    c.SetSpeed(0)
    c.engine = OFF
}
 
func (c *Car) ToString() string {
    return fmt.Sprintf("Car: %s; Engine turned on: %t; Current speed: %b", c.model, c.engine, c.speed)
}
 
func main() {
    c := NewCar("TESLA")
    c.Drive(100)
    fmt.Println(c.ToString())
}

多态

我们抽象出事物的共性,尝试用计算机来模拟。抽象的结果就是一系列的描述,这些描述分为两部分:属性和方法。

例如:二维形状是日常生活中很重要的概念,窗户是方形的,轮胎是圆形的,你的房子占地是多边形,买房子很关心面积。 我们抽象出 shape 的定义,也就是代码中的 interface,属性有长、宽、半径等,方法有计算面积。

多态的意思是,同一个的 interface 的属性或者方法可以有不同的实现。例如圆形、矩形和三角形的属性描述和计算面积的实现都是不同的。

image

多态在生活中非常常见,比如方向盘,无论材质是合金、塑料还是碳纤维,无论是机是电动助力、液压助力还是纯手动机械(卡丁车)转动,只要实现了方向盘,你会用一种,就会使用所有类型的方向盘。

制造业的模块化,IEEE 这种组织定义工业化的标准,都是在描述各种产品的 interface shape,然后不同工厂、不同工艺按照这种标准去实现。

image

编程也是如此,对于同一个 interface 的所有实现,它们应该具有相同的方法,甚至属性列表,只是这些方法的具体实现细节不同。

在这样的情况下,这些实现在运行时的行为是确定的,我们就可以在相同运行环境中动态绑定任何想要的实现,从而实现代码的简化。

class Shape {
  area() {
      return 0;
  }
  toString() {
      return Object.getPrototypeOf(this).constructor.name;
  }
}
 
class Circle extends Shape {
  constructor(r) {
      super();
      this.radius = r;
  }
 
  area() {
      return Math.PI * this.radius ** 2;
  }
}
 
class Rectangle extends Shape {
  constructor(w, h) {
      super();
      this.width = w;
      this.height = h;
  }
 
  area() {
      return this.width * this.height;
  }
}
 
class Triangle extends Shape {
  constructor(b, h) {
      super();
      this.base = b;
      this.height = h;
  }
 
  area() {
      return this.base * this.height / 2;
  }
}
 
function cumulateShapes(shapes) {
  return shapes.reduce((sum, shape) => {
      if (shape instanceof Shape) {
          console.log(`Shape: ${shape.toString()} - area: ${shape.area()}`);
          return sum + shape.area()
      }
      throw Error('Bad argument shape.');
  }, 0);
}
 
function main() {
  // 任意动态传入所需要的实例
  const shapes = [new Circle(3), new Rectangle(2, 3), new Triangle(3, 4), new Circle(2)];
  console.log(cumulateShapes(shapes)); 
}
 
main();

控制反转

如何写出清晰可读,扩展性高、可维护性强的代码?如何组织代码可以实现这样的目标呢?

独立模块

我们来看一段代码:

class Car {
  constructor(model) {
    this._model = model;
    this.engine = new Engine();
  }
}
 
class Engine {
  constructor() {
    this._currentPower = 0;
    this._maxPower = 100;
  }
 
  setPower(val) {
    if (val >= 0 && val < this._maxPower) {
      this._currentPower = val;
    } else {
      throw new Error(`Incorrect power value: ${val}. Should be between 0 and ${this._maxPower}`);
    }
  }
}
 
const car = new Car('Tesla');

上面代码中,Car 依赖 Engine,这里的问题在于,一旦 Engine 代码发生变更,例如需要初始化添加一个属性 ,那 Car 就必须要跟着改。

class Car {
  constructor(model, engineName) {
    this._model = model;
    this.engine = new Engine(engineName);
  }
}
 
class Engine {
  constructor(name) {
    this.name = name;
    this._currentPower = 0;
    this._maxPower = 100;
  }
 
  setPower(val) {
    if (val >= 0 && val < this._maxPower) {
      this._currentPower = val;
    } else {
      throw new Error(`Incorrect power value: ${val}. Should be between 0 and ${this._maxPower}`);
    }
  }
}
 
const car = new Car('Tesla', 'DongFeng');

这说明 Car 与 Engine 有耦合,改动一个 class 会影响到所有依赖此 class 的代码。为了避免此类情况,可以使用依赖反转。 当前的情况是:

  1. Car 依赖 Engine,Engine 是被依赖方;
  2. 如果 Engine 改变,那依赖 Engine 的 Car 也要改;

控制反转的意思,Car 从依赖方反转变成提需求方,掌握控制的主动权,具体表现为:

  1. Car 实例化时需要一个 Engine 实例;
  2. Car 不关心 Engine 的具体实现细节,但 Engine 的实现必须满足 Car 所要求 Engine 抽象;

我们改一下 Car 的代码:

interface EngineInterface {
  setPower: (val: number) => void;
}
 
class Car {
  model: string;
  engine: EngineInterface;
  constructor(model, engine) {
    this.model = model;
    this.engine = null;
  }
 
  setEngine(val: EngineInterface) {
    this.engine = val;
  }
}
 
const car = new Car('Tesla', new Engine('DongFeng'));

这样就实现了最简单的依赖倒置,这样 Engine 代码更新的时候,Car 不需要跟着更新,做到了解耦。

Car 需要定义好自己需要的 interface,在实例化的过程中传入符合多态的各种依赖实例都可以正常运行。

image

基础定义

依赖(Dependency):在程序中被使用的对象或任何编程单元,例如上文中的 Engine; 依赖倒置原则(Dependency Inversion Principle): SOLID 原则之一,通过共享抽象来解耦高层和底层的关系,DIP 满足以下要求:

  1. 高层模块不依赖于低层模块的实现,他们都应该依赖于抽象;
  2. 抽象不依赖于具体实现,但具体实现应该依赖于抽象;

控制反转(IoC):一种满足 DIP 的抽象编程原则,相比于主动控制去直接获取对象,IoC 的控制流是反转的,由框架(容器)来帮忙创建和注入依赖对象;

不使用依赖倒置的系统构架,控制流和依赖关系流是一个方向的,由高层指向低层,也就是高层依赖低层,最明显的特点就是高层包需要 import 很多低层包里的类,这样的话任何的低层小改动,都可能产生影响高层业务逻辑类的改动的蝴蝶效应;

这样的系统耦合严重,维护,模拟,测试,实验和拓展都很困难,可能动不动就要重构,让程序员苦不堪言。而使用依赖倒置,使得低层包依赖高层包。高层包里不会import 任何一个低层类。只要 interface 设计的好,低层的任何变动,都不会影响高层一行 code;

IoC 容器里的代码执行控制流由框架的特定实现来管理,class 或者 function 的调用不再由我们的代码来控制,而是由框架来控制和调用。

React、Vue 和 Angular 都是 IoC framework,都遵循 IoC设计:

  1. 所有的要被渲染的组件都遵循统一抽象,既对应框架的组件 API;
  2. 设好 router 配置,根据理由路由规则,每个 path 都能找到对应的组件(或 redirect 后的组件);
  3. 框架会监听浏览器 popstate (hashchange) 事件,找到 path 对应的组件实例化运行后进行渲染;

IoC 的核心要点:

  1. 基于抽象把 class (代码单元)写好;
  2. 按照框架(React、Spring)的配置文件或注解来描述class(代码单元)之间的关系;
  3. 框架(IoC容器)按照 2 中的配置去控制对象的实例化和运行;

依赖注入

依赖注入是 IoC 的具体实现方式,我们以 inversify 为例了解依赖注入原理。

定义接口

// 定义服务对象标识
export const Warrior = Symbol.for('Warrior');
export const Weapon = Symbol.for('Weapon');
export const ThrowableWeapon = Symbol.for('ThrowableWeapon');
 
export interface Warrior {
    fight(): string;
    sneak(): string;
}
 
export interface Weapon {
    hit(): string;
}
 
export interface ThrowableWeapon {
    throw(): string;
}

定义依赖

import { injectable, inject } from 'inversify';
import 'reflect-metadata';
import { Weapon, ThrowableWeapon, Warrior } from './interfaces';
 
@injectable()
export class Katana implements Weapon {
    public hit() {
        return "cut!";
    }
}
 
@injectable()
export class Shuriken implements ThrowableWeapon {
    public throw() {
        return "hit!";
    }
}
 
@injectable()
export class Ninja implements Warrior {
 
    public constructor(
        @inject(Weapon) protected katana: Weapon,
        @inject(ThrowableWeapon) protected shuriken: ThrowableWeapon
    ) {}
 
    public fight() { return this.katana.hit(); }
    public sneak() { return this.shuriken.throw(); }
 
}

创建并配置 IoC 容器

import { Container } from "inversify";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";
 
const myContainer = new Container();
myContainer.bind<Warrior>(Warrior).to(Ninja);
myContainer.bind<Weapon>(Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>ThrowableWeapon).to(Shuriken);
 
export { myContainer };

依赖解析

import { myContainer } from "./inversify.config";
import { Warrior } from "./interfaces";
 
const ninja = myContainer.get<Warrior>(Warrior);
 
expect(ninja.fight()).eql("cut!"); // true
expect(ninja.sneak()).eql("hit!"); // true

wang2lang avatar Jul 15 '22 03:07 wang2lang