Net Architecture for Web Applications

I want to share with you an approach that I have been using for many years in developing applications, including web applications. Many developers of desktop, server, and mobile applications are familiar with this approach. is fundamental when building such applications, however, it is represented very poorly on the web, although there are definitely people who want to use this approach. In addition, the VS Code editor is written on this approach .

Pure Architecture

As a result of applying this approach, you will get rid of a specific framework. You can easily switch the view library inside your application, for example React, Preact, Vue, Mithril without rewriting business logic, and in most cases even views. If you have an application on Angular 1, you can easily translate it to Angular 2+, React, Svelte, WebComponents, or even your presentation library. If you have an application on Angular 2+, but there are no specialists for it, then you can easily transfer the application to a more popular library without rewriting business logic. But in the end, completely forget about the problem of migration from the framework to the framework. What kind of magic is this?

What is Clean Architecture


In order to understand this, it is best to read the book of Martin Robert "Clean Architecture» ( by Robert C.Martin "Clean Architecture» ). A brief excerpt from which is given in the article by reference .

The main ideas embedded in architecture:

  1. Independence from the framework. Architecture does not depend on the existence of any library. This allows you to use the framework as a tool, instead of squeezing your system into its limitations.
  2. Testability. Business rules can be tested without a user interface, database, web server, or any other external component.
  3. Independence from UI. The user interface can be easily changed without changing the rest of the system. For example, the web interface can be replaced by the console, without changing business rules.
  4. Independence from the database. You can swap Oracle or SQL Server for MongoDB, BigTable, CouchDB or something else. Your business rules are not database related.
  5. Independence from any external service. In fact, your business rules simply do not know anything about the outside world.

The ideas described in this book for many years have been the basis for building complex applications in various fields.

This flexibility is achieved by dividing the application into Service, Repository, Model layers. I added the MVC approach to Clean Architecture and got the following layers:

  • View - displays data to the client, actually visualizes the state of the logic to the client.
  • Controller - is responsible for interacting with the user through IO (input-output).
  • Service - is responsible for business logic and its reuse between components.
  • Repository - responsible for receiving data from external sources, such as a database, api, local storage, etc.
  • Models - is responsible for transferring data between layers and systems, as well as for the logic of processing this data.

The purpose of each layer is discussed below.

Who is Pure Architecture


Web development has come a long way, from simple jquery scripting to developing large SPA applications. And now web applications have become so large that the amount of business logic has become comparable or even superior to server, desktop and mobile applications.

For developers who write complex and large applications, as well as transfer business logic from the server to web applications to save on the cost of servers, Pure Architecture will help organize the code and scale without problems to a huge scale.

At the same time, if your task is just layout and animation of landing pages, then Clean Architecture simply has nowhere to insert. If your business logic is on the backend and your task is to get the data, display it to the client and process the click on the button, then you will not feel the flexibility of Clean Architecture, but it can be an excellent springboard for the explosive growth of the application.

Where already applied?


Pure architecture is not tied to any particular framework, platform, or programming language. For decades, it has been used to write desktop applications. Its reference implementation can be found in the frameworks for server applications Asp.Net Core, Java Spring and NestJS. It is also very popular when writing iOs and Android applications. But in web development, he appeared in an extremely unsuccessful form in the Angular frameworks.

Since I myself am not only Typescript, but also a C # developer, for example I will take the reference implementation of this architecture for Asp.Net Core.

Here is a simplified sample application:

Sample Application on Asp.Net Core
    /**
     * View
     */

    @model WebApplication1.Controllers.Profile

    <div class="text-center">
        <h1>  @Model.FirstName</h1>
    </div>

    /**
     * Controller
     */

    public class IndexController : Controller
    {
        private static int _counter = 0;
        private readonly IUserProfileService _userProfileService;

        public IndexController(IUserProfileService userProfileService)
        {
            _userProfileService = userProfileService;
        }

        public async Task<IActionResult> Index()
        {
            var profile = await this._userProfileService.GetProfile(_counter);
            return View("Index", profile);
        }

        public async Task<IActionResult> AddCounter()
        {
            _counter += 1;
            var profile = await this._userProfileService.GetProfile(_counter);
            return View("Index", profile);
        }
    }

    /**
     * Service
     */

    public interface IUserProfileService
    {
        Task<Profile> GetProfile(long id);
    }

    public class UserProfileService : IUserProfileService
    {
        private readonly IUserProfileRepository _userProfileRepository;

        public UserProfileService(IUserProfileRepository userProfileRepository)
        {
            this._userProfileRepository = userProfileRepository;
        }

        public async Task<Profile> GetProfile(long id)
        {
            return await this._userProfileRepository.GetProfile(id);
        }
    }

    /**
     * Repository
     */

    public interface IUserProfileRepository
    {
        Task<Profile> GetProfile(long id);
    }

    public class UserProfileRepository : IUserProfileRepository
    {
        private readonly DBContext _dbContext;
        public UserProfileRepository(DBContext dbContext)
        {
            this._dbContext = dbContext;
        }

        public async Task<Profile> GetProfile(long id)
        {
            return await this._dbContext
                .Set<Profile>()
                .FirstOrDefaultAsync((entity) => entity.Id.Equals(id));
        }
    }

    /**
     * Model
     */

    public class Profile
    {
        public long Id { get; set; }
        public string FirstName { get; set; }
        public string Birthdate { get; set; }
    }


