Netzarchitektur für Webanwendungen

Ich möchte Ihnen einen Ansatz vorstellen, den ich seit vielen Jahren bei der Entwicklung von Anwendungen, einschließlich Webanwendungen, verwende. Viele Entwickler von Desktop-, Server- und Mobilanwendungen sind mit diesem Ansatz vertraut. ist beim Erstellen solcher Anwendungen von grundlegender Bedeutung, wird jedoch im Web sehr schlecht dargestellt, obwohl es definitiv Leute gibt, die diesen Ansatz verwenden möchten. Darüber hinaus ist der VS-Code-Editor für diesen Ansatz geschrieben .

Reine Architektur

Durch die Anwendung dieses Ansatzes wird ein bestimmtes Framework entfernt. Sie können die Präsentationsbibliothek in Ihrer Anwendung einfach wechseln, z. B. React, Preact, Vue, Mithril, ohne die Geschäftslogik und in den meisten Fällen sogar Ansichten neu zu schreiben. Wenn Sie eine Anwendung auf Angular 1 haben, können Sie diese problemlos in Angular 2+, React, Svelte, WebComponents oder sogar in Ihre Präsentationsbibliothek übersetzen. Wenn Sie eine Anwendung auf Angular 2+ haben, aber keine Spezialisten dafür haben, können Sie die Anwendung problemlos in eine beliebtere Bibliothek übertragen, ohne die Geschäftslogik neu zu schreiben. Vergessen Sie am Ende jedoch völlig das Problem der Migration vom Framework zum Framework. Was für eine Art von Magie ist das?

Was ist saubere Architektur?


Um dies zu verstehen, lesen Sie am besten das Buch von Martin Robert "Saubere Architektur" ( von Robert C. Martin "Saubere Architektur " ). Ein kurzer Auszug, auf den im Artikel Bezug genommen wird .

Die Hauptideen der Architektur:

  1. Unabhängigkeit vom Rahmen. Architektur hängt nicht von der Existenz einer Bibliothek ab. Auf diese Weise können Sie das Framework als Tool verwenden, anstatt Ihr System in seine Grenzen zu drängen.
  2. Testbarkeit. Geschäftsregeln können ohne Benutzeroberfläche, Datenbank, Webserver oder andere externe Komponenten getestet werden.
  3. Unabhängigkeit von der Benutzeroberfläche. Die Benutzeroberfläche kann einfach geändert werden, ohne den Rest des Systems zu ändern. Beispielsweise kann die Weboberfläche durch die Konsole ersetzt werden, ohne die Geschäftsregeln zu ändern.
  4. Unabhängigkeit von der Datenbank. Sie können Oracle oder SQL Server gegen MongoDB, BigTable, CouchDB oder etwas anderes austauschen. Ihre Geschäftsregeln sind nicht datenbankbezogen.
  5. Unabhängigkeit von externen Diensten. Tatsächlich wissen Ihre Geschäftsregeln einfach nichts über die Außenwelt.

Die in diesem Buch seit vielen Jahren beschriebenen Ideen waren die Grundlage für die Erstellung komplexer Anwendungen in verschiedenen Bereichen.

Diese Flexibilität wird erreicht, indem die Anwendung in Service-, Repository- und Modellebenen unterteilt wird. Ich habe den MVC-Ansatz zu Clean Architecture hinzugefügt und die folgenden Ebenen erhalten:

  • Ansicht - Zeigt dem Client Daten an und visualisiert dem Client den Status der Logik.
  • Controller - ist für die Interaktion mit dem Benutzer über IO (Input-Output) verantwortlich.
  • Service - ist verantwortlich für die Geschäftslogik und deren Wiederverwendung zwischen Komponenten.
  • Repository - verantwortlich für den Empfang von Daten aus externen Quellen wie einer Datenbank, einer API, einem lokalen Speicher usw.
  • Modelle - ist verantwortlich für die Übertragung von Daten zwischen Schichten und Systemen sowie für die Logik der Verarbeitung dieser Daten.

Der Zweck jeder Schicht wird unten diskutiert.

Wer ist reine Architektur?


Die Webentwicklung hat einen langen Weg zurückgelegt, von einfachen JQuery-Skripten bis zur Entwicklung großer SPA-Anwendungen. Und jetzt sind Webanwendungen so groß geworden, dass der Umfang der Geschäftslogik mit Server-, Desktop- und Mobilanwendungen vergleichbar oder sogar überlegen ist.

