There is a myth among many developers that the constant degradation of code quality and maintainability in projects is due to time constraints and changing requirements. With clearer product requirements and adequate development time, development teams can ensure code quality and maintainability over the long term.

The DDD(Domain-driven design) and ADT(algebraic data typing) models introduced today provide another part of the answer: the development team is largely to blame for the continued deterioration in code quality.

Without a more rational development model, a project’s code quality will deteriorate dramatically over time and complexity. No amount of clear product requirements, no amount of development time, can prevent code base corruption.

Because DDD and ADT are big topics. It is difficult to develop fully in a technical article. Therefore, this article is mainly to briefly introduce the relationship between them and the thought behind, hoping to bring you some inspiration. For those of you who are interested, you can check out more in-depth material later.

The main content is divided into the following parts:

  1. How code quality is evaluated

  2. DDD concept and definition

  3. Definition of ADT concept

  4. Case: Data modeling with DDD + ADT

  5. Example: Process modeling with DDD + ADT

  6. conclusion

1. Evaluation method of code quality

No matter what kind of improvement or optimization you do, the most critical starting step is always to find a measure and a way to measure it. Improving code quality is no exception.

1.1 Evaluation criteria for code quality

  • Extensibility

  • Maintainability

  • Readability (Readability)

  • Testability

  • etc.

The above indicators are to evaluate the code at a relatively macro level, preferring qualitative indicators.

Given two pieces of code, developers can decide which is more extensible, but it’s not easy to agree on how much. Readability, in particular, depends heavily on developer experience and subjective preference.

Quantitative metrics, such as Lint rules, circular reference checks, etc., can also help improve code quality. But the downside of these quantitative metrics is that they evaluate code at a very micro level and don’t care about the problem the code is trying to solve.

This kind of quantitative index is easy to fail or even bring adverse effect. There is also limited help in dealing with specific problems, and it mainly serves to unify code styles, constrain writing, and avoid common anti-patterns.

We want an evaluation criterion that can help us think about problems at the macro qualitative level, but also help us solve problems at the micro quantitative level.

1.2. Several methods to improve code quality

  • SOLID Principles

  • Clean Code Principles

  • Design Patterns

  • Low Coupling, High Cohesion

  • etc.

There are a number of programming principles that can improve code quality, and two of the most famous are probably SOLID and Design Patterns, which have been around for at least two or three decades and are still talked about by developers today.

Single responsibility, open/close principle, interface separation principle, observer pattern, subscription pattern, interpreter pattern, decorator pattern, low coupling, high cohesion… These words are familiar to the programmer community and have never changed.

But:

  • Some developers believe that programming languages have evolved so many new features, so many problems have changed, that the original design patterns are no longer applicable.

  • Many developers find that strictly following and promoting these principles makes the code harder to understand and maintain.

They found a cognitive bias behind the programming principle that correlation does not equal causation.

Features extracted from high-quality code have some correlation with code quality, but do not imply causation: high-quality code has certain characteristics, and does not mean that code with certain characteristics is high-quality code.

As shown in the figure above, assume that the largest circle is the entire code universe, with the small green circles inside being high-quality code, the small green circles outside being low-quality code, and the yellow circles being code that conforms to programming principles such as SOLID.

We can see that in this relationship:

  • Good code mostly features programming principles like SOLID (so we can extract SOLID features from them)

  • But there is a lot more code that conforms to characteristics like SOLID that is of low quality

  • There is a small percentage of code that is not SOLID, but it is good code

Therefore, blindly promoting a code feature in a code base can often backfire.

1.3. Deficiencies of the current code guidelines

The current code quality assessment model and code guidelines still have the following shortcomings:

  • Subjective, depending on the Subjective experience of the developer

  • It is Unclear and Unclear

  • Hindsight, the after-the-fact evaluation of the code already written, lacks constructive guidance for the code itself

  • Imprecise, not precise enough

  • External, the form around the surface of the code, ignores the nature of the problem, or assumes that the problem has already been solved

  • etc.