If you don’t understand that it says nothing bad, then we will analyze it in parts each fragment.

An example is given for an Asp.Net Core application, but for Java Spring, WinForms, Android, React the architecture and code will be the same, only the language and work with the view (if any) will change.

Web Application


The only framework that tried to use Clean Architecture was Angular. But it turned out just awful, that in 1, that in 2+.

And there are many reasons for this:

  1. Angular monolithic framework. And this is his main problem. If you don’t like something in it, you have to choke on it daily, and there’s nothing you can do about it. Not only is there a lot of problem places in it, it also contradicts the ideology of pure architecture.
  2. DI. , Javascript.
  3. . JSX. , 6, . .
  4. . Rollup ES2015 2 4 , angular 9.
  5. And many more problems. In general, up to the angular modern technology rolls with a delay of 5 years relative to React.

But what about other frameworks? React, Vue, Preact, Mithril and others are exclusively presentation libraries and do not provide any architecture ... but we already have the architecture ... it remains to assemble everything into a single whole!

We start to create an application


We will consider Pure Architecture by the example of a fictitious application that is as close as possible to a real web application. This is an office in the insurance company that displays the user profile, insured events, proposed insurance rates and tools for working with this data.

Application prototype

In the example, only a small part of the functional will be implemented, but from it you can understand where and how to position the rest of the functional. Let's start creating the application from the Controller layer, and connect the View layer at the very end. And in the course of creation, we consider each layer in more detail.

Controller Pattern


Controller - is responsible for user interaction with the application. It can be a click on a button on a web page, a desktop application, a mobile application, or entering a command in a Linux console, or a network request, or any other IO event coming into the application.

The simplest controller in a clean architecture is as follows:

export class SimpleController { // extends React.Component<object, object>

    public todos: string[] = []; //  

    public addTodo(todo: string): void { //    
        this.todos.push(todo);
    }

    public removeTodo(index: number): void { //    
        this.todos.splice(index, 1);
    }

    // public render(): JSX.Element {...} // view injection

}

Its task is to receive an event from the user and start business processes. In the ideal case, the Controller does not know anything about View, and then it can be reused between platforms, such as Web, React-Native or Electron.

Now let's write a controller for our application. Its task is to obtain a user profile, available tariffs and offer the best tariff to the user:

UserPageController. Controller with business logic
export class UserPageControlle {

    public userProfile: any = {};
    public insuranceCases: any[] = [];
    public tariffs: any[] = [];
    public bestTariff: any = {};