Für Entwickler, die komplexe und große Anwendungen schreiben und Geschäftslogik vom Server auf Webanwendungen übertragen, um Serverkosten zu sparen, hilft Clean Architecture dabei, den Code und die Skalierung ohne Probleme in großem Umfang zu organisieren.

Wenn Ihre Aufgabe nur das Layout und die Animation von Zielseiten ist, kann Clean Architecture einfach nirgendwo eingefügt werden. Wenn sich Ihre Geschäftslogik im Backend befindet und Ihre Aufgabe darin besteht, die Daten abzurufen, dem Kunden anzuzeigen und den Klick auf die Schaltfläche zu verarbeiten, werden Sie die Flexibilität von Clean Architecture nicht spüren, aber es kann ein hervorragendes Sprungbrett für das explosive Wachstum der Anwendung sein.

Wo bereits angewendet?


Reine Architektur ist nicht an ein bestimmtes Framework, eine bestimmte Plattform oder eine bestimmte Programmiersprache gebunden. Seit Jahrzehnten wird es zum Schreiben von Desktop-Anwendungen verwendet. Die Referenzimplementierung finden Sie in den Frameworks für Serveranwendungen Asp.Net Core, Java Spring und NestJS. Es ist auch sehr beliebt beim Schreiben von iOs und Android-Anwendungen. In der Webentwicklung erschien er jedoch in einer äußerst erfolglosen Form in den Angular-Frameworks.

Da ich selbst nicht nur Typescript, sondern auch C # -Entwickler bin, werde ich beispielsweise die Referenzimplementierung dieser Architektur für Asp.Net Core übernehmen.

Hier ist eine vereinfachte Beispielanwendung:

Beispielanwendung auf 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; }
    }


Wenn Sie nicht verstehen, dass es nichts Schlechtes sagt, werden wir es in Teilen jedes Fragments analysieren.

Ein Beispiel wird für eine Asp.Net Core-Anwendung gegeben, aber für Java Spring, WinForms, Android, React sind Architektur und Code identisch, nur die Sprache und die Arbeit mit der Ansicht (falls vorhanden) ändern sich.

Internetanwendung


Das einzige Framework, das Clean Architecture verwenden wollte, war Angular. Aber es stellte sich einfach als schrecklich heraus, dass in 1, dass in 2+.

Dafür gibt es viele Gründe:

  1. Winkel monolithisches Gerüst. Und das ist sein Hauptproblem. Wenn Ihnen etwas darin nicht gefällt, müssen Sie täglich daran ersticken, und Sie können nichts dagegen tun. Es gibt nicht nur viele Problemstellen, sondern widerspricht auch der Ideologie der reinen Architektur.
  2. DI. , Javascript.
  3. . JSX. , 6, . .
  4. . Rollup ES2015 2 4 , angular 9.
  5. Und viele weitere Probleme. Im Allgemeinen rollt bis zur eckigen modernen Technologie mit einer Verzögerung von 5 Jahren relativ zu React.

Aber was ist mit anderen Frameworks? React, Vue, Preact, Mithril und andere sind ausschließlich Repräsentationsbibliotheken und bieten keine Architektur ... und wir haben bereits Architektur ... es bleibt, alles zu einem Ganzen zusammenzufügen!

Wir beginnen eine Anwendung zu erstellen


Wir werden Pure Architecture am Beispiel einer fiktiven Anwendung betrachten, die einer echten Webanwendung so nahe wie möglich kommt. Dies ist ein Büro in der Versicherungsgesellschaft, in dem das Benutzerprofil, die versicherten Ereignisse, die vorgeschlagenen Versicherungstarife und die Tools für die Arbeit mit diesen Daten angezeigt werden.

Anwendungsprototyp

Im Beispiel wird nur ein kleiner Teil der Funktion implementiert, aber daraus können Sie erkennen, wo und wie der Rest der Funktion positioniert werden muss. Beginnen wir mit der Erstellung der Anwendung auf der Controller-Ebene und verbinden die View-Ebene ganz am Ende. Und im Verlauf der Erstellung betrachten wir jede Ebene detaillierter.

Controller-Muster