In contrast to the above deficiencies, we might want to:

  • Objective, all rational developers have the same understanding

  • Clear C

  • Insight, which provides Insight into problems before or during code writing

  • Precise, code evaluation criteria

  • Internal is about the nature of the problem, not just the way the code is written

  • etc.

Elegant code is not just some kind of writing or programming technique, but a byproduct of a deeper understanding of the nature of the problem.

Therefore, how to write code, how to write high-quality code, is inseparable from improving the level of understanding of the problem.

A guide model for writing high-quality code should not only focus on the phase of how the code is written, but also have a pre-phase of how the problem is understood.

2. DDD concept and definition

DDD (Domain-driven Design) is a development pattern that became popular in 2003, and it has a very broad meaning.

Domain-driven design focuses not only on how the code is written, but also on the activities before the code is written, such as production processes and collaborations.

Tactical DDDS tend to write code. Strategic DDD is a DDD that favors production and research processes and collaboration. But the core of them is the same, and we’ll talk about that later. Before we do that, let’s look at the naive production-research model.

2.1 Naive production-research model: demand-driven design

We can call this pattern demand-driven design.

The general process is that product managers communicate with business colleagues and do market research and analysis, dig out product requirements and make product requirements document (PRD), and then develop and provide code implementation according to the requirement description in PRD.

In this model, businesses have their own business language, products have their own product language, and development engineers have their own technical language. There is a certain correlation, but it’s not strong enough.

No one is asking developers to write code using business-language nouns and concepts.

The problem with this pattern from a domain-driven design perspective is that it’s not like one team, it’s like three teams.

Developers communicate mainly in technical terms and consume materials used in upstream products. It is difficult for developers to determine whether the requirements they receive are the real requirements of the business, whether the logic they understand is the same as that of their business colleagues.

In this case, the developer strongly wants the requirements to be as real, unambiguous, and stable as possible. Developers resent being delivered upstream with uncertain requirements and frequent code changes.

When it is difficult to reach a consensus with the product through communication, the developer will want to communicate directly with the business to understand the first-hand needs of the business.

In the pattern of requirements driven design, requirements have a short life cycle and are basically abandoned shortly after they are released. Few product teams can always maintain complete product documentation that corresponds to functionality online. Often, after the product manager changes, you need to read the code to push back the product logic.

The problem here is that the code is written in a technical context, and it doesn’t easily reflect concepts in the product, let alone business concepts.

2.2 Core ideas and key processes of domain-driven design

In the production and development model of domain-driven design, the emphasis is on knowledge.

If need is about How and How to do, knowledge is about What and Why, What and Why.

Knowledge and need are not in conflict. Need comes from knowledge.

We can also call domain-driven design knowledge driven development. It emphasizes the process from domain knowledge to code implementation.

Tactical DDD, strategic DDD, and other aspects of DDD revolve around the core idea of knowledge-driven development. Grasp the core, not easy to be confused by many complex concepts in DDD.

The key process of domain-driven design is to solve the problem of communication language inconsistency between different positions and roles in the team.

The domain experts in the figure above are, in most cases, business people. But that’s not always the case. Domain expertise in DDD is not a job, it’s a role. As he becomes more specialized, knowledgeable, and authoritative in his particular field, he becomes the domain expert.

By building a common language for the team, developers can be more certain that the product requirements are real requirements and that the logic they understand is consistent with that of domain experts or business colleagues. Because everybody uses the same set of words, the same definitions, there’s a common ground.

The difference between domain-driven design and requirements driven design can be seen in the format of their conferences.

In the demand-driven design meeting, the product manager usually explains the completed PRD, and the pre-knowledge extraction, sorting and definition have generally taken shape.

