AI 生成的摘要
文章详细解读了依赖注入(DI)和依赖倒置原则(DIP)在软件设计中的应用。DI的核心思想是将依赖通过外部传入而不是在类内部创建,这样可以减少耦合并提高测试性和灵活性。通过实际例子展示了在不使用和使用依赖注入情况下的代码差异,强调了使用DI后类的可替换性和测试性增强。 文章还深入阐述了DIP,即高层模块不应该依赖低层模块,二者都应依赖抽象接口。这种设计使高层模块与具体实现无关,从而提高系统的扩展性和可维护性。具体实例解释了在创建一个UI渲染引擎时,如何通过DI和DIP实现逻辑与具体实现分离,增强系统的解耦性和灵活组合能力。总结来说,这种设计模式能够构建出高度可扩展和可复用的架构,适用于多种运行环境。

依赖注入是一种设计模式,核心思想是:不要在类内部创建依赖,而是通过外部传入,这样可以减少耦合、提高测试性和灵活性。

不使用依赖注入的例子

ts
class EmailService {
  send(email: string, message: string) {
    console.log(`发送邮件到 ${email}: ${message}`);
  }
}

class UserController {
  private emailService = new EmailService(); // 直接创建,耦合很强
  
  notifyUser(email: string) {
    this.emailService.send(email, "欢迎加入!");
  }
}

使用依赖注入的例子

ts
class EmailService {
  send(email: string, message: string) {
    console.log(`发送邮件到 ${email}: ${message}`);
  }
}

class UserController {
  constructor(private emailService: EmailService) {}

  notifyUser(email: string) {
    this.emailService.send(email, "欢迎加入!");
  }
}

// 使用方式
const emailService = new EmailService();
const userController = new UserController(emailService);
  • UserController 不再依赖于具体实现,易于替换或测试。
  • 更符合“单一职责”和“模块化”的思路。

依赖倒置原则 > 高层模块(业务逻辑)不应该依赖低层模块(工具类),二者都应该依赖于抽象抽象不应该依赖细节细节应该依赖抽象

ts
// 抽象接口
interface MessageService {
  send(to: string, message: string): void;
}

// 低层模块实现
class EmailService implements MessageService {
  send(to: string, message: string): void {
    console.log(`发送邮件给 ${to}: ${message}`);
  }
}

// 高层模块
class UserController {
  constructor(private messageService: MessageService) {}

  notifyUser(email: string) {
    this.messageService.send(email, "欢迎!");
  }
}

// 使用
const service = new EmailService();
const controller = new UserController(service);
  • 高层模块(UserController)只依赖接口(MessageService),与具体实现(EmailService)无关。
  • 更容易扩展,比如换成短信服务 SMSService,无需改动 UserController。

实例

renderer.ts
ts
export interface RendererOptions<HostNode = RendererNode> {
  setElementText(node: HostNode, text: string): void
}

export interface RendererNode {
  [key: string]: any
}

export interface RendererElement extends RendererNode {}

export type RootRenderFunction<HostElement = RendererElement> = (
  message: string,
  container: HostElement,
) => void

export function createRenderer(options: RendererOptions) {
  const { setElementText: hostSetElementText } = options

  const render: RootRenderFunction = (message, container) => {
    hostSetElementText(container, message)
  }

  return { render }
}

createRenderer 接收了setElementText 并以setElementText 实现并返回了 render(这里render 是操作DOM相关的操作), render 接受参数并调用 传入进来的setElementText ,这样实际的操作就与creatRenderer无关。

createAppAPI 接收了 render,并在返回的对象的方法中使用了render,这样顺序就是 app(createApp) -> render(createRenderer) -> createRenderer 的参数

GPT解析实例

runtime-dom/index.ts     ↓ createRenderer(nodeOps)     ↓   render 函数(通过注入 nodeOps 的 setElementText 实现)     ↓ createAppAPI(render)     ↓ createApp().mount(selector)     ↓ rootComponent.render() → render(message, container)

DI(依赖注入)

createRenderer(options) 不自己创建 DOM 操作函数,而是通过参数注入 setElementText。也就是说:渲染逻辑依赖于外部注入的底层实现(DOM 还是 Canvas,甚至是 SSR 都无所谓,只要实现同样的接口就行)。

ts
createRenderer(nodeOps)

DIP(依赖倒置原则)

这体现在以下几点:

  • createRenderer 并不依赖于 DOM 或具体的实现细节,而是依赖于抽象接口 RendererOptions。
  • createAppAPI 依赖的也是抽象的 render 函数,不关心具体渲染方式。
  • 高层模块(如 createAppAPI)依赖的是抽象接口,而不是底层细节。 *

🔧 也就是说:

高层模块 createAppAPI 和底层实现 nodeOps.setElementText 都依赖于 RendererOptions 这个抽象。

这就是典型的 依赖倒置

✅ 这种设计的好处

  1. 解耦:逻辑与具体实现完全分离,可以轻松替换渲染逻辑(例如服务端渲染/Canvas 渲染/WebGL 渲染)。
  2. 扩展性强:想换底层实现,只要实现同样接口就行。
  3. 测试友好:可以 mock RendererOptions 来测试 createRenderer 的行为。
  4. 灵活组合:上层代码无需修改即可适配多种运行环境(DOM、Node.js、Native 等)。

✅ 如果要总结一句话:

这套设计用 createRenderer 做了渲染层的抽象,并通过依赖注入(DI)和依赖倒置原则(DIP)将 DOM 操作从业务逻辑中解耦出来,从而构建出一个高度可扩展和可复用的 UI 渲染引擎。

Q & A

Q: 依赖倒置 就是尽可能把依赖抽离到更下一层? A: 依赖倒置不是“放到底层”,而是“依赖抽象,而不是依赖具体实现”

传统的设计是这样的: 高层模块(业务逻辑)       ↓ 依赖 低层模块(数据库/文件系统/DOM/网络请求等) 高层模块直接依赖底层模块,一旦底层改变,高层也要改,耦合强,灵活性差

而“依赖倒置”后: 高层模块 → 依赖接口/抽象 ← 底层模块实现接口

🔁 这就是“倒置”:原本是高层依赖低层,现在是两者都依赖抽象。

来源: link-faviconhttps://book.chibivue.land/10-minimum-example/015-package-architecture.html#di-and-dip