    constructor() {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> { //  
        try {
            const response = await fetch("./api/user-profile");
            this.userProfile = await response.json();
            this.findBestTariff();
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> { //  
        try {
            const response = await fetch("./api/tariffs");
            this.tariffs = await response.json();
            this.findBestTariff();
        } catch (e) {
            console.error(e);
        }
    }

    public findBestTariff(): void { //    
        if (this.userProfile && this.tariffs) {
            this.bestTariff = this.tariffs.find((tarif: any) => {
                return tarif.ageFrom <= this.userProfile.age && this.userProfile.age < tarif.ageTo;
            });
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}


We got a regular controller without a clean architecture, if we inherit it from React.Component we get a working component with logic. So many web application developers write, but this approach has many significant drawbacks. The main one is the inability to reuse logic between components. After all, the recommended tariff can be displayed not only in your personal account, but also on the landing page and many other places to attract a client to the service.

In order to be able to reuse the logic between components it is necessary to place it in a special layer called Service.

Service Pattern


Service - responsible for the entire business logic of the application. If the Controller needed to receive, process, send some data - it does this through the Service. If multiple controllers need the same logic, they work with Service. But the Service layer itself should not know anything about the Controller and View layer and the environment in which it works.

Let's move the logic from the controller to the service and implement the service in the controller:

UserPageController. Controller without business logic
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";

export class UserPageController {

    public userProfile: any = {};
    public insuranceCases: any[] = [];
    public tariffs: any[] = [];
    public bestTariff: any = {};

    //    
    private readonly userProfilService: UserProfilService = new UserProfilService();
    private readonly tarifService: TariffService = new TariffService();

    constructor() {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            //     
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            //     
            this.tariffs = await this.tarifService.getTariffs();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

UserProfilService. Service for working with user profile
export class UserProfilService {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TariffService. Service for working with tariffs
export class TariffService {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    //    
    public async findBestTariff(userProfile: any): Promise<any> {
        const tariffs = await this.getTariffs();
        return tariffs.find((tarif: any) => {
            return tarif.ageFrom <= userProfile.age &&
                userProfile.age < tarif.ageTo;
        });
    }
    
    /**
     * ...       
     */
}


Now, if several controllers need to get a user profile or tariffs, they can reuse the same logic from the services. In services, the main thing is not to forget about SOLID principles and that each service is responsible for its area of ​​responsibility. In this case, one service is responsible for working with the user profile, and another service is responsible for working with tariffs.

But what if the data source changes, for example, fetch can change to websocket or grps or the database, and the real data needs to be replaced with test data? And in general, why does business logic need to know something about a data source? To solve these problems, there is a Repository layer.

Repository Pattern


Repository - responsible for communication with the data warehouse. The storage can be a server, database, memory, localstorage, sessionstorage, or any other storage. Its task is to abstract the Service layer from the specific storage implementation.

Let's make network requests from services in the repository, while the controller does not change:
UserProfilService. Service for working with user profile
import { UserProfilRepository } from "./UserProfilRepository";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

    public async getUserProfile(): Promise<any> {
        return await this.userProfilRepository.getUserProfile();
    }
    
    /**
     * ...        
     */
}

UserProfilRepository. Service for working with user profile storage
export class UserProfilRepository {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TariffService. Service for working with tariffs
import { TariffRepository } from "./TariffRepository";

export class TariffService {
    
    private readonly tarifRepository: TariffRepository = new TariffRepository();

    public async getTariffs(): Promise<any> {
        return await this.tarifRepository.getTariffs();
    }

    //    
    public async findBestTariff(userProfile: any): Promise<any> {
        //    
        const tariffs = await this.tarifRepository.getTariffs();
        return tariffs.find((tarif: any) => {
            return tarif.ageFrom <= userProfile.age &&
                userProfile.age < tarif.ageTo;
        });
    }
    
    /**
     * ...       
     */
}

TariffRepository. Repository for working with tariff storage
export class TariffRepository {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}


Now it’s enough to write a data request once and any service will be able to reuse this request. Later we will look at an example of how to redefine the repository without touching the service code and implement the mocha repository for testing.

In the UserProfilService service, it may seem that it is not needed and the controller can directly access the repository for data, but this is not so. At any time, requirements may appear or change in the business layer, an additional request may be required, or data may be enriched. Therefore, even when there is no logic in the service layer, the Controller - Service - Repository chain must be preserved. This is a contribution to your tomorrow.

It is time to figure out what kind of repository gets set, whether they are correct at all. The Models layer is responsible for this.

Models: DTO, Entities, ViewModels


Models - is responsible for the description of the structures with which the application works. Such a description greatly helps new project developers to understand what the application is working with. In addition, it is very convenient to use it for building databases or validating data stored in the model.

Models are divided into different patterns depending on the type of use:
  • Entities - are responsible for working with the database and are a structure repeating a table or document in the database.
  • DTO (Data Transfer Object) - are used to transfer data between different layers of the application.
  • ViewModel - contain pre-prepared information necessary for display in the view.


Add the user profile model and other models to the application, and let the other layers know that now we are working not with an abstract object, but with a very specific profile:
UserPageController. Instead of any, the described models are used.
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";

export class UserPageController {

    /**
     *          
     *          .
     */
    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    private readonly userProfilService: UserProfilService = new UserProfilService();
    private readonly tarifService: TariffService = new TariffService();

    constructor() {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

UserProfilService. Instead of any, specify the returned model
import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

    public async getUserProfile(): Promise<UserProfileDto> { //  
        return await this.userProfilRepository.getUserProfile();
    }
    
    /**
     * ...        
     */
}

TariffService. Instead of any, specify the returned model
import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";

export class TariffService {
    
    private readonly tarifRepository: TariffRepository = new TariffRepository();

    public async getTariffs(): Promise<TariffDto[]> { //  
        return await this.tarifRepository.requestTariffs();
    }

    //  
    public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
        const tariffs = await this.tarifRepository.requestTariffs();
        return tariffs.find((tarif: TariffDto) => {
            //  userProfile.age  userProfile.getAge()
            const age = userProfile.getAge();
            return age &&
                tarif.ageFrom <= age &&
                age < tarif.ageTo;
        });
    }
    
    /**
     * ...       
     */
}

UserProfilRepository. Instead of any, specify the returned model
import { UserProfileDto } from "./UserProfileDto";

export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

TariffRepository. Instead of any, specify the returned model
import { TariffDto } from "./TariffDto";

export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> { //  
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}

UserProfileDto. A model with a description of the data we work with
export class UserProfileDto { // <--      
    public firstName: string | null = null;
    public lastName: string | null = null;
    public birthdate: Date | null = null;

    public getAge(): number | null {
        if (this.birthdate) {
            const ageDifMs = Date.now() - this.birthdate.getTime();
            const ageDate = new Date(ageDifMs);
            return Math.abs(ageDate.getUTCFullYear() - 1970);
        }
        return null;
    }

    public getFullname(): string | null {
        return [
            this.firstName ?? "",
            this.lastName ?? ""
        ]
            .join(" ")
            .trim() || null;
    }

}

TariffDto. A model with a description of the data we work with
export class TariffDto {
    public ageFrom: number = 0;
    public ageTo: number = 0;
    public price: number = 0;
}


Now no matter what layer of the application we are in, we know exactly what data we are working with. Also, due to the description of the model, we found an error in our service. In the service logic, the userProfile.age property was used, which actually does not exist, but has a date of birth. And to calculate the age, you must call the userProfile.getAge () model method.

But there is one problem. If we try to use the methods from the model that the current repository provided, we will get an exception. The thing is that the response.json () and JSON.parse () methodsIt returns not our model, but a JSON object, which is in no way associated with our model. You can verify this if you execute the command userProfile instanceof UserProfileDto, you get a false statement. In order to convert the data received from an external source to the described model, there is a process of data deserialization.

Data deserialization


Deserialization - the process of restoring the necessary structure from a sequence of bytes. If the data contains information not specified in the models, it will be ignored. If there is information in the data that contradicts the description of the model, a deserialization error will occur.

And the most interesting thing here is that when designing the ES2015 and adding the class keyword, they forgot to add deserialization ... The fact that in all languages ​​is out of the box, in ES2015 they simply forgot ...

To solve this problem, I wrote a library for TS-Serializable deserialization , an article about which can be read on this link . The purpose of which is to return the lost functionality.

Add deserialization support in the model and deserialization itself to the repository:
TariffRepository. Add a deserialization process
import { UserProfileDto } from "./UserProfileDto";

export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> {
        const response = await fetch("./api/user-profile");
        const object = await response.json();
        return new UserProfileDto().fromJSON(object); //  
    }
    
    /**
     * ...        
     */
}

TariffRepository. Add a deserialization process
import { TariffDto } from "./TariffDto";

export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> { //  
        const response = await fetch("./api/tariffs");
        const objects: object[] = await response.json();
        return objects.map((object: object) => {
            return new TariffDto().fromJSON(object); //  
        });
    }

    /**
     * ...        
     */
}

ProfileDto. Adding Deserialization Support
import { Serializable, jsonProperty } from "ts-serializable";

export class UserProfileDto extends Serializable { // <--    

    @jsonProperty(String, null) // <--  
    public firstName: string | null = null;

    @jsonProperty(String, null) // <--  
    public lastName: string | null = null;

    @jsonProperty(Date, null) // <--  
    public birthdate: Date | null = null;

    public getAge(): number | null {
        if (this.birthdate) {
            const ageDifMs = Date.now() - this.birthdate.getTime();
            const ageDate = new Date(ageDifMs);
            return Math.abs(ageDate.getUTCFullYear() - 1970);
        }
        return null;
    }

    public getFullname(): string | null {
        return [
            this.firstName ?? "",
            this.lastName ?? ""
        ]
            .join(" ")
            .trim() || null;
    }

}

TariffDto. Adding Deserialization Support
import { Serializable, jsonProperty } from "ts-serializable";

export class TariffDto extends Serializable { // <--    

    @jsonProperty(Number, null) // <--  
    public ageFrom: number = 0;

    @jsonProperty(Number, null) // <--  
    public ageTo: number = 0;

    @jsonProperty(Number, null) // <--  
    public price: number = 0;

}


Now in all layers of the application, you can be absolutely sure that we are working with the models that we expect. In the view, controller, and other layers, you can call the methods of the described model.

What are Serializable and jsonProperty for?
Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .

Now we have an almost finished application. It is time to test the logic written in the Controller, Service, and Models layers. To do this, we need to return specially prepared test data in the Repository layer instead of a real request to the server. But how to replace the Repository without touching the code that goes into production. There is a Dependency Injection pattern for this.

Dependency Injection - Dependency Injection


Dependency Injection - injects dependencies into the Contoller, Service, Repository layers and allows you to override these dependencies outside of these layers.

In the program, the Controller layer depends on the Service layer, and it depends on the Repository layer. In the current form, layers themselves cause their dependencies through instantiation. And in order to redefine the dependence, the layer needs to set this dependence from the outside. There are many ways to do this, but the most popular is passing the dependency as a parameter in the constructor.

Then creating a program with all the dependencies will look like this:
var programm = new IndexPageController(new ProfileService(new ProfileRepository()));

Agree - it looks awful. Even taking into account that there are only two dependencies in the program, it already looks awful. What to say about programs in which hundreds and thousands of dependencies.

To solve the problem, you need a special tool, but for this you need to find it. If we turn to the experience of other platforms, for example, Asp.Net Core, then there the registration of dependencies occurs at the initialization stage of the program and looks something like this:
DI.register(IProfileService,ProfileService);

and then, when creating the controller, the framework itself will create and implement this dependency.

But there are three significant problems:
  1. When transpiling Typescript in Javascript, there is no trace left of the interfaces.
  2. Everything that fell into the classic DI remains in it forever. It is very difficult to clean it during refactoring. And in a web application, you need to save every byte.
  3. Almost all view libraries do not use DI, and controller designers are busy with parameters.


In web applications, DI is used only in Angular 2+. In Angular 1, when registering dependencies, instead of an interface, a string was used; in InversifyJS, Symbol is used instead of the interface. And all this is implemented so terribly that it’s better to have a lot of new as in the first example of this section than these solutions.

To solve all three problems, my own DI was invented, and the solution for it helped me find the Java Spring framework and its autowired decorator. The description of how this DI works can be found in the article at the link , and the GitHub repository .

It is time to apply the resulting DI in our application.

Putting it all together


To implement DI on all layers, we will add a reflection decorator, which will cause typescript to generate additional meta-information about dependency types. In the controller where you need to call the dependencies, we will hang the autowired decorator. And in the place where the program is initialized, we determine in which environment which dependency will be implemented.

For the UserProfilRepository repository, create the same repository, but with test data instead of the actual request. As a result, we get the following code:
Main.ts. Program Initialization Location
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";

if (process.env.NODE_ENV === "test") {
    //         
    override(UserProfilRepository, MockUserProfilRepository);
}

UserPageController. Dependence through the autowired decorator
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";

export class UserPageController {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    @autowired() //  
    private readonly userProfilService!: UserProfilService;

    @autowired() //  
    private readonly tarifService!: TariffService;

    constructor() {
        //     , ..  
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

UserProfilService. Introducing reflection and dependency generation
import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository;

    constructor(userProfilRepository: UserProfilRepository) {
        //    
        this.userProfilRepository = userProfilRepository;
    }

    public async getUserProfile(): Promise<UserProfileDto> {
        return await this.userProfilRepository.getUserProfile();
    }

    /**
     * ...        
     */
}

TariffService. Introducing reflection and dependency generation
import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class TariffService {

    private readonly tarifRepository: TariffRepository;

    constructor(tarifRepository: TariffRepository) {
        //    
        this.tarifRepository = tarifRepository;
    }

    public async getTariffs(): Promise<TariffDto[]> {
        return await this.tarifRepository.requestTariffs();
    }

    public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
        const tariffs = await this.tarifRepository.requestTariffs();
        return tariffs.find((tarif: TariffDto) => {
            const age = userProfile.getAge();
            return age &&
                tarif.ageFrom <= age &&
                age < tarif.ageTo;
        });
    }

    /**
     * ...       
     */
}


UserProfilRepository. Introducing reflection generation
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> {
        const response = await fetch("./api/user-profile");
        const object = await response.json();
        return new UserProfileDto().fromJSON(object);
    }

    /**
     * ...        
     */
}

MockUserProfilRepository. New repository for testing
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class MockUserProfilRepository { //   
    public async getUserProfile(): Promise<UserProfileDto> {
        const profile = new UserProfileDto();
        profile.firstName = "";
        profile.lastName = "";
        profile.birthdate = new Date(Date.now() - 1.5e12);
        return Promise.resolve(profile); //   
    }

    /**
     * ...        
     */
}

TariffRepository. Introducing reflection generation
import { TariffDto } from "./TariffDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> {
        const response = await fetch("./api/tariffs");
        const objects: object[] = await response.json();
        return objects.map((object: object) => {
            return new TariffDto().fromJSON(object);
        });
    }

    /**
     * ...        
     */
}


Now, anywhere in the program there is an opportunity to change the implementation of any logic. In our example, instead of a real user profile request to the server in the test environment, test data will be used.

In real life, replacements can be found anywhere, for example, you can change the logic in a service, implementing the old service in production, and in the refactoring it is already new. Conduct A / B tests with business logic, change the document-based database to relational, and generally change the network request to web sockets. And all this without stopping the development to rewrite the solution.

It is time to see the result of the program. There is a View layer for this.

Implementing View


The View layer is responsible for presenting the data that is contained in the Controller layer to the user. In the example, I will use React for this, but in its place can be any other, for example Preact, Svelte, Vue, Mithril, WebComponent or any other.

To do this, simply inherit our controller from React.Component, and add a render method to it with the representation of the view:

Main.ts. Starts drawing a React component
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
import { UserPageController } from "./UserPageController";
import React from "react";
import { render } from "react-dom";

if (process.env.NODE_ENV === "test") {
    //         
    override(UserProfilRepository, MockUserProfilRepository);
}

render(React.createElement(UserPageController), document.body);

UserPageController. Inherits from React.Component and adds a render method
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import React from "react";

//    React.Component
export class UserPageController extends React.Component<object, object> {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    @autowired()
    private readonly userProfilService!: UserProfilService;

    @autowired()
    private readonly tarifService!: TariffService;

    //   
    constructor(props: object, context: object) {
        super(props, context);
    }

    //     
    public componentDidMount(): void {
        this.activate();
    }

    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            this.forceUpdate(); //  view   
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
            this.forceUpdate(); //  view   
        } catch (e) {
            console.error(e);
        }
    }

    //   view
    public render(): JSX.Element {
        return (
            <>
                <div className="user">
                    <div className="user-name">
                         : {this.userProfile.getFullname()}
                    </div>
                    <div className="user-age">
                        : {this.userProfile.getAge()}
                    </div>
                </div>
                <div className="tarifs">
                    {/*    */}
                </div>
            </>
        );
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}


By adding just two lines and a presentation template, our controller turned into a react component with working logic.

Why is forceUpdate called instead of setState?
forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .

But even in such an implementation, it turned out that we tied to the React library with its life cycle, view implementation, and the principle of invalidating the view, which contradicts the concept of Clean Architecture. And the logic and layout are in the same file, which complicates the parallel work of the typesetter and developer.

Separation of Controller and View


To solve both problems, we put the view layer into a separate file, and instead of the React component, we make the base component that will abstract our controller from a specific presentation library. At the same time, we describe the attributes that the component can take.

We get the following changes:
UserPageView. They took the view to a separate file
import { UserPageOptions, UserPageController } from "./UserPageController";
import React from "react";

export const userPageView = <P extends UserPageOptions, S>(
    ctrl: UserPageController<P, S>,
    props: P
): JSX.Element => (
    <>
        <div className="user">
            <div className="user-name">
                 : {ctrl.userProfile.getFullname()}
            </div>
            <div className="user-age">
                : {ctrl.userProfile.getAge()}
            </div>
        </div>
        <div className="tarifs">
            {/*    */}
        </div>
    </>
);

UserPageOptions. Take view and React to a separate file
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import { BaseComponent } from "./BaseComponent";
import { userPageView } from "./UserPageview";

export interface UserPageOptions {
    param1?: number;
    param2?: string;
}

//    BaseComponent
export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    //  
    public readonly view = userPageView;

    @autowired()
    private readonly userProfilService!: UserProfilService;

    @autowired()
    private readonly tarifService!: TariffService;

    //  
    constructor(props: P, context: S) {
        super(props, context);
    }

    //   componentDidMount, . BaseComponent
    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

BaseComponent A component that abstracts us from a particular framework
import React from "react";

export class BaseComponent<P, S> extends React.Component<P, S> {

    //  view
    public view?: (ctrl: this, props: P) => JSX.Element;

    constructor(props: P, context: S) {
        super(props, context);
        //    
    }

    //      
    public componentDidMount(): void {
        this.activate && this.activate();
    }

    //      
    public shouldComponentUpdate(
        nextProps: Readonly<P>,
        nextState: Readonly<S>,
        nextContext: any
    ): boolean {
        return this.update(nextProps, nextState, nextContext);
    }

    public componentWillUnmount(): void {
        this.dispose();
    }

    public activate(): void {
        //   
    }

    public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
        //   
        return false;
    }

    public dispose(): void {
        //   
    }

    //   view
    public render(): React.ReactElement<object> {
        if (this.view) {
            return this.view(this, this.props);
        } else {
            return React.createElement("div", {}, "  ");
        }
    }

}


Now our view is in a separate file, and the controller knows nothing about it, except that it is. Perhaps you should not inject the view through the controller property, but do it through the decorator as Angular does, but this is a topic for some thought.

The basic component also contains an abstraction from the life cycle of the framework. They are different in all frameworks, but they are in all frameworks. Angular is ngOnInit, ngOnChanges, ngOnDestroy. In React and Preact, this is componentDidMount, shouldComponentUpdate, componentWillUnmount. In Vue, this is created, updated, destroyed. In Mithril it is oncreate, onupdate, onremove. In WebComponents, this is connectedCallback, attributeChangedCallback, disconnectedCallback. And so in every library. Most even have the same or similar interface.

In addition, now the library components can be expanded with their own logic for subsequent reuse between all components. For example, introducing tools for analytics, monitoring, logging, etc.

We look at the result


It remains only to evaluate what happened. The whole program has the following final form:
Main.ts. The file from which the program is launched
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
import { UserPageController } from "./UserPageController";
import React from "react";
import { render } from "react-dom";

if (process.env.NODE_ENV === "test") {
    override(UserProfilRepository, MockUserProfilRepository);
}

render(React.createElement(UserPageController), document.body);

UserPageView. Representation of one of the program components.
import { UserPageOptions, UserPageController } from "./UserPageController";
import React from "react";

export const userPageView = <P extends UserPageOptions, S>(
    ctrl: UserPageController<P, S>,
    props: P
): JSX.Element => (
    <>
        <div className="user">
            <div className="user-name">
                 : {ctrl.userProfile.getFullname()}
            </div>
            <div className="user-age">
                : {ctrl.userProfile.getAge()}
            </div>
        </div>
        <div className="tarifs">
            {/*    */}
        </div>
    </>
);

UserPageController. The logic of one of the components for user interaction
import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import { BaseComponent } from "./BaseComponent";
import { userPageView } from "./UserPageview";

export interface UserPageOptions {
    param1?: number;
    param2?: string;
}

export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {

    public userProfile: UserProfileDto = new UserProfileDto();
    public insuranceCases: InsuranceCaseDto[] = [];
    public tariffs: TariffDto[] = [];
    public bestTariff: TariffDto | void = void 0;

    public readonly view = userPageView;

    @autowired()
    private readonly userProfilService!: UserProfilService;

    @autowired()
    private readonly tarifService!: TariffService;

    //   componentDidMount, . BaseComponent
    public activate(): void {
        this.requestUserProfile();
        this.requestTariffs();
    }

    public async requestUserProfile(): Promise<void> {
        try {
            this.userProfile = await this.userProfilService.getUserProfile();
            this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    public async requestTariffs(): Promise<void> {
        try {
            this.tariffs = await this.tarifService.getTariffs();
            this.forceUpdate();
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * ...   ,   ,
     *  ,    
     */
}

BaseComponent Base class for all program components
import React from "react";

export class BaseComponent<P, S> extends React.Component<P, S> {

    public view?: (ctrl: this, props: P) => JSX.Element;

    constructor(props: P, context: S) {
        super(props, context);
    }

    public componentDidMount(): void {
        this.activate && this.activate();
    }

    public shouldComponentUpdate(
        nextProps: Readonly<P>,
        nextState: Readonly<S>,
        nextContext: any
    ): boolean {
        return this.update(nextProps, nextState, nextContext);
    }

    public componentWillUnmount(): void {
        this.dispose();
    }

    public activate(): void {
        //   
    }

    public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
        //   
        return false;
    }

    public dispose(): void {
        //   
    }

    public render(): React.ReactElement<object> {
        if (this.view) {
            return this.view(this, this.props);
        } else {
            return React.createElement("div", {}, "  ");
        }
    }

}

UserProfilService. Service for reusing logic between components to work with a user profile
import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection
export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository;

    constructor(userProfilRepository: UserProfilRepository) {
        this.userProfilRepository = userProfilRepository;
    }

    public async getUserProfile(): Promise<UserProfileDto> {
        return await this.userProfilRepository.getUserProfile();
    }

    /**
     * ...        
     */
}

TariffService. Service for reusing logic between components for working with tariffs
import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection
export class TariffService {

    private readonly tarifRepository: TariffRepository;

    constructor(tarifRepository: TariffRepository) {
        this.tarifRepository = tarifRepository;
    }

    public async getTariffs(): Promise<TariffDto[]> {
        return await this.tarifRepository.requestTariffs();
    }

    public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
        const tariffs = await this.tarifRepository.requestTariffs();
        return tariffs.find((tarif: TariffDto) => {
            const age = userProfile.getAge();
            return age &&
                tarif.ageFrom <= age &&
                age < tarif.ageTo;
        });
    }

    /**
     * ...       
     */
}

UserProfilRepository. Repository for receiving a profile from the server, checking and validating it
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection
export class UserProfilRepository {
    public async getUserProfile(): Promise<UserProfileDto> {
        const response = await fetch("./api/user-profile");
        const object = await response.json();
        return new UserProfileDto().fromJSON(object);
    }

    /**
     * ...        
     */
}

MockUserProfilRepository.
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class MockUserProfilRepository { //   
    public async getUserProfile(): Promise<UserProfileDto> {
        const profile = new UserProfileDto();
        profile.firstName = "";
        profile.lastName = "";
        profile.birthdate = new Date(Date.now() - 1.5e12);
        return Promise.resolve(profile); //   
    }

    /**
     * ...        
     */
}

TariffRepository. ,
import { TariffDto } from "./TariffDto";
import { reflection } from "first-di";

@reflection //  typescript  
export class TariffRepository {
    public async requestTariffs(): Promise<TariffDto[]> {
        const response = await fetch("./api/tariffs");
        const objects: object[] = await response.json();
        return objects.map((object: object) => {
            return new TariffDto().fromJSON(object);
        });
    }

    /**
     * ...        
     */
}

UserProfileDto.
import { Serializable, jsonProperty } from "ts-serializable";

export class UserProfileDto extends Serializable {

    @jsonProperty(String, null)
    public firstName: string | null = null;

    @jsonProperty(String, null)
    public lastName: string | null = null;

    @jsonProperty(Date, null)
    public birthdate: Date | null = null;

    public getAge(): number | null {
        if (this.birthdate) {
            const ageDifMs = Date.now() - this.birthdate.getTime();
            const ageDate = new Date(ageDifMs);
            return Math.abs(ageDate.getUTCFullYear() - 1970);
        }
        return null;
    }

    public getFullname(): string | null {
        return [
            this.firstName ?? "",
            this.lastName ?? ""
        ]
            .join(" ")
            .trim() || null;
    }

}

TariffDto.
import { Serializable, jsonProperty } from "ts-serializable";

export class TariffDto extends Serializable {

    @jsonProperty(Number, null)
    public ageFrom: number = 0;

    @jsonProperty(Number, null)
    public ageTo: number = 0;

    @jsonProperty(Number, null)
    public price: number = 0;

}


As a result, we got a modular scalable application with a very small amount of boilerplate (1 basic component, 3 lines for implementing dependencies per class) and very low overhead (actually only for implementing dependencies, everything else is logic). Also, we are not tied to any presentation library. When Angular 1 died, many started rewriting applications in React. When the developers for Angular 2 ran out, many companies began to suffer because of the speed of development. When React dies again, you will have to rewrite the solutions tied to its framework and ecosystem. But with Chita Architecture you can forget about tying the framework.

What is the advantage over Redux?


In order to understand the difference, let's see how Redux behaves when the application grows.
Redux

As you can see from the diagram, with the growth of Redux applications scaled vertically, the Store and the number of Reducers also increases and turns into a bottleneck. And the amount of overhead for rebuilding the Store and finding the right Reducer is starting to exceed the payload.

You can check the ratio of overhead to payload on a medium-sized application with a simple test.
let create = new Function([
"return {",
...new Array(100).fill(1).map((val, ind) => `param${ind}:${ind},`),
"}"
].join(""));

let obj1 = create();

console.time("store recreation time");
let obj2 = {
    ...obj1,
    param100: 100 ** 2
}
console.timeEnd("store recreation time");

console.time("clear logic");
let val = 100 ** 2;
console.timeEnd("clear logic");

console.log(obj2, val);

// store recreation time: 0.041015625ms
// clear logic: 0.0048828125ms

To recreate the Store in 100 properties took 8 times more time than the logic itself. With 1000 elements, this is already 50 times more. In addition, one user action can spawn a whole chain of actions, the call of which is difficult to catch and debug. You can certainly argue that 0.04 ms to recreate the Store is very small and will not slow down. But 0.04 ms is on the Core i7 processor and for one action. Given the weaker mobile processors and the fact that a single user action can spawn dozens of actions, all this leads to the fact that the calculations do not fit into 16 ms and a feeling is created that the application is slowing down.

Let's compare how the Clean Architecture application grows:
Pure Architecture

As can be seen due to the separation of logic and responsibilities between the layers, the application scales horizontally. If any component needs to process the data, it will turn to the corresponding service without touching the services not related to its task. After receiving the data, only one component will be redrawn. The data processing chain is very short and obvious, and for a maximum of 4 code jumps, you can find the necessary logic and debug it. In addition, if we draw an analogy with a brick wall, then we can remove any brick from this wall and replace it with another without affecting the stability of this wall.

Bonus 1: Enhanced functionality of the components of the framework


A bonus was the ability to complement or change the behavior of the components of the presentation library. For example, a reaction in case of an error in the view does not render the entire application, if a little revision is done:
export class BaseComponent<P, S> extends React.Component<P, S> {

    ...

    public render(): React.ReactElement<object> {
        try {
            if (this.view) {
                return this.view(this, this.props);
            } else {
                return React.createElement("div", {}, "  ");
            }
        } catch (e) {
            return React.createElement(
                "div",
                { style: { color: "red" } },
                `    : ${e}`
            );
        }
    }

}

Now the reaction will not draw only the component in which the error occurred. In the same simple way, you can add monitoring, analytics and other nishtyaki.

Bonus 2: Data Validation


In addition to describing data and transferring data between layers, models have a large margin of opportunity. For example, if you connect the class-validator library , then by simply hanging the decorators, you can validate the data in these models, including with a little refinement, you can validate web forms.

Bonus 3: Creating Entities


In addition, if you needed to work with a local database, you can connect the typeorm library and your models will turn into entities by which the database will be generated and run.

Thank you for the attention


If you liked the article or approach, like and don’t be afraid to experiment. If you are a Redux adept and don’t like dissent, please explain in the comments how you scale, test and validate the data in your application.

All Articles