Domain-driven design conferences are different. This is a meeting pattern called Event Storming. Multiple roles such as domain experts and development engineers work together on a long whiteboard to express and construct domain knowledge based on timeline and events.

In this process, the concepts of entities, events, processes, relationships and so on in the business domain are proposed, defined and clarified, and everyone in the field has a consensus on this. The words and terms formed in each event-storm meeting serve as the unified language and corpus for the next meeting or working communication.

2.3, summary

  • High-quality code comes from understanding a problem correctly, and it’s hard to solve a problem elegantly without understanding it

  • Code writing, style, mode and other means, based on the correct cognitive basis to achieve the best results

  • Neglecting to improve your awareness of the problem and blindly applying code tricks and design patterns often makes your code worse

  • The core idea of DDD is to establish a common language with domain experts as the core to ensure the reliable transfer of knowledge and requirements

3. Definition of ADT concept

Adopting the strategic component of domain-driven design optimizes the team collaboration pattern, ensuring that the knowledge component is based on a well-defined consensus as it moves from knowledge to code, and that requirements are more likely to be real.

The tactical part of domain-driven design is the other way around: from code to knowledge.

Simply put, DDD requires the use of words and concepts in the “team language” in your code.

Code should faithfully reflect domain knowledge. Variable names, method names, and core logic in the code should reflect the definitions in the domain knowledge. Coding is not arbitrary, but rather every developer with domain knowledge can write roughly the same code, rather than a wide variety of implementations.

If requirements drive design, the code is required to meet the epitaxial definition of product requirements. That is to meet product requirements at the functional level of input/output.

Domain-driven design requires that the code meet the definition of domain knowledge. Functional requirements are met not only at the black box level of input/output, but also at the white box level of code details.

This requires that we know how to translate domain knowledge into code implementation.

Algebraic Data Types (ADT) are key programming features that support these requirements.

3.1 Definition of domain and domain knowledge

First we need to define precisely what is Domain and what is Domain Knowledge.

  • A Domain is a collection of related problems

  • Domain Knowledge is the collection of all the true propositions involved in a series of interrelated problems

All domains are composed of some key conceptual nodes, and domain knowledge is the definition of the relationship between these conceptual nodes, expressed as logical true propositions.

Domain knowledge as a proposition is expressed in product requirements as a set of domain rules/business rules.

What production and research teams often call business logic can be translated into a more rigorous form of mathematical logic.

3.2 Curry-Howard isomorphism

Curry-howard Isomorphism: The principle of domain knowledge expression in code.

  • Propositions as Types, Proofs as Programs

  • Type-driven Development, using types to express domain knowledge (true propositions in the domain)

  • All values that conform to a type are proofs of the propositions represented by that type (Witness)

  • True statement: a type that has at least one value

  • False statement: type without any value

Curry-howard isomorphism, which gives a way to translate propositions into types, provides a general way to translate domain knowledge into code implementation.

In particular, the Product Type corresponding to And relation And the Sum Type corresponding to Or relation constitute algebraic data Type (ADT).

Later we will demonstrate how to use Product Type and Sum Type to do data modeling and process modeling with domain knowledge/business rules.

It is worth noting that in curry-Howard isomorphism, true and false statements do not correspond to true and false of Boolean Type. It is the number of values allowed by the type (size), 0 of which are false statements and at least 1 of which are true statements.

That is, all code that works is true in this sense, and throwing errors and so on means encountering false statements about the program.

3.3 Definition of bugs and criteria for identifying high-quality code

Clearer statements are made through Curry-Howard isomorphisms, Bug definitions, and criteria for high-quality code.

In the process of translating domain logic into code logic, a Bug occurs when a mismatch occurs and the propositions in the domain do not match those in the code.

  • True propositions in the domain (domain knowledge), false propositions in the code (Error/Crash/Halt)

  • Illegal-states /Unexpected Behaviors are false propositions in the domain but true propositions in the code

