preface
At present, we have a problem in the actual business: the business logic in multiple terminals is actually the same, but the forms of interaction are really different. For example, the interaction between PC terminal and mobile terminal is sometimes very different. So is there a way to separate the business logic from the interaction? If you can break it up how do you break it up? Here are some of my thoughts:
What is dependency injection
What is dependency injection (DI)? Take a small matter in reality as an example: Zhang SAN drives home; The pseudo-code of the program is as follows:
class GoHome {
private person = newThe Person (s);private car = new Car();
constructor() {
this.person.setName('Joe');
}
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) {
// Start the car to reach the destination}}Copy the code
What’s wrong with this code? We coupled the Person and Car classes in the GoHome concrete business logic, which caused some problems:
- There’s no way you can give it to three people at the same time (the actual business might be more complicated than the example);
- The maintainer needs to read all the code in the business during each maintenance process. Otherwise, if there is a bug in the Car class and the maintainer accidentally removes the startup function or the parameters in the startup function, the entire application will crash immediately. The biggest fear is that hidden bugs will be buried when the business is complicated. Bugs will not appear immediately, but only in certain situations after the release of users.)
So how to solve these two problems?
First, we need to decouple the three classes GoHome, Person and Car, and then define the interface of Person and Car to facilitate the call of GoHome.
In this way, GoHome does not need to care about the specific implementation of Person and Car. It only needs to call function development according to the defined interface definition. Similarly, Person and Car can also be developed by two other people, as long as it is implemented according to interface.
The code is as follows (the DI framework can be used at 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('Joe');
}
go() {
this.person.drive(this.car, 'home'); }}@injectable(a)class Person implements IPerson {
private name;
setName(name: string) {
this.name = name;
}
drive(car: Car, address: string) {
car.startup(address)
}
}
@injectable(a)class Car implements ICar {
startup(address: string) {
// Start the car to reach the destination}}Copy the code
So, the dependency injection pattern is that classes in an application request dependencies from external sources, rather than creating them themselves, thus decoupling the application from the classes it depends on. Back to our original question: How do you decouple business logic from view interaction?
How to split business and view
“Business” and “View” can be thought of as two large classes: Service and View. After splitting, there are two association patterns between them:
- Inject the view’s interaction logic class into the business logic class
- Inject the business logic class into the view’s interaction logic class
Class Service {constructor(@inject('IView') private view:IView) { @injectable() class View implements IView {/* Implements IView */}Copy the code
Constructor (@inject('IService') private service: {constructor(@inject('IService') private service: } @injectable() class Service implements IService {/* Implements IService {/* Implements IService */}Copy the code
What’s the difference between these two approaches? Let’s take a more concrete TODO List as an example: Inject the view into the logical layer as follows:
import "reflect-metadata";
import { injectable, container } from 'tsyringe';
interface TodoItem {
name: string;
state: 0 | 1;
}
@injectable(a)class Dom {
private inputValue = ' ';
private contentFactory(content: HTMLElement) {
content.innerHTML = 'todo demo
Add
`;
}
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") asHTMLElement; 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(a)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();
Copy the code
The implementation of injecting the logical layer into the view layer:
import "reflect-metadata";
import { injectable, container } from 'tsyringe';
interface TodoItem {
name: string;
state: 0 | 1;
}
@injectable(a)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(a)class Dom {
private inputValue = "";
constructor(
private todoService: TodoService
){}
private contentFactory(content: HTMLElement) {
content.innerHTML = 'todo demo
Add
`;
}
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") asHTMLElement; 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();
Copy the code
By comparing the above code, THE usage scenarios of the two methods are as follows:
-
Injecting views into the logical layer is suitable for businesses where the DOM structure is relatively fixed; In this way, it is convenient to make the whole business into an internal NPM package, which can be quickly developed based on the business NPM package in accordance with the current platform when developing on different platforms.
-
Injecting the logical layer into the view is suitable for situations where the underlying business logic is consistent, but the interaction pattern varies greatly from platform to platform. This ensures the reuse of business code, reduces the amount of code developed for new platforms, and ensures the flexibility of interaction, allowing for more platform interactions without frequent changes to the NPM package.
Of course, the above is only a personal point of view, welcome to discuss, at present these ideas are still in the experimental exploration.
advantages
The advantages of using this dependency injection approach to split view and logic are obvious:
- Decoupled the association between view and logic, resulting in lower coupling
- It is convenient for many people to cooperate and develop at the same time
- The code can be reused more likely. We can make all the dependent classes extracted into internal NPM packages, which can easily reuse business logic when developing new platforms
- Improved code reuse reduces maintenance costs. When a business logic is modified, multiple changes take effect at a time
disadvantages
Of course, there are some disadvantages to such a scheme:
- The amount of code will increase during initial development
- More business abstraction is required of the developer and the cost of understanding concepts is higher
At the end
At present, these are some preliminary thoughts and practices for the current business, which are not perfect. There may be more comments to share with you in the future, and you are welcome to put forward your thoughts and comments and discuss them together. The examples in the article are not suitable now, and will be revised and improved in the future to ensure that the examples can reflect more views.
TODO LIST address: github.com/webKity/tod…