Controller - ist für die Benutzerinteraktion mit der Anwendung verantwortlich. Dies kann ein Klick auf eine Schaltfläche auf einer Webseite, einer Desktop-Anwendung, einer mobilen Anwendung oder die Eingabe eines Befehls in der Linux-Konsole oder eine Netzwerkanforderung oder ein anderes E / A-Ereignis sein, das in die Anwendung eingeht.

Der einfachste Controller in einer sauberen Architektur lautet wie folgt:

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

}

Seine Aufgabe ist es, ein Ereignis vom Benutzer zu empfangen und Geschäftsprozesse zu starten. Im Idealfall weiß der Controller nichts über View und kann dann zwischen Plattformen wie Web, React-Native oder Electron wiederverwendet werden.

Schreiben wir nun einen Controller für unsere Anwendung. Seine Aufgabe ist es, ein Benutzerprofil und verfügbare Tarife zu erhalten und dem Benutzer den besten Tarif anzubieten:

UserPageController. Controller mit Geschäftslogik
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;
            });
        }
    }

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


Wir haben einen regulären Controller ohne saubere Architektur. Wenn wir ihn von React.Component erben, erhalten wir eine funktionierende Komponente mit Logik. So viele Webanwendungsentwickler schreiben, aber dieser Ansatz hat viele wesentliche Nachteile. Das Hauptproblem ist die Unfähigkeit, Logik zwischen Komponenten wiederzuverwenden. Schließlich kann der empfohlene Tarif nicht nur in Ihrem persönlichen Konto, sondern auch auf der Zielseite und an vielen anderen Orten angezeigt werden, um einen Kunden für den Service zu gewinnen.

Um die Logik zwischen Komponenten wiederverwenden zu können, muss sie in einer speziellen Ebene namens Service platziert werden.

Servicemuster


Service - verantwortlich für die gesamte Geschäftslogik der Anwendung. Wenn der Controller Daten empfangen, verarbeiten und senden musste, geschieht dies über den Dienst. Wenn mehrere Controller dieselbe Logik benötigen, arbeiten sie mit Service zusammen. Die Service-Schicht selbst sollte jedoch nichts über die Controller- und View-Schicht und die Umgebung wissen, in der sie arbeitet.

Verschieben wir die Logik vom Controller zum Service und implementieren den Service im Controller:

UserPageController. Controller ohne Geschäftslogik
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 zum Arbeiten mit Benutzerprofil
export class UserProfilService {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

Tarifservice. Service für die Arbeit mit Tarifen
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;
        });
    }
    
    /**
     * ...       
     */
}


Wenn mehrere Controller ein Benutzerprofil oder Tarife benötigen, können sie dieselbe Logik aus den Diensten wiederverwenden. Bei Dienstleistungen ist es wichtig, die SOLID-Prinzipien nicht zu vergessen und dass jeder Dienst für seinen Verantwortungsbereich verantwortlich ist. In diesem Fall ist ein Dienst für die Arbeit mit dem Benutzerprofil verantwortlich, und ein anderer Dienst ist für die Arbeit mit Tarifen verantwortlich.

Was aber, wenn sich die Datenquelle ändert, z. B. Fetch in Websocket oder Grps oder die Datenbank ändern kann und die realen Daten durch Testdaten ersetzt werden müssen? Und warum muss die Geschäftslogik im Allgemeinen etwas über eine Datenquelle wissen? Um diese Probleme zu lösen, gibt es eine Repository-Schicht.

Repository-Muster


Repository - verantwortlich für die Kommunikation mit dem Data Warehouse. Der Speicher kann ein Server, eine Datenbank, ein Speicher, ein lokaler Speicher, ein Sitzungsspeicher oder ein anderer Speicher sein. Seine Aufgabe besteht darin, die Service-Schicht von der spezifischen Speicherimplementierung zu abstrahieren.

Lassen Sie uns Netzwerkanforderungen von Diensten im Repository stellen, während sich der Controller nicht ändert:
UserProfilService. Service zum Arbeiten mit Benutzerprofil
import { UserProfilRepository } from "./UserProfilRepository";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

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

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

Tarifservice. Service für die Arbeit mit Tarifen
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;
        });
    }
    
    /**
     * ...       
     */
}