All programs that run are true in the procedural sense, but not in the business sense. These mismatches are called illegal-states, illegal-operations, or Unexpected Behaviors. Should be avoided.

High quality code, contrary to the definition of bugs, is the same proposition in the domain as in the code.

  • True propositions in the domain (domain knowledge) are also true propositions in code

  • False propositions in the domain, false propositions in the code

This means that the programs and data running in the code need to make business sense. Fewer illegal states, illegal operations, and unexpected behavior.

3.4 Basic knowledge of Type Theory

In type theory, every term has a type. A term and its type are often written together as “term : type”.

Type theory and set theory are similar in some respects in that they are different models of collections. In this case, a collection means a collection of things.

In type theory, things are called terms, and they have one and only one type. And type can have zero to any number of terms. Some of the typical types are listed below:

  • Empty type, 0 items;

  • Unit type, 1 item;

  • Boolean type, two items, true or false;

  • Natural Numbers type, infinite number of entries, starting at 0.

  • etc.

Above, we have listed some basic types, and the number of terms for them is also marked, similar to the number of elements in set theory.

Algebraic Data Types

In type theory, an algebraic data type is a kind of composite type, i.e., a type formed by combining other types.

The so-called algebraic data type (ADT) refers to the way in which multiple types are combined into new types with some algebraic characteristics.

From the previous Cury-Howard isomorphism, we know that the corresponding type of logical Or is Sum type, which expresses the relationship of mutually exclusive Or. The corresponding type of logical And is Product type, which expresses the coexistence And relationship.

Where, “Sum” means adding, “Product” means multiplying, and describes the relationship between “Sum type” and the number of items of “Product type” and the number of items of its component types.

  • Sum type: size(A | B) = size(A) + size(B)

  • Product type: size(A & B) = size(A) * size(B)

As above, the size (..) The function gets the number of terms allowed for a particular type.

So, Sum type corresponds to addition, because A and B are mutually exclusive and will not appear together, so either A or B is the Sum type. So all the possibilities are the number of possibilities of A, plus the number of possibilities of B.

// sum types
type Value = string | number;

const a: Value = 'John';
const b: Value = 70;
Copy the code

Product type corresponds to multiplication, because A and B are in the relationship of “and”, they coexist and appear together, so there is both A and B. So let’s go to the possibilities of A and B, where each of A matches all of B, so there’s A B, so A times B.

// product types
type Person = { name: string; age: number };
// type Person = { name: string } & { age: number }

const person: Person = { name: 'John', age: 70 };
Copy the code

As above, there is a Product Type relationship between the fields of an object. Product type is not a specific type, but a series of types, as long as the size relationship behind them is multiplicative. Similarly, sum type refers to those types whose size relationship is additive.

Just so far, algebraic data types (ADT) have been used to optimize the quality of our code. Next, let’s look at its application to data modeling and process modeling.

4. Case: Use DDD + ADT to do data modeling

Assume a domain rule (business definition) with the following user information:

  • The user is either a logged in user or an unlogged user (tourist)

  • Visitors have random nicknames

  • The login user has a nickname and Email address

  • The Email information is either an authenticated Email or an unauthenticated Email

  • An authenticated Email has a validation timestamp

  • User information is obtained through the Http API

They are described as a set of rules described in natural language, and can actually extract some propositions described in logical language. Such as:

  • Logged-in users have nicknames — true, true propositions

  • The logged-in user has Email information — false, false statement

  • Unverified emails have time stamps — false, false propositions

  • etc

Thus, domain knowledge is not necessarily a straightforward logical language, but it can always extract logical propositions that guide us to translate into types.

4.1. Common data modeling

