What is the problem to be solved?

A problem well-stated is Half-solved

Fred Brooks, in his famous book “No Silver Bullet – Essence and Accident in Software Engineering”, addresses these points of complexity

  • Complexity
  • Conformity
  • Changeability
  • Invisibility

In another famous article, “Out of the Tar Pit”, more specific reasons are proposed for these three types of complexity

  • Complexity caused by State
  • Complexity caused by Control
  • Complexity caused by Volume

They both put State in the cause of the complexity.

What is the problem with Imperative Programming?

It is not impossible for us to update these states. Imperative Programming is very intuitive, that is, give a bunch of read and write State instructions to the CPU, and the CPU will execute them exactly. We can draw the software execution process like this:


The external behavior of the software is to produce a series of status updates in chronological order. That’s the yellow nodes, and the chronological order of those nodes. But here’s the thing:

This step-by-step, chronological programming style describing these yellow nodes produces source code that is neither simple nor readable.

Even though we drew a lot of nice functions, these blue circles. It’s still not going to change. Last year I wrote two article about code readability: zhuanlan.zhihu.com/p/46435063 and zhuanlan.zhihu.com/p/34982747. It’s a little too long-winded now. And readable is a somewhat subjective concept. Rich Hickey had a great talk called “Simple Made Easy, “where he said Simple is an objective indicator. I’ve specified Simple as the following four properties that can be measured objectively

  • The Quantity is small
  • Sequential: Sequential
  • Continuous: It is necessary to have a causal relationship between the previous line and the next line. And the logic of cause and effect should not be too far apart
  • Isolated: Things have little interaction with each other. Being able to isolate means you can break it down into components

The opposite of these four properties is

  • Quantity large
  • Concurrent, parallel: concurrency is logical, parallelism is physical. Either way, it is more complex than sequential.
  • 1. Long range causality
  • Entangled: A bit of formation

Imperative Programming represents this real world. The real world is Quantity large, parallel all the time, long range causality and entangled. Simplicity represents an imaginary Eden, a accommodation for our fleshbrain’s weak perception and computing power. Simplicity is hard, when Simplicity is not the reality.

So, we can break down the problem we’re trying to solve into these two problems:

  1. Create a virtual Eden for our flesh brain, in which Quantity small, Sequential, Continuous, Isolated.
  2. Because Eden is detached from the real world. So when things go wrong in the brutal real world, it provides tools to help humans understand what actually happens.

Does OOP/DDD solve these four problems?

DDD can be thought of as three steps

  1. Application Service loads the Domain Model
  2. State changes are encapsulated by Aggregate Root
  3. Side effects are Domain Model updates and generated Domain events

At its core is a black-box encapsulation that aggregates root pairs of state. There are two problems with this so-called black box packaging

  1. After all, there is no essential difference between method of aggregate root and function of Imperative programming
  2. Interactions between objects, especially updates to multiple objects by business processes, have no natural aggregation root attribution. In other words, the true root of the aggregation should be the business process itself. But a process is not an Entity.

Why there is no essential difference:

  • Quantity Small: In OOP/DDD all states are still updated chronologically, one by one
  • Sequential: For performance, code is still written in a multi-coroutine or multi-threaded mode
  • Continuous: a complete business process, or is it broken down into controller apis? Often, however, in a controller, there is code that just happens to be happening at the same time, but is not logically related to each other.
  • Isolated: ORM creates an illusion for us, and then the problem of 1+N queries brings us back to reality. Requiring the Application Service to load the entire Domain Model into memory at once is not isolated at all. There is often a feeling that you might as well write all your code in Application Service.

In summary, object orientation is not the silver bullet, and neither is DDD.

How does TypeScript address these four issues?

TypeScript addresses these four issues

View is bound to data

The first problem to solve is to minimize State. For example, we could make views “stateless” and bind all views to data. For example, to implement this function:


The corresponding View is the Html DOM, which is itself a state. But we can bind it to data:

<Button @onClick="onMinusClick">-</Button>
<span margin="8px">{{ value }}</span>
<Button @onClick="onPlusClick">+</Button>Copy the code

Corresponding data

export class CounterDemo extends RootSectionModel { value = 0; onMinusClick() { this.value -= 1; } onPlusClick() { this.value += 1; }}Copy the code

Why is this elimination state? Isn’t the DOM state updated when this.value is written? Value -= 1 and then this.updateView(this.value) is the state. The core problem is that the essence of a binding is that it describes the identity relationship between two states. The relationship is not in the timeline. This way we can ignore the bound states when we tell a story about time. This is the core principle by which binding reduces state.

Front-end state is bound to database state