TarifRepository. Repository für die Arbeit mit Tarifspeicher
export class TariffRepository {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}


Jetzt reicht es aus, eine Datenanforderung einmal zu schreiben, und jeder Dienst kann diese Anforderung wiederverwenden. Später sehen wir uns ein Beispiel an, wie Sie das Repository neu definieren, ohne den Servicecode zu berühren, und das Mokka-Repository zum Testen implementieren.

Im UserProfilService-Dienst scheint dies nicht erforderlich zu sein, und der Controller kann direkt auf das Repository für Daten zugreifen. Dies ist jedoch nicht der Fall. Zu jedem Zeitpunkt können Anforderungen in der Geschäftsschicht auftreten oder sich ändern, eine zusätzliche Anforderung kann erforderlich sein oder Daten können angereichert werden. Selbst wenn die Serviceschicht keine Logik enthält, muss die Controller-Service-Repository-Kette daher beibehalten werden. Dies ist ein Beitrag zu Ihrem Morgen.

Es ist Zeit herauszufinden, welche Art von Repository eingerichtet wird, ob sie überhaupt korrekt sind. Die Ebene Models ist dafür verantwortlich.

Modelle: DTO, Entities, ViewModels


Modelle - ist verantwortlich für die Beschreibung der Strukturen, mit denen die Anwendung arbeitet. Eine solche Beschreibung hilft neuen Projektentwicklern sehr zu verstehen, mit was die Anwendung arbeitet. Darüber hinaus ist es sehr praktisch, damit Datenbanken zu erstellen oder im Modell gespeicherte Daten zu validieren.

Die Modelle sind je nach Verwendungsart in verschiedene Muster unterteilt:
  • Entitäten - sind für die Arbeit mit der Datenbank verantwortlich und sind eine Struktur, die eine Tabelle oder ein Dokument in der Datenbank wiederholt.
  • DTO (Data Transfer Object) - werden zum Übertragen von Daten zwischen verschiedenen Ebenen der Anwendung verwendet.
  • ViewModel - enthält vorbereitete Informationen, die für die Anzeige in der Ansicht erforderlich sind.


Fügen Sie der Anwendung das Benutzerprofilmodell und andere Modelle hinzu, und teilen Sie den anderen Ebenen mit, dass wir jetzt nicht mit einem abstrakten Objekt, sondern mit einem ganz bestimmten Profil arbeiten:
UserPageController. Anstelle von irgendwelchen werden die beschriebenen Modelle verwendet.
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. Geben Sie statt eines das zurückgegebene Modell an
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();
    }
    
    /**
     * ...        
     */
}

Tarifservice. Geben Sie statt eines das zurückgegebene Modell an
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. Geben Sie statt eines das zurückgegebene Modell an
import { UserProfileDto } from "./UserProfileDto";

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

TarifRepository. Geben Sie statt eines das zurückgegebene Modell an
import { TariffDto } from "./TariffDto";

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

    /**
     * ...        
     */
}

UserProfileDto. Ein Modell mit einer Beschreibung der Daten, mit denen wir arbeiten
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;
    }

}

TarifDto. Ein Modell mit einer Beschreibung der Daten, mit denen wir arbeiten
export class TariffDto {
    public ageFrom: number = 0;
    public ageTo: number = 0;
    public price: number = 0;
}


Unabhängig davon, in welcher Ebene der Anwendung wir uns befinden, wissen wir genau, mit welchen Daten wir arbeiten. Aufgrund der Beschreibung des Modells haben wir auch einen Fehler in unserem Service gefunden. In der Servicelogik wurde die Eigenschaft userProfile.age verwendet, die eigentlich nicht vorhanden ist, aber ein Geburtsdatum hat. Um das Alter zu berechnen, müssen Sie die Modellmethode userProfile.getAge () aufrufen.

Es gibt jedoch ein Problem. Wenn wir versuchen, die Methoden aus dem Modell zu verwenden, das das aktuelle Repository bereitgestellt hat, erhalten wir eine Ausnahme. Die Sache ist, dass die Methoden response.json () und JSON.parse ()Es gibt nicht unser Modell zurück, sondern ein JSON-Objekt, das in keiner Weise mit unserem Modell verknüpft ist. Sie können dies überprüfen, wenn Sie den Befehl userProfile instanceof UserProfileDto ausführen und eine falsche Anweisung erhalten. Um die von einer externen Quelle empfangenen Daten in das beschriebene Modell zu konvertieren, wird die Daten deserialisiert.