Type UserInfo = {// If the user is not logged in, the id is an empty string id: string; // If the user is not logged in, name is a randomly generated nickname name: string; // If the user is not logged in, email is an empty string email: string; // Whether the user is logged in isLogin: Boolean; // This field is false when the mailbox is not verified. IsEmailVerified: Boolean; EmailVerifiedAt: string; emailVerifiedAt: string; emailVerifiedAt: string; }; Type JsonResponse = {error? : string; // When error is null, this field is user information data? : UserInfo; };Copy the code

The data modeling of user information in this example is so simple that most developers are used to it and can easily write code like the one above without any problems. The code is concise, clear and intuitive, with complete comments. If not good code, at least good code.

4.2 Domain type modeling based on DDD + ADT

Using DDD + ADT model, the types of user information are roughly as follows:

type VerifiedEmailInfo = {
  type: 'VerifiedEmailInfo';
  email: string;
  verifiedAt: string;
};

type UnverifiedEmailInfo = {
  type: 'UnverifiedEmailInfo';
  email: string;
};

type EmailInfo = VerifiedEmailInfo | UnverifiedEmailInfo;

type LoginUserInfo = {
  type: 'LoginUserInfo';
  id: string;
  name: string;
  emailInfo: EmailInfo;
};

type GuestUserInfo = {
  type: 'GuestUserInfo';
  name: string;
};

type UserInfo = LoginUserInfo | GuestUserInfo;

type ErrorResponse = {
  type: 'ErrorResponse';
  error: string;
};

type DataResponse = {
  type: 'DataResponse';
  data: UserInfo;
};

type JsonResponse = ErrorResponse | DataResponse;
Copy the code

We wrote nine types at a time, which looked much more complex, with more nesting, more repeating fields (email, name, etc.), no comment, and double the number of lines of code.

The above code, however, more closely matches the description of business rules And more accurately matches the relationship between And And Or in domain knowledge. If it doesn’t seem as simple as the first, this complexity is also the complexity of domain knowledge itself, and the simpler definition is, in a sense, oversimplification.

Moreover, the complexity of the code is not necessarily related to the length of the code. Superficial simplicity is not the same thing as essential simplicity.

On the surface, the left side has less nesting, fewer fields, fewer types, shorter code, and is definitely simpler in form than the right side.

In fact, when we label the relationships between types, we see that the left side actually has more multiplications, while the right side contains several additions. Multiple types of multiplicative complexity allow more possibility of term and larger size. Therefore, the term size on the left is larger than that on the right.

4.3 Illegal state of code base corruption

When the propositional space of type is larger than the propositional space of domain rule, the extra part is the space of illegal state.

The illegal state continues to corrupt our code base, as can be seen in the following ways.

1) Illegal states encourage bad code

const handleLoginUser = (userInfo: IsLogin console.log('login user email', userinfo.email); }Copy the code

The product Type exists between the fields of the object. Even if the user is not logged in, there is also an email field, which is an empty string. Developers need to take the initiative, consciously remember to judge whether to log in, otherwise there will be errors.

2) Illegal state leads to overly defensive programming, increasing code complexity and code volume

Const handleLoginUser = (userInfo: userInfo) => {// Defensive programming, check whether login if (! userInfo.isLogin) { return; } console.log('login user email', userInfo.email); } const handleLoginUser1 = (userInfo: userInfo) => {// Defensive programming, check whether the login if (! userInfo.isLogin) { return; } console.log('login user email', userInfo.email); }Copy the code

In all functions or methods that consume UserInfo, you must add defensive logic to manually validate and exclude illegal states before consuming the data.

That is, the complexity savings in defining types add additional defensive code everywhere types are consumed. There is only one type definition of data, but data consumption can be in many places. In contrast, the overall code volume is higher and the code complexity is higher.

3) Illegal state brings more logic out of sync

A lot of defensive judgments are built up little by little over the course of iteration. They are often not closed to common functions at first; they appear repeatedly in multiple functions that consume data. When a defensive judgment needs to be updated, remember to change it all the way through. Any omissions create inconsistencies in the defense logic.