We can look at all the states in the system.


Hosting interface state is not enough. It just shifts the problem, doesn’t it have to manage the front-end state? All kinds of story? So we have to simplify it a little bit more, so for each of the states, we have to say, is it possible to simplify?

For example, we want to directly bind the front-end state to the main stored state in the database.


This is a very common list of page requirements. We can certainly encapsulate a domain Object at the back end, then make some urls, encapsulate the DTO, and then encapsulate some View Models at the front end, and then present them. We can also do this:

<CreateReservation /> <Card title=" Reserved list "margin="16px"> <Form Layout ="inline"> <InputNumber :value="&from" label=" Seat number From "/> <span margin="8px"> ~ </span> <InputNumber :value="&to" label="to" /> </Form> <span> <List :dataSource="filteredReservations" itemLayout="vertical" size="small"> <json #pagination> { "pageSize": 10 } </json> <slot #element="::element"> <ShowReservation :reservation="element.item"> </slot> </List> <Row justifyContent="flex-end" marginTop="8px"> <Button type="primary" icon="plus" @onclick ="onNewReservationClick"> </Button> </Row> </Card>Copy the code

The corresponding bound object is then written like this:

export class ListDemo extends RootSectionModel { public from: number = 1; public to: number = 9; public get filteredReservations() { return this.scene.query(Reservation_SeatInRange, { from: this.from, to: this.to }); } public get totalCount() { return this.filteredReservations.length; } public onNewReservationClick() { this.getSectionModel(CreateReservation).isOpen = true; } public viewCreateReservation() { return this.scene.add(CreateReservation); }}Copy the code

As you can see, after the from value changes, the filteredReservations change, and so does the totalCount. If the data source is an array, this demo really doesn’t matter. But notice that the data source here is the Mysql database. But we use it as if it were a local array.

Here we combine the front-end, back-end, and middle RPC states into one using a generic back-end interface like GraphQL. But unlike the GraphQL front end, which defines queries, what can be queried is still pre-registered to avoid the problem of the front end abusing indexless queries. The one that does this registration is Reservation_SeatInRange, which is defined as follows

@sources.Mysql()
export class Reservation extends Entity {
    public seatCount: number;
    public phoneNumber: string;
}

@where('seatCount >= :from AND seatCount <= :to')
export class Reservation_SeatInRange {
    public static SubsetOf = Reservation;
    public from: number;
    public to: number;
}Copy the code

Avoid the extra state added by the front and back end translations

Both the front end and the back end are working on the same step of the same process, and the context is highly consistent. We can consider that there are actually two layers of RPCS


When the RPC protocol fully serves the corresponding page form, the request and response states of the RPC protocol are basically equivalent to the state of the page form. Of course you can say that RPC protocols can be generic, reusable and front-end independent. Because of this attitude, there is an extra layer of BFF, isn’t there? Create new problems.


Suppose you want to implement the simple form above. The view looks something like this

<Card title=" restaurant reservation "width="320px"> <Form> {{message}} <Input :value=" &phonenumber" label=" phoneNumber" /> <InputNumber <Button @onclick ="onReserveClick"> </Button> </Form> </Card>Copy the code

We then bind this view to a form object that doubles as a front-and-back RPC interaction protocol:

@sources.Scene export class FormDemo extends RootSectionModel { @constraint.min(1) public seatCount: number; @constraint.required public phoneNumber: string; public message: string = ''; public onBegin() { this.reset(); } public onReserveClick() { if (constraint.validate(this)) { return; } this.saveReservation(); setTimeout(this.clearMessage.bind(this), 1000); } @command({ runAt: 'server' }) private saveReservation() { if (constraint.validate(this)) { return; } const reservation = this.scene.add(Reservation, this); try { this.scene.commit(); } catch (e) { const existingReservations = this.scene.query(Reservation, { phoneNumber: this.phoneNumber }); if (existingReservations.length > 0) { this.scene.unload(reservation); Constraint. ReportViolation (this, 'phoneNumber, {message:' the same phone number can only have a reservation,}); return; } throw e; } this.reset(); This. message = 'scheduled success '; } private reset() { this.seatCount = 1; this.phoneNumber = ''; } private clearMessage() { this.message = ''; }}Copy the code

It’s actually stored in a database, not this form, but another one:

@sources.Mysql()
export class Reservation extends Entity {
    public seatCount: number;
    public phoneNumber: string;
}Copy the code

We either omit the state or change it from a manually managed state to a derived state by:

  • Convert to derived states: computed properties, state synchronization, visual chart, materialized visual chart, cache
  • Let the remote state be used directly as if it were local
  • Reduce transient states introduced by network traffic