Daten-Deserialisierung


Deserialisierung - der Prozess der Wiederherstellung der erforderlichen Struktur aus einer Folge von Bytes. Wenn die Daten Informationen enthalten, die nicht in den Modellen angegeben sind, werden sie ignoriert. Wenn die Daten Informationen enthalten, die der Beschreibung des Modells widersprechen, tritt ein Deserialisierungsfehler auf.

Und das Interessanteste dabei ist, dass sie beim Entwerfen des ES2015 und beim Hinzufügen des Klassenschlüsselworts vergessen haben, Deserialisierung hinzuzufügen ... Die Tatsache, dass sie in allen Sprachen sofort einsatzbereit sind, haben sie in ES2015 einfach vergessen ...

Um dieses Problem zu lösen, habe ich eine Bibliothek für TS-Serializable Deserialization geschrieben , einen Artikel darüber kann unter diesem Link gelesen werden . Der Zweck besteht darin, die verlorene Funktionalität zurückzugeben.

Fügen Sie dem Modell Deserialisierungsunterstützung und Deserialisierung selbst zum Repository hinzu:
TarifRepository. Fügen Sie einen Deserialisierungsprozess hinzu
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); //  
    }
    
    /**
     * ...        
     */
}

TarifRepository. Fügen Sie einen Deserialisierungsprozess hinzu
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. Deserialisierungsunterstützung hinzufügen
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;
    }

}

TarifDto. Deserialisierungsunterstützung hinzufügen
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;

}


Jetzt können Sie in allen Ebenen der Anwendung absolut sicher sein, dass wir mit den Modellen arbeiten, die wir erwarten. In der Ansicht, dem Controller und anderen Ebenen können Sie die Methoden des beschriebenen Modells aufrufen.

Wofür sind Serializable und jsonProperty?
Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .

Jetzt haben wir eine fast fertige Bewerbung. Es ist Zeit, die in den Ebenen Controller, Service und Models geschriebene Logik zu testen. Dazu müssen wir speziell vorbereitete Testdaten in der Repository-Schicht anstelle einer echten Anfrage an den Server zurückgeben. Aber wie man das Repository ersetzt, ohne den Code zu berühren, der in die Produktion geht. Hierfür gibt es ein Abhängigkeitsinjektionsmuster.

Abhängigkeitsinjektion - Abhängigkeitsinjektion


Abhängigkeitsinjektion - Fügt Abhängigkeiten in die Ebenen "Contoller", "Service" und "Repository" ein und ermöglicht es Ihnen, diese Abhängigkeiten außerhalb dieser Ebenen zu überschreiben.

Im Programm hängt die Controller-Schicht von der Service-Schicht und von der Repository-Schicht ab. In der aktuellen Form verursachen Ebenen selbst ihre Abhängigkeiten durch Instanziierung. Und um die Abhängigkeit neu zu definieren, muss die Schicht diese Abhängigkeit von außen einstellen. Es gibt viele Möglichkeiten, dies zu tun, aber die beliebteste ist das Übergeben der Abhängigkeit als Parameter im Konstruktor.

Dann sieht das Erstellen eines Programms mit allen Abhängigkeiten folgendermaßen aus:
var programm = new IndexPageController(new ProfileService(new ProfileRepository()));

Stimme zu - es sieht schrecklich aus. Selbst wenn man bedenkt, dass das Programm nur zwei Abhängigkeiten enthält, sieht es schon schrecklich aus. Was soll man über Programme sagen, in denen Hunderte und Tausende von Abhängigkeiten bestehen?

Um das Problem zu lösen, benötigen Sie ein spezielles Werkzeug, aber dafür müssen Sie es finden. Wenn wir uns den Erfahrungen anderer Plattformen zuwenden, beispielsweise Asp.Net Core, erfolgt die Registrierung von Abhängigkeiten in der Initialisierungsphase des Programms und sieht ungefähr so ​​aus:
DI.register(IProfileService,ProfileService);

Beim Erstellen des Controllers erstellt und implementiert das Framework selbst diese Abhängigkeit.