It is for this reason that Don’t Repeat Yourself(DRP) is proposed as a guiding principle of best practice. In this scenario, DRP is a palliative approach, while the palliative approach is to address the source, with more precise type definitions and fewer unnecessary defensive judgments.

4) Illegal state brings worse performance

All functions and methods that consume data require specific defensive logic. Although we can use the DRP principle to keep repeating defensive logic in one place. But they still need to be called in the individual consumption functions.

When these consuming functions call each other, the defensive logic is repeated. Even if the previous function has already been called, the next function still defends. Because it can’t tell if it’s going to be called independently, it needs to get its own defensive logic together.

Therefore, the illegal state leads to worse performance, and the data validation work is computed over and over again during the code run.

4.4 huge benefits of knowledge and code isomorphism

In contrast to illegal states, when types in code more faithfully reflect domain knowledge, illegal states are reduced or eliminated, making them harder to construct and propagate. Domain knowledge is encoded into types and is constrained by type-checker.

It brings great benefits from the following aspects:

1) Reject bad code

UserInfo is a Sum type and there are two possibilities LoginUserInfo and GuestUserInfo. GuestUserInfo complies with the domain rule description and has only one name attribute but no emailInfo attribute.

Therefore, user.email does not pass type checking. In order to access the emailInfo property, you must prove that the user belongs to the login user.

As above, we can access emailInfo when user.type === LoginUserInfo. If user.type === GuestUserInfo, emailInfo cannot be accessed.

When we correctly use Sum type to express the relationship between Or in domain knowledge, it becomes more difficult to write wrong code.

2) Reduce unnecessary defensive logic and its computational overhead

We can construct our function directly using the part of Sum Type that we’re interested in. As shown above, when dealing with a logged-in user, we use the LoginUserInfo type directly and do not have to defend logged-in within the function. If LoginUserInfo exists, you have logged in.

HandleLoginUser1 calls handleLoginUser, and handleLoginUser2 calls handleLoginUser1. None of them have additional defensive logic code.

Sum type shunt judgment is performed only when handleLoginUser2 is called externally. To some extent, defensive code is isolated to the most external function calls. When internal functions are written and composed, the code is shorter, safer, and has less redundant overhead.

3) The code is easier to read and maintain

In contrast to the traditional pattern, domain knowledge is placed in annotations that describe the business implications of the synergy between fields. DDD + ADT domain knowledge is in the type.

We don’t have to guess at the dependencies of User.islogin, user.name, and user.emailinfo on each other and how many possible combinations and scenarios there are.

type EmailInfo = VerifiedEmailInfo | UnverifiedEmailInfo;
type UserInfo = LoginUserInfo | GuestUserInfo;
type JsonResponse = ErrorResponse | DataResponse;
Copy the code

We can see intuitively that two is two, and can be understood correctly without comments. Not only does the developer understand it, but the compiler understands it, and every time it consumes the Sum Type, it construes the developer to remember and misunderstand it.

4.5, summary

  • The relationship of Or in domain knowledge is misinterpreted as And, And the type goes from additive complexity to multiplicative complexity

  • The number of values that can be written on the code is greater than the requirement of true propositions in the domain knowledge

  • The true statements in the code (the extra values) are false statements in the domain, and they become illegal-States.

  • Wherever data is consumed, defensive judgments need to be made to exclude illegal status, otherwise it will lead to bugs in the program

  • The maintainability of the system is inversely proportional to the amount of illegal state leakage in the code base. The more leakage, the harder it is to maintain and predict

Unhealthy code state space, illegal states and side effects randomly distributed. It’s up to developers to put in extra effort, write more comments, write more defensive code… To alleviate the process of code corruption. However, without addressing the root cause of the problem, illegal status can easily spread beyond the control of the development team. In particular, in the process of attrition and turnover, the code base may have deteriorated at an accelerated rate as domain knowledge was lost in the mind of the previous developer and rebuilt in the mind of the next developer.