Sequential expression, Concurrent execution

Having fulfilled one goal of Quantity Small, let’s look at the second goal, making code sequential. Code sequential is very simple, just write it sequentially. The problem is that if the execution is sequential, it will load very slowly. We have two reference objects to learn from:

  • Speculative CPU execution: Although we give the CPU a single core of a serial instruction stream, the CPU can speculative concurrent execution without data dependency.
  • Facebook Haxl:wiki.haskell.org/wikiupload/…

Suppose we have two tables:

CREATE TABLE `User` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `inviterId` int(11) NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;

CREATE TABLE `Post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `authorId` int(11) NOT NULL,
  `editorId` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;Copy the code

The corresponding class definition:

@sources.Mysql() export class User extends Entity { public id: number; public name: string; public inviterId: number; public get inviter(): User { return this.scene.load(User, { id: this.inviterId }); } public get posts() { return this.scene.query(Post, { authorId: this.id }); } } @sources.Mysql() export class Post extends Entity { public id: number; public title: string; public authorId: number; public get author(): User { return this.scene.load(User, { id: this.authorId }); } public get editor(): User { return this.scene.load(User, { id: this.editorId }); } public get authorName(): string { return this.author.name; } public get inviterName(): string { const inviter = this.author.inviter; return inviter ? inviter.name : 'N/A'; }}Copy the code

When accessing the author and editor, write it in serial:

const author = somePost.author
const editor = somePost.editor
return { author, editor }Copy the code

But since there is no actual access to either object, there is no actual data dependency, and such serial code is executed concurrently. But such a visit

const author = somePost.author
const authorInviter = author.inviter
return { author, authorInviter }Copy the code

Because author.inviter creates a data dependency, it cannot be executed concurrently. So this provides a way to express concurrency in serial code using data dependencies.

Isolated, allowing the component to pipe only itself

And then let’s look at the third target, Isolated.


Suppose you want to render the Post as the table above. We know that the author and inviter fields are both foreign key associated. So if there is no optimization (Isolated write, Isolated execute), then there must be additional N + N sliver query, where N is 4 rows.


However, only 3 queries are generated in the actual execution. The first query is for multiple posts, the second query is for all authors, and the third query is for all invitators of these authors. Here the IO merging of multiple HTTP requests into three is done automatically.

2019-07-19T11:25:04.136927Z	   27 Query	START TRANSACTION
2019-07-19T11:25:04.137426Z	   27 Query	SELECT id, title, authorId FROM Post
2019-07-19T11:25:04.138444Z	   27 Query	COMMIT
2019-07-19T11:25:04.772221Z	   27 Query	START TRANSACTION
2019-07-19T11:25:04.773019Z	   27 Query	SELECT id, name, inviterId FROM User WHERE id IN (10, 9, 11)
2019-07-19T11:25:04.774173Z	   27 Query	COMMIT
2019-07-19T11:25:04.928393Z	   27 Query	START TRANSACTION
2019-07-19T11:25:04.936851Z	   27 Query	SELECT id, name, inviterId FROM User WHERE id IN (8, 7, 9)
2019-07-19T11:25:04.937918Z	   27 Query	COMMITCopy the code

Select * from general log where id = XXX and id = IN (XXX); So not only did it merge into two HTTP requests, but it further merged into two Mysql queries.

This avoids the requirement that the Application Service load all the domain-level data in one big JOIN query. You can keep code Isolated as well as Isolated. Each component takes care of its own business, tying up its own data, regardless of what everyone else is doing.

Continous business processes

Let’s look at the last property, Continuous. Two problems were mentioned earlier

  • In DDD, a business process does not know to which aggregation root it belongs.
  • Imperative Programming shreds sequential business processes into small segments for execution. Contextual logic conveys causality through global state (that is, the database).

Our solution is to provide an Entity called Process. Like other Entities, it is bound to database tables and is the data carrier. It also represents a business process. So we’ve persisted a business process function as an Entity. We can also say that we have turned the business document into an executable function.


Suppose you need to implement the lifecycle of the Account shown above. The account is initially locked unless a password is set. Login permission then fails, but fails up to three times. If more than three times, it returns to the locked state. The business logic, written in Process, looks like this:

const MAX_RETRY_COUNT = 3; @sources.Mysql() export class Account extends Process { public name: string; // plain text, just a demo public password: string; public retryCount: number; public reset: ProcessEndpoint<string, boolean>; public login: ProcessEndpoint<string, boolean>; public process() { let password: string; while (true) { locked: this.commit(); const resetCall = this.recv('reset'); password = resetCall.request; if (this.isPasswordComplex(password)) { this.respond(resetCall, true); break; } this.respond(resetCall, false); } let retryCount = MAX_RETRY_COUNT; for (; retryCount > 0; retryCount -= 1) { normal: this.commit(); const loginAttempt = this.recv('login'); const success = loginAttempt.request === password; this.respond(loginAttempt, success); if (success) { retryCount = MAX_RETRY_COUNT + 1; continue; } } __GOBACK__('locked'); } private isPasswordComplex(password: string) { return password && password.length > 6; }}Copy the code

This entity is persistent and the table structure looks like this:

CREATE TABLE `Account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL UNIQUE,
  `password` varchar(255) NOT NULL,
  `status` varchar(255) NOT NULL,
  `retryCount` int(11) NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;Copy the code