Es gibt jedoch drei wesentliche Probleme:
  1. Beim Transpilieren von Typescript in Javascript sind keine Spuren der Schnittstellen mehr vorhanden.
  2. Alles, was in den klassischen DI fiel, bleibt für immer darin. Es ist sehr schwierig, es während des Refactorings zu reinigen. In einer Webanwendung müssen Sie jedes Byte speichern.
  3. Fast alle Ansichtsbibliotheken verwenden DI nicht, und Controller-Designer sind mit Parametern beschäftigt.


In Webanwendungen wird DI nur in Angular 2+ verwendet. In Winkel 1 wurde beim Registrieren von Abhängigkeiten anstelle einer Schnittstelle eine Zeichenfolge verwendet, in InversifyJS wird Symbol anstelle der Schnittstelle verwendet. Und all dies ist so schrecklich implementiert, dass es besser ist, viel Neues als im ersten Beispiel dieses Abschnitts zu haben als diese Lösungen.

Um alle drei Probleme zu lösen, wurde mein eigenes DI erfunden, und die Lösung dafür half mir, das Java Spring-Framework und seinen automatisch verdrahteten Dekorator zu finden. Die Beschreibung der Funktionsweise dieses DI finden Sie im Artikel unter dem Link und im GitHub-Repository .

Es ist Zeit, den resultierenden DI in unserer Anwendung anzuwenden.

Alles zusammenfügen


Um DI auf allen Ebenen zu implementieren, fügen wir einen Reflection Decorator hinzu, der bewirkt, dass Typoskript zusätzliche Metainformationen zu Abhängigkeitstypen generiert. In der Steuerung, in der Sie die Abhängigkeiten aufrufen müssen, hängen wir den automatisch verdrahteten Dekorator auf. Und an der Stelle, an der das Programm initialisiert wird, bestimmen wir, in welcher Umgebung welche Abhängigkeit implementiert wird.

Erstellen Sie für das UserProfilRepository-Repository dasselbe Repository, jedoch mit Testdaten anstelle der eigentlichen Anforderung. Als Ergebnis erhalten wir den folgenden Code:
Main.ts. Speicherort für die Programminitialisierung
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";

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

UserPageController. Abhängigkeit durch den Autodrahtdekorateur
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. Einführung in Reflexion und Abhängigkeitsgenerierung
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();
    }

    /**
     * ...        
     */
}

Tarifservice. Einführung in Reflexion und Abhängigkeitsgenerierung
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. Einführung in die Reflexionsgenerierung
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. Neues Repository zum Testen
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); //   
    }

    /**
     * ...        
     */
}

TarifRepository. Einführung in die Reflexionsgenerierung
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);
        });
    }

    /**
     * ...        
     */
}


Jetzt gibt es überall im Programm die Möglichkeit, die Implementierung einer Logik zu ändern. In unserem Beispiel werden anstelle einer echten Benutzerprofilanforderung an den Server in der Testumgebung Testdaten verwendet.

Im wirklichen Leben können Ersetzungen überall gefunden werden. Sie können beispielsweise die Logik in einem Service ändern, den alten Service in der Produktion implementieren und beim Refactoring ist er bereits neu. Führen Sie A / B-Tests mit Geschäftslogik durch, ändern Sie die dokumentbasierte Datenbank in relational und ändern Sie die Netzwerkanforderung im Allgemeinen in Web-Sockets. Und das alles, ohne die Entwicklung anzuhalten, um die Lösung neu zu schreiben.

Es ist Zeit, das Ergebnis des Programms zu sehen. Hierfür gibt es eine Ansichtsebene.

View implementieren


Die Ansichtsebene ist dafür verantwortlich, dem Benutzer die in der Controller-Ebene enthaltenen Daten zu präsentieren. In diesem Beispiel werde ich React dafür verwenden, aber an seiner Stelle kann es sich auch um ein anderes handeln, z. B. Preact, Svelte, Vue, Mithril, WebComponent oder ein anderes.

Erben Sie dazu einfach unseren Controller von React.Component und fügen Sie ihm eine Rendermethode mit der Darstellung der Ansicht hinzu:

Main.ts. Startet das Zeichnen einer React-Komponente
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. Erbt von React.Component und fügt eine Rendermethode hinzu
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>
            </>
        );
    }

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