With DDD + ADT, we can build a healthier code state space that isolates defensive judgments of illegal state layer by layer to the boundary with more precise and domain-aware types. Making our core code simple and reliable, domain knowledge is encoded into types, guaranteed by the compiler’s Type-checker. Even if developers change, the compiler can still do the right thing.

By making illegal states impossible to represent through ADT, code quality is fundamentally optimized.

5. Case: Use DDD + ADT to do data modeling

Assume the following domain rules:

  • User posts have three stages: draft, review and release

  • Drafts cannot be released directly without review

  • Drafts can be submitted for review

  • It can be released after approval

  • Posts under review cannot be modified

  • Review does not pass the return to draft stage

5.1 Common process modeling

class Post { constructor( private isDraft: boolean, private isReviewing: boolean, private isPublished: boolean, private content: string ) {} edit(content: string) { if (! this.isDraft) { throw new Error('Post is not in draft stage'); } this.content = content; } review() { if (! this.isDraft) { throw new Error('Post is not in draft stage'); } this.isDraft = false; this.isReviewing = true; } publish() { if (! this.isReviewing) { throw new Error('Post is not in reviewing stage'); } this.isReviewing = false; this.isPublished = true; } reject() { if (! this.isReviewing) { throw new Error('Post is not in reviewing stage'); } this.isReviewing = false; this.isDraft = true; }}Copy the code

Many developers write the above code logic naturally. When the edit method is called to edit content, it determines whether it is in the draft phase. Within each related method, there is state validation.

The problem is that this approach is not isomorphic to business rules. In business rules, operations such as edit, review, publish, and return do not completely coexist, but change with the draft stage, review stage, and publish stage. However, in Post Class, edit, review, publish, reject and other methods coexist, which is the relationship of product type and does not faithfully reflect domain knowledge.

Therefore, there are a lot of Illegal Operations in the Post instance. Each method call requires the caller to determine the current phase in advance, otherwise calling a method such as Edit will throw an error. Inadvertently forgetting to make defensive judgments ahead of time, bugs will creep into the code base.

When we change the defensive logic in each method from throw Error to silent, we only do the operation when the condition is met, otherwise we do nothing. Then, Illegal Operations become Unexpected Behaviors. That is, with all method calls, we are not sure whether they have utility or not, and often need additional judgment logic to confirm.

The existence of illegal operations, however concealed, continues to corrupt the code base.

5.2 Loyal Process Modeling based on DDD + Class

export class DraftPost { constructor(private content: string) {} edit(content: string) { this.content = content; } review() { return new ReviewingPost(this.content); } } class ReviewingPost { constructor(private content: string) {} publish() { return new PublishedPost(this.content); } reject() { return new DraftPost(this.content); } approve() { return new PublishedPost(this.content); } } class PublishedPost { constructor(private content: string) {} getContent() { return this.content; }}Copy the code

As shown above, we define three classes that represent three phases of Post, and for each phase, the allowed methods correspond precisely to the domain rules. Only DraftPost has the Edit method, which is editable; Only ReviewingPost has a publish method and is publishable.

When we get a DraftPost instance, we can edit it, but we can’t just publish it without auditing it.

When we get an instance of ReviewingPost, we can publish it, but we can’t edit it.

When we acquire an instance of PublishPost, we can neither edit nor republish it.

The flow described by the business rules, which are encoded into the passing of method calls to the Post phases, is constrained by the compiler’s Type-checker.

5.3 Faithful process modeling based on DDD + ADT

type DraftPost = { type: 'DraftPost'; content: string; } type ReviewingPost = { type: 'ReviewingPost'; content: string; } type PublishedPost = { type: 'PublishedPost'; content: string; } const edit = (post: DraftPost, newContent: string): DraftPost => { return { ... post, content: newContent } } const review = (post: DraftPost): ReviewingPost => { return { type: 'ReviewingPost', content: post.content } } const approve = (post: ReviewingPost): PublishedPost => { return { type: 'PublishedPost', content: post.content } } const reject = (post: ReviewingPost): DraftPost => { return { type: 'DraftPost', content: post.content } }Copy the code

