前言
目前我们在实际的业务中存在一个问题:多个端中业务逻辑实际上是一样的,但是交互形式确实不一样的,如PC端和手机端的交互有的时候会相差很大;所以有没有一种方式能把业务逻辑和交互分离开呢?如果能够拆分应该怎么拆分呢?以下的内容就是我的一些思考:
什么是依赖注入
首先我们先了解下什么是依赖注入(DI)?以现实中的一件小事为例子: 张三开车回老家;程序伪代码如下:
class GoHome {
private person = new Person();
private car = new Car();
constructor() {
this.person.setName('张三');
}
go() {
this.person.drive(this.car);
}
}
class Person {
private name;
setName(name: string) {
this.name = name;
}
drive(car: Car, address: string) {
car.startup(address)
}
}
class Car {
startup(address: string) {
// 启动车到达目的地
}
}
这段代码存在什么问题呢?我们在GoHome这个具体的业务逻辑中耦合了Person、Car两个类,这样会导致一些问题:
- 这个业务你没有办法把他交给三个人同时去开发(实际业务可能比例子复杂太多);
- 维护的时候维护者每次维护的时候都有必要阅读业务中的所有代码,否则如果Car类中存在一个bug,维护者不小心删除了startup函数, 或者删除了startup函数中的参数后,会立马导致整个程序瘫痪(立即瘫痪还是好的,你能立马发现问题;最怕的是业务复杂的时候会埋下隐藏bug,bug并不会立即出现只有发布后用户某些情境下才出现)
那么怎么解决这两个问题呢?
首先我们需要把GoHome、Person、Car三个类进行解偶,其次定义好Person、Car的接口(interface)方便GoHome调用。
这样GoHome不需要关心Person、Car的具体实现,只需要按照定义好的接口定义去调用函数开发就行; 同理Person、Car也可以交给另外两个人去开发,只要按照interface来实现就OK。
代码如下(di框架可以使用github.com/Microsoft/t…
interface IPerson {
setName(name: string): void;
drive(car: ICar, address: string): void;
}
interface ICar {
startup(address: string): void;
}
class GoHome {
constructor(
@inject('IPerson') private person:IPerson,
@inject('ICar') private car:IPersICaron,
) {
this.person.setName('张三');
}
go() {
this.person.drive(this.car, 'home');
}
}
@injectable()
class Person implements IPerson {
private name;
setName(name: string) {
this.name = name;
}
drive(car: Car, address: string) {
car.startup(address)
}
}
@injectable()
class Car implements ICar {
startup(address: string) {
// 启动车到达目的地
}
}
所以,依赖注入模式就是:应用程序中的类是从外部源中请求获取依赖,而不是自己创建它们,这样就实现了应用和所依赖的类的解偶。 回到我们最初的问题:如何把业务逻辑和视图交互进行解偶呢?
如何拆分业务、视图
“业务”、“视图”我们可以把它们看成两个大的类:Service、View. 拆分后它们之间存在两种关联模式:
- 把视图的交互逻辑类注入到业务逻辑类中
- 把业务逻辑类注入到视图的交互逻辑类中
interface IView {
/* 关于接口的定义 */
}
class Service {
constructor(
@inject('IView') private view:IView
) {
}
/* 在业务逻辑中调用视图的方法完成交互 */
}
@injectable()
class View implements IView {
/* 具体代码 */
}
interface IService {
/* 关于接口的定义 */
}
class View {
constructor(
@inject('IService') private service: IService
) {
}
/* 在视图中调用底层业务逻辑的方法完成整个具体业务 */
}
@injectable()
class Service implements IService {
/* 具体代码 */
}
这两个方式有什么区别呢? 我们这里拿一个具体一点的TODO List作为例子:
把视图注入到逻辑层代码如下:
import "reflect-metadata";
import { injectable, container } from 'tsyringe';
interface TodoItem {
name: string;
state: 0 | 1;
}
@injectable()
class Dom {
private inputValue = '';
private contentFactory(content: HTMLElement) {
content.innerHTML = `
<h1>todo demo</h1>
<div id="content">
<input
id="input"
placeholder="请输入任务"
/>
<button id="button" type="button">
添加
</button>
</div>
<div id="list"></div>
`;
}
private factory(task: TodoItem) {
return `
<div class="task" data-name="${task.name}">
<div class="${task.state === 1 ? "task-name done" : "task-name"}"
data-name="${task.name}">
${task.name}
</div>
</div>
`;
}
addEvent(btnCallback: Function, toggleCallback: Function) {
const inputElem = document.getElementById("input") as HTMLInputElement;
const buttonElem = document.getElementById("button") as HTMLButtonElement;
const listElem = document.getElementById("list") as HTMLElement;
inputElem?.addEventListener('input', (event: any) => {
this.inputValue = event.target.value;
});
buttonElem?.addEventListener('click', () => {
btnCallback(this.inputValue);
this.inputValue = "";
inputElem.value = "";
});
listElem.addEventListener("click", (event: any) => {
const target = event.target;
if (
target.className.indexOf("task-name") >= 0 ||
target.className.indexOf("task") >= 0
) {
const name = target.getAttribute("data-name");
toggleCallback(name);
}
});
}
init() {
this.contentFactory(document.body);
}
update(tasks: TodoItem[]) {
const listElem = document.getElementById("list") as HTMLElement;
listElem.innerHTML = `${tasks.map(task => this.factory(task)).join('')}`;
}
}
@injectable()
class TodoService {
private todos: TodoItem[] = [];
constructor(
private dom: Dom
) {}
init() {
this.dom.init();
this.dom.addEvent(
(name: string) => this.add(name),
(name: string) => this.toggle(name)
);
this.dom.update(this.todos);
}
add(name: string) {
if (name === "") {
return;
}
this.todos.push({ name, state: 0 });
this.dom.update(this.todos);
}
toggle(name: string) {
const index = this.todos.findIndex((item) => item.name === name);
if (this.todos[index].state === 0) {
this.todos[index].state = 1;
} else {
this.todos[index].state = 0;
}
this.dom.update(this.todos);
}
}
const todoService = container.resolve(TodoService);
todoService.init();
把逻辑层注入到视图层的实现:
import "reflect-metadata";
import { injectable, container } from 'tsyringe';
interface TodoItem {
name: string;
state: 0 | 1;
}
@injectable()
class TodoService {
private _onUpdate: ((doms: TodoItem[]) => void) | undefined;
private todos: TodoItem[] = [];
private update() {
this._onUpdate && this._onUpdate(this.todos);
}
add(name: string) {
if (name === "") {
return;
}
this.todos.push({ name, state: 0 });
this.update();
}
toggle(name: string) {
const index = this.todos.findIndex((item) => item.name === name);
if (this.todos[index].state === 0) {
this.todos[index].state = 1;
} else {
this.todos[index].state = 0;
}
this.update();
}
onUpdate(callback: (tasks: TodoItem[]) => void) {
typeof callback === "function" && (this._onUpdate = callback);
}
}
@injectable()
class Dom {
private inputValue = "";
constructor(
private todoService: TodoService
){}
private contentFactory(content: HTMLElement) {
content.innerHTML = `
<h1>todo demo</h1>
<div id="content">
<input
id="input"
placeholder="请输入任务"
/>
<button id="button" type="button">
添加
</button>
</div>
<div id="list"></div>
`;
}
private factory(task: TodoItem) {
return `
<div class="task" data-name="${task.name}">
<div class="${task.state === 1 ? "task-name done" : "task-name"}"
data-name="${task.name}">
${task.name}
</div>
</div>
`;
}
private update(tasks: TodoItem[]) {
const listElem = document.getElementById("list") as HTMLElement;
listElem.innerHTML = tasks ? tasks.map(task => this.factory(task)).join("") : "";
}
private addEvent() {
const inputElem = document.getElementById("input") as HTMLInputElement;
const buttonElem = document.getElementById("button") as HTMLButtonElement;
const listElem = document.getElementById("list") as HTMLElement;
inputElem?.addEventListener('input', (event: any) => {
this.inputValue = event.target.value;
});
buttonElem?.addEventListener('click', () => {
this.todoService.add(this.inputValue);
this.inputValue = "";
inputElem.value = "";
});
listElem.addEventListener("click", (event: any) => {
const target = event.target;
if (
target.className.indexOf("task-name") >= 0 ||
target.className.indexOf("task") >= 0
) {
const name = target.getAttribute("data-name");
this.todoService.toggle(name);
}
});
}
init() {
this.contentFactory(document.body);
this.todoService.onUpdate(this.update.bind(this));
this.addEvent();
}
}
const domService = container.resolve(Dom);
domService.init();
比较上面的代码我得出的两种方式分别使用的使用场景为:
-
把视图注入到逻辑层的方式比较适合DOM结构相对比较固定的业务; 这样方便把整个业务制作成内部的NPM包,在不同平台开发的时候可以快速基于该业务NPM包开发出符合当前平台的交互;
-
而把逻辑层注入到视图适合底层业务逻辑一致,但是不同平台交互形式差别很大的情况;这样既可以保证业务代码的复用, 减少新平台开发的代码量, 又能保证交互的灵活性, 可以在不频繁更改NPM包的情况下,满足更多的平台交互.
当然以上仅为个人观点,欢迎大家一起讨论,目前这些想法都还在实验探索中.
优点
使用这种依赖注入的方式拆分视图和逻辑的方案优点很明显:
- 解耦了视图和逻辑的关联,耦合度更低
- 方便多人进行合作,同时进行开发
- 代码可以更大可能的复用, 我们可以把所有抽离出来的所有被依赖类做成内部的NPM包,后续开发新平台的时候可以很方便的复用业务逻辑
- 代码复用的提高带来了维护成本的降低, 某个业务逻辑修改的时候,一次修改多处生效
缺点
当然这样的方案也会带来一些不利的地方:
- 初次开发的时候代码量会有增多
- 对开发人员的业务抽象能力要求更高,理解概念成本会更高
结尾
目前这些都是针对当前业务的一些初步的思考、实践,还不是太完善; 后续也可能会有更多的心得继续和大家分享, 也欢迎大家如果有什么想法心得可以提出来, 共同讨论.文章中的例子现在还不是太合适后续也会进行修改、完善, 保证例子能更多的体现观点.
TODO LIST 的地址: github.com/webKity/tod…