So it’s not a technique to persist javascript coroutines into unreadable binaries, that’s the last generation of persistent coroutines. Note that there is a status field, which corresponds to the label statement in the code. When the label statement is executed on the corresponding line, the status is set to the corresponding value. Instead of using a separate BPM engine, we do not have to manage the process context and synchronize the process state back to the business database. Process is the business document business entity, business document carries the process.

In this way, we can also solve the problem of where to put the process logic in DDD, which should be put on the process document. For example, orders, quotation sheets, which represent the state of the process. At the same time, we solved the problem of Continous. But what about such a big process() function? You can’t do it from scratch every time. The code used looks like this:

This is the interface accountDemo.xml

<Form width="320px" margin="24px"> <Input label=" user name" :value="&name" /> <Input Label =" password" :value="&password" /> <switch <slot #default><Button @onclick ="onLoginClick"> </Button></slot> <slot #locked><Button </Button></slot> </switch> {{notice}} </Form>Copy the code

The interface is reactive, and whatever state the process drives, the interaction of that state is displayed.

This is the interface corresponding accountDemo.ts

@sources.Scene export class AccountDemo extends RootSectionModel { @constraint.required public name: string; @constraint.required public password: string; private justFailed: boolean; private get account() { const accounts = this.scene.query(Account, { name: this.name }); return accounts.length === 0 ? undefined : accounts[0]; } public get notice() { if (this.justFailed === undefined) { return ''; } if (this.justFailed === false) {return 'login succeeded '; } if (! this.account) { return ''; } if (this.account.status === 'locked') {return 'account is locked'; } return 'leave ${this.account. RetryCount} times retry'; } public get status() { if (! this.justFailed || ! this.account) { return 'default'; } return this.account.status; } public onLoginClick() { if (constraint.validate(this)) { return; } if (! This. The account) {constraint. ReportViolation (this, "password", {message: 'user name or password mistake,}); return; } try { const success = this.scene.call(this.account.login, this.password); if (! success) { throw new Error('failed'); } this.justFailed = false; } catch (e) { this.justFailed = true; The constraint. ReportViolation (this, "password", {message: 'user name or password mistake,}); return; } } public onResetClick() { if (this.account) { this.scene.call(this.account.reset, 'p@55word'); }}}Copy the code

We can drive the Process through the ProcessEndpoint exposed by Process. If you do not need a return value, one-way communication with ProcessEvent will do.

With Process, we can encapsulate all state changes of a Process into this Process, achieving true encapsulation. Also, forking merges within the process can be expressed more naturally. As well as the case that a user operation needs to drive multiple processes at the same time, such as marketing Process, sales Process, warehousing and inventory Process, etc., which can well realize their independent closed-loop. You don’t have to do a little bit of everybody’s business in a big controller.

So, OOP/DDD isn’t enough, TypeScript is. But is your TypeScript TypeScript?

Who are you?

Our name is multiplication cloud. The question we are challenging is

From business idea to software launch, how can speed up 10x?

The TypeScript syntax shown here can be fully checked by ESLint/tsLint and is pure TypeScript. But we have our own aPaaS platform that implements runtime support for all of the above. The official website and IDE are under intense recruitment development. The following is the commercial break, thanks for reading.

For the front. For the front. For the front.

We provide top engineers with suitable technical development space, free working environment and competitive compensation and welfare. At the same time, it also provides full learning and growth opportunities for high potential newcomers. Here, there’s no 996. Efficiency. It is not rank and appointment that counts, but the persuasive power of the code. Here, we do not fight chicken blood, we use reason and drive to conquer all kinds of challenges. Here, there will also be a project schedule, but we are not afraid of delay, we have enough time to make ourselves more satisfied.

Working location in Beijing West two banner, salary see recruitment link: www.zhipin.com/job_detail/…