In addition to using Class for process modeling, data-oriented ADT can also do this, and the two are equivalent in the expression of process modeling. The difference is that data and behavior are no longer grouped together, but defined separately. But our Edit only accepts DraftPost data, thus indicating that only the draft phase can be edited. Review, approve, reject, and so on.

5.4, summary

  • By putting mutually exclusive operations together And co-existing, the relationship changes from Or to And from additive to multiplicative complexity

  • The number of functions/methods that can be called on the code (terms size) is greater than the actual requirements of true propositions in the domain knowledge

  • True propositions in code (extra method calls) are false propositions in the domain. They become illegal-operations.

  • All calls to the method, need to make defensive judgment, rule out illegal calls, otherwise it may lead to program errors and bugs

  • The maintainability of the system is inversely proportional to the amount of leaks from illegal operations in the code base. The more leaks, the harder it is to maintain and predict

Adopt DDD + ADT model, so that illegal operations can not be called, from the root of the code quality optimization.

6, summary

Both data modeling of user information and process modeling of Post are common business requirements. Their logic is not complicated and has even been simplified in this article. Even so, we can see that most developers’ subconscious code implementations contain a lot of pitfalls. There are many illegal states that are difficult to eliminate, and illegal operations that are difficult to manage, constantly eating into the code base.

As you can imagine, as the project iterates, the continued leakage of illegal states and illegal operations increases the complexity of the project exponentially, making the code base difficult to understand, read, and maintain. Mistakes are encouraged, domain knowledge is written in comments that cannot be run or checked, and performance, logical consistency, and many other metrics of code are not guaranteed.

The DDD + ADT mode can change the status quo from the root and optimize the overall quality of the code base.

  • Use domain driven design (DDD) to establish a common language for the team, obtain reliable domain knowledge, and explore real needs

  • Algebraic data type (ADT) is used to model domain knowledge one-to-one to obtain reliable code design

  • DDD+ADT: Code can be derived from knowledge, knowledge can be derived from code, knowledge and code isomorphism

  • Core skills: Use more Sum type and less Product type to reduce the leakage of illegal status and illegal operation

It not only meets the business needs from the denotation of product function and behavior; From the logic details of the code, it also meets the requirements of business knowledge.

Let’s review our desired code quality improvement model:

  • Objective, all rational developers have the same understanding

  • Clear C

  • Insight, which provides Insight into problems before or during code writing

  • Precise, code evaluation criteria

  • Internal is about the nature of the problem, not just the way the code is written

ADT+DDD mode, better to achieve the above objectives. They are not just a summary of experience in the field of code engineering, but a century of academic accumulation behind them.

In the words of Phillip Wadle, ADT was not invented, it was discovered. The programming languages most developers use and their features are primarily human inventions. Now I invite you to use the features of language that humans have discovered.

  • In 1874, Cantor established set theory

  • In 1901, Russell discovered Russell’s paradox in set theory

  • In 1903, Russell tried to overcome the paradox by using the idea of type, which is regarded as one of the origins of type theory

  • From 1934 to 1969, Currie and Howard respectively discovered the implicit correspondence between type theory and logical deduction system

  • 1936 Alan Turing published his model of the Turing machine

  • In 1972, the C language was born

  • From 1977 to 1980, algebraic data types (ADT) appeared in functional programming languages

  • In 1995, the front-end JavaScript language was born

  • In 2000, SOLID Principles was published

  • In 2003, domain-driven Design (DDD) was proposed

  • In 2012, the front-end TypeScript language (the sample language for this article) was born

  • In 2015, version 1.0 of the Rust language was released

  • 2022, today, the date of publication of our article.

Each line of DDD + ADT modeling code is loaded with the history and shines with the brilliance of human rationality.