Durch Hinzufügen von nur zwei Zeilen und einer Präsentationsvorlage wurde unser Controller zu einer Reaktionskomponente mit Arbeitslogik.

Warum wird forceUpdate anstelle von setState aufgerufen?
forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .

Aber selbst in einer solchen Implementierung stellte sich heraus, dass wir mit der React-Bibliothek mit ihrem Lebenszyklus, der Ansichtsimplementierung und dem Prinzip der Ungültigmachung der Ansicht verbunden waren, was dem Konzept der sauberen Architektur widerspricht. Logik und Layout befinden sich in derselben Datei, was die parallele Arbeit von Schriftsetzer und Entwickler erschwert.

Trennung von Controller und Ansicht


Um beide Probleme zu lösen, fügen wir die Ansichtsebene in eine separate Datei ein und erstellen anstelle der React-Komponente die Basiskomponente, die unseren Controller von einer bestimmten Präsentationsbibliothek abstrahiert. Gleichzeitig beschreiben wir die Attribute, die die Komponente annehmen kann.

Wir erhalten folgende Änderungen:
UserPageView. Sie nahmen die Ansicht in eine separate Datei
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. Betrachten und reagieren Sie auf eine separate Datei
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 Eine Komponente, die uns von einem bestimmten Framework abstrahiert
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", {}, "  ");
        }
    }

}


Jetzt befindet sich unsere Ansicht in einer separaten Datei, und der Controller weiß nichts darüber, außer dass dies der Fall ist. Vielleicht sollten Sie die Ansicht nicht über die Controller-Eigenschaft einfügen, sondern wie Angular über den Dekorator, aber dies ist ein Thema, über das Sie nachdenken sollten.

Die Grundkomponente enthält auch eine Abstraktion vom Lebenszyklus des Frameworks. Sie unterscheiden sich in allen Frameworks, aber in allen Frameworks. Angular ist ngOnInit, ngOnChanges, ngOnDestroy. In React and Preact ist dies componentDidMount, shouldComponentUpdate, componentWillUnmount. In Vue wird dies erstellt, aktualisiert und zerstört. In Mithril ist es oncreate, onupdate, onremove. In WebComponents ist dies ConnectedCallback, AttributeChangedCallback, DisconnectedCallback. Und so in jeder Bibliothek. Die meisten haben sogar die gleiche oder eine ähnliche Schnittstelle.

Außerdem können die Bibliothekskomponenten jetzt mit einer eigenen Logik für die spätere Wiederverwendung zwischen allen Komponenten erweitert werden. Zum Beispiel Einführung von Tools für Analyse, Überwachung, Protokollierung usw.

Wir schauen uns das Ergebnis an


Es bleibt nur zu bewerten, was passiert ist. Das gesamte Programm hat die folgende endgültige Form:
Main.ts. Die Datei, aus der das Programm gestartet wird
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. Darstellung einer der Programmkomponenten.
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. Die Logik einer der Komponenten für die Benutzerinteraktion
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 Basisklasse für alle Programmkomponenten
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. Dienst zum Wiederverwenden der Logik zwischen Komponenten, um mit einem Benutzerprofil zu arbeiten
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();
    }

    /**
     * ...        
     */
}

Tarifservice. Service zur Wiederverwendung von Logik zwischen Komponenten für die Arbeit mit Tarifen
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 zum Empfangen, Überprüfen und Validieren eines Profils vom Server
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;

}


Als Ergebnis haben wir eine modular skalierbare Anwendung mit einer sehr geringen Menge an Boilerplate (1 Basiskomponente, 3 Zeilen zum Implementieren von Abhängigkeiten pro Klasse) und einem sehr geringen Overhead (eigentlich nur zum Implementieren von Abhängigkeiten, alles andere ist logisch) erhalten. Wir sind auch nicht an eine Präsentationsbibliothek gebunden. Als Angular 1 starb, begannen viele, Anwendungen in React neu zu schreiben. Als die Entwickler von Angular 2 erschöpft waren, litten viele Unternehmen unter der Geschwindigkeit der Entwicklung. Wenn React erneut stirbt, müssen Sie die Lösungen, die an das Framework und das Ökosystem gebunden sind, neu schreiben. Mit Chita Architecture können Sie jedoch vergessen, das Framework zu binden.

Was ist der Vorteil gegenüber Redux?


Um den Unterschied zu verstehen, wollen wir sehen, wie sich Redux verhält, wenn die Anwendung wächst.
Redux

Wie Sie dem Diagramm entnehmen können, nehmen mit dem vertikal skalierten Wachstum von Redux-Anwendungen auch der Store und die Anzahl der Reduzierer zu und werden zu einem Engpass. Und der Aufwand für den Wiederaufbau des Geschäfts und die Suche nach dem richtigen Reduzierer beginnt, die Nutzlast zu überschreiten.

Sie können das Verhältnis von Overhead zu Nutzlast in einer mittelgroßen Anwendung mit einem einfachen Test überprüfen.
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

Das Wiederherstellen des Speichers in 100 Eigenschaften dauerte achtmal länger als die Logik selbst. Mit 1000 Elementen ist dies bereits 50-mal mehr. Darüber hinaus kann eine Benutzeraktion eine ganze Reihe von Aktionen erzeugen, deren Aufruf schwer abzufangen und zu debuggen ist. Sie können sicherlich argumentieren, dass 0,04 ms zum Wiederherstellen des Stores sehr klein sind und nicht langsamer werden. Aber 0,04 ms sind auf dem Core i7-Prozessor und für eine Aktion. Angesichts der schwächeren mobilen Prozessoren und der Tatsache, dass eine einzelne Benutzeraktion Dutzende von Aktionen hervorrufen kann, führt dies dazu, dass die Berechnungen nicht in 16 ms passen und das Gefühl entsteht, dass die Anwendung langsamer wird.

Vergleichen wir, wie die Clean Architecture-Anwendung wächst:
Reine Architektur

Wie aufgrund der Trennung von Logik und Verantwortlichkeiten zwischen den Ebenen zu sehen ist, wird die Anwendung horizontal skaliert. Wenn eine Komponente die Daten verarbeiten muss, wendet sie sich an den entsprechenden Dienst, ohne die Dienste zu berühren, die nicht mit ihrer Aufgabe zusammenhängen. Nach dem Empfang der Daten wird nur eine Komponente neu gezeichnet. Die Datenverarbeitungskette ist sehr kurz und offensichtlich, und für maximal 4 Codesprünge können Sie die erforderliche Logik finden und debuggen. Wenn wir eine Analogie zu einer Mauer ziehen, können wir außerdem jeden Stein von dieser Mauer entfernen und durch einen anderen ersetzen, ohne die Stabilität dieser Mauer zu beeinträchtigen.

Bonus 1: Verbesserte Funktionalität der Komponenten des Frameworks


Ein Bonus war die Fähigkeit, das Verhalten der Komponenten der Präsentationsbibliothek zu ergänzen oder zu ändern. Eine Reaktion im Falle eines Fehlers in der Ansicht rendert beispielsweise nicht die gesamte Anwendung, wenn eine kleine Überarbeitung vorgenommen wird:
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}`
            );
        }
    }

}

Jetzt zeichnet die Reaktion nicht nur die Komponente, in der der Fehler aufgetreten ist. Auf die gleiche einfache Weise können Sie Überwachung, Analyse und andere Nishtyaki hinzufügen.

Bonus 2: Datenvalidierung


Modelle beschreiben nicht nur Daten und übertragen Daten zwischen Ebenen, sondern bieten auch einen großen Spielraum. Wenn Sie beispielsweise die Klassenvalidatorbibliothek verbinden , können Sie durch einfaches Aufhängen der Dekoratoren die Daten in diesen Modellen validieren, einschließlich Mit ein wenig Verfeinerung können Sie Webformulare validieren.

Bonus 3: Entitäten erstellen


Wenn Sie mit einer lokalen Datenbank arbeiten müssen, können Sie außerdem die Typorm- Bibliothek verbinden, und Ihre Modelle werden zu Entitäten, mit denen die Datenbank generiert und ausgeführt wird.

Vielen Dank für Ihre Aufmerksamkeit


Wenn Ihnen der Artikel oder Ansatz gefallen hat, mögen Sie es und haben Sie keine Angst zu experimentieren. Wenn Sie ein Redux-Anhänger sind und Dissens nicht mögen, erläutern Sie bitte in den Kommentaren, wie Sie die Daten in Ihrer Anwendung skalieren, testen und validieren.

All Articles