Arsitektur Net untuk Aplikasi Web

Saya ingin berbagi dengan Anda suatu pendekatan yang telah saya gunakan selama bertahun-tahun dalam mengembangkan aplikasi, termasuk aplikasi web. Banyak pengembang aplikasi desktop, server, dan seluler terbiasa dengan pendekatan ini. sangat mendasar ketika membangun aplikasi seperti itu, bagaimanapun, itu direpresentasikan dengan sangat buruk di web, meskipun pasti ada orang yang ingin menggunakan pendekatan ini. Selain itu, editor Kode VS ditulis pada pendekatan ini .

Arsitektur Murni

Sebagai hasil dari penerapan pendekatan ini, Anda akan menyingkirkan kerangka kerja tertentu. Anda dapat dengan mudah mengganti perpustakaan tampilan di dalam aplikasi Anda, misalnya Bereaksi, Bertindak, Vue, Mithril tanpa menulis ulang logika bisnis, dan dalam kebanyakan kasus bahkan tampilan. Jika Anda memiliki aplikasi pada Angular 1, Anda dapat dengan mudah menerjemahkannya ke Angular 2+, React, Svelte, WebComponents, atau bahkan perpustakaan presentasi Anda. Jika Anda memiliki aplikasi pada Angular 2+, tetapi tidak ada spesialis untuk itu, maka Anda dapat dengan mudah mentransfer aplikasi ke perpustakaan yang lebih populer tanpa menulis ulang logika bisnis. Namun pada akhirnya, sepenuhnya lupakan masalah migrasi dari framework ke framework. Sihir macam apa ini?

Apa itu Arsitektur Bersih


Untuk memahami hal ini, yang terbaik adalah membaca buku Martin Robert "Clean Architecture» ( oleh Robert C.Martin "Clean Architecture» ). Kutipan singkat dari yang diberikan dalam artikel dengan referensi .

Gagasan utama yang tertanam dalam arsitektur:

  1. Bebas dari kerangka kerja. Arsitektur tidak bergantung pada keberadaan perpustakaan apa pun. Ini memungkinkan Anda untuk menggunakan kerangka kerja sebagai alat, alih-alih memeras sistem Anda ke dalam keterbatasannya.
  2. Testabilitas. Aturan bisnis dapat diuji tanpa antarmuka pengguna, database, server web, atau komponen eksternal lainnya.
  3. Bebas dari UI. Antarmuka pengguna dapat dengan mudah diubah tanpa mengubah sisa sistem. Misalnya, antarmuka web dapat diganti oleh konsol, tanpa mengubah aturan bisnis.
  4. Bebas dari basis data. Anda dapat menukar Oracle atau SQL Server dengan MongoDB, BigTable, CouchDB atau yang lainnya. Aturan bisnis Anda tidak terkait dengan basis data.
  5. Bebas dari layanan eksternal apa pun. Bahkan, aturan bisnis Anda tidak tahu apa-apa tentang dunia luar.

Gagasan yang diuraikan dalam buku ini selama bertahun-tahun telah menjadi dasar untuk membangun aplikasi yang kompleks di berbagai bidang.

Fleksibilitas ini dicapai dengan membagi aplikasi menjadi lapisan Layanan, Repositori, Model. Saya menambahkan pendekatan MVC ke Arsitektur Bersih dan mendapatkan lapisan berikut:

  • Lihat - menampilkan data ke klien, sebenarnya memvisualisasikan keadaan logika kepada klien.
  • Controller - bertanggung jawab untuk berinteraksi dengan pengguna melalui IO (input-output).
  • Layanan - bertanggung jawab atas logika bisnis dan penggunaan kembali antar komponen.
  • Repositori - bertanggung jawab untuk menerima data dari sumber eksternal, seperti database, api, penyimpanan lokal, dll.
  • Model - bertanggung jawab untuk mentransfer data antara lapisan dan sistem, serta untuk logika pemrosesan data ini.

Tujuan setiap lapisan dibahas di bawah ini.

Siapa Arsitektur Murni


Pengembangan web telah berjalan jauh, dari scripting jquery sederhana untuk mengembangkan aplikasi SPA besar. Dan sekarang aplikasi web telah menjadi sangat besar sehingga jumlah logika bisnis telah menjadi sebanding atau bahkan lebih unggul dari aplikasi server, desktop dan seluler.

Untuk pengembang yang menulis aplikasi yang kompleks dan besar, serta mentransfer logika bisnis dari server ke aplikasi web untuk menghemat biaya server, Arsitektur Murni akan membantu mengatur kode dan skala tanpa masalah ke skala besar.

Pada saat yang sama, jika tugas Anda hanya tata letak dan animasi halaman arahan, maka Arsitektur Bersih tidak punya tempat untuk disisipkan. Jika logika bisnis Anda ada di backend dan tugas Anda adalah untuk mendapatkan data, menampilkannya kepada klien dan memproses klik pada tombol, maka Anda tidak akan merasakan fleksibilitas Arsitektur Bersih, tetapi itu bisa menjadi batu loncatan yang sangat baik untuk ledakan pertumbuhan aplikasi.

Di mana sudah diterapkan?


Arsitektur murni tidak terikat pada kerangka kerja, platform, atau bahasa pemrograman tertentu. Selama beberapa dekade, telah digunakan untuk menulis aplikasi desktop. Implementasi referensi dapat ditemukan dalam kerangka kerja untuk aplikasi server Asp.Net Core, Java Spring dan NestJS. Ini juga sangat populer saat menulis aplikasi Android dan iOs. Tetapi dalam pengembangan web, ia muncul dalam bentuk yang sangat tidak berhasil dalam kerangka kerja Angular.

Karena saya sendiri bukan hanya mengetikkan naskah, tetapi juga pengembang C #, misalnya saya akan mengambil referensi implementasi arsitektur ini untuk Asp.Net Core.

Berikut adalah contoh aplikasi yang disederhanakan:

Contoh Aplikasi pada 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; }
    }


Jika Anda tidak mengerti bahwa itu tidak mengatakan hal buruk, maka kami akan menganalisisnya di setiap bagian fragmen.

Contoh diberikan untuk aplikasi Asp.Net Core, tetapi untuk Java Spring, WinForms, Android, Bereaksi arsitektur dan kode akan sama, hanya bahasa dan bekerja dengan tampilan (jika ada) akan berubah.

Aplikasi web


Satu-satunya kerangka kerja yang mencoba menggunakan Arsitektur Bersih adalah Angular. Tapi ternyata mengerikan, bahwa dalam 1, dalam 2+.

Dan ada banyak alasan untuk ini:

  1. Kerangka kerja monolitik sudut. Dan ini adalah masalah utamanya. Jika Anda tidak menyukai sesuatu di dalamnya, Anda harus mencekiknya setiap hari, dan tidak ada yang dapat Anda lakukan untuk itu. Tidak hanya terdapat banyak tempat bermasalah di dalamnya, tetapi juga bertentangan dengan ideologi arsitektur murni.
  2. DI. , Javascript.
  3. . JSX. , 6, . .
  4. . Rollup ES2015 2 4 , angular 9.
  5. Dan masih banyak lagi masalah. Secara umum, hingga gulungan teknologi modern sudut dengan penundaan 5 tahun relatif terhadap Bereaksi.

Tapi bagaimana dengan kerangka kerja lain? React, Vue, Preact, Mithril, dan lainnya adalah perpustakaan presentasi khusus dan tidak menyediakan arsitektur apa pun ... tapi kami sudah memiliki arsitekturnya ... tetap merakit semuanya menjadi satu kesatuan!

Kami mulai membuat aplikasi


Kami akan mempertimbangkan Arsitektur Murni dengan contoh aplikasi fiktif yang sedekat mungkin dengan aplikasi web nyata. Ini adalah kantor di perusahaan asuransi yang menampilkan profil pengguna, acara yang diasuransikan, tarif asuransi yang diusulkan, dan alat untuk bekerja dengan data ini.

Prototipe aplikasi

Dalam contoh, hanya sebagian kecil dari fungsional yang akan diimplementasikan, tetapi dari itu Anda dapat memahami di mana dan bagaimana memposisikan sisa fungsional. Mari kita mulai membuat aplikasi dari lapisan Controller, dan hubungkan layer View di bagian paling akhir. Dan dalam proses penciptaan, kami mempertimbangkan setiap lapisan secara lebih rinci.

Pola Pengendali


Controller - bertanggung jawab atas interaksi pengguna dengan aplikasi. Itu bisa berupa klik pada tombol di halaman web, aplikasi desktop, aplikasi seluler, atau memasukkan perintah di konsol Linux, atau permintaan jaringan, atau acara IO lainnya yang masuk ke dalam aplikasi.

Pengontrol sederhana dalam arsitektur bersih adalah sebagai berikut:

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

}

Tugasnya adalah untuk menerima suatu peristiwa dari pengguna dan memulai proses bisnis. Dalam kasus yang ideal, Pengendali tidak tahu apa-apa tentang Tampilan, dan kemudian dapat digunakan kembali di antara platform, seperti Web, React-Native atau Electron.

Sekarang mari kita tulis controller untuk aplikasi kita. Tugasnya adalah untuk mendapatkan profil pengguna, tarif yang tersedia dan menawarkan tarif terbaik kepada pengguna:

UserPageController. Pengontrol dengan logika bisnis
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;
            });
        }
    }

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


Kami mendapat pengontrol reguler tanpa arsitektur yang bersih, jika kami mewarisinya dari React.Component kami mendapatkan komponen yang berfungsi dengan logika. Banyak pengembang aplikasi web menulis, tetapi pendekatan ini memiliki banyak kelemahan signifikan. Yang utama adalah ketidakmampuan untuk menggunakan kembali logika antar komponen. Setelah semua, tarif yang disarankan dapat ditampilkan tidak hanya di akun pribadi Anda, tetapi juga di halaman arahan dan banyak tempat lain untuk menarik klien ke layanan.

Untuk dapat menggunakan kembali logika antar komponen, perlu menempatkannya di lapisan khusus yang disebut Layanan.

Pola Layanan


Layanan - bertanggung jawab atas seluruh logika bisnis aplikasi. Jika Pengendali perlu menerima, memproses, mengirim beberapa data - pengontrol melakukan hal ini melalui Layanan. Jika beberapa pengontrol memerlukan logika yang sama, mereka bekerja dengan Layanan. Tetapi lapisan Layanan itu sendiri seharusnya tidak tahu apa-apa tentang lapisan Pengendali dan Tampilan dan lingkungan di mana ia bekerja.

Mari kita pindahkan logika dari controller ke layanan dan mengimplementasikan layanan di controller:

UserPageController. Pengontrol tanpa logika bisnis
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. Layanan untuk bekerja dengan profil pengguna
export class UserProfilService {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

Layanan Tarif. Layanan untuk bekerja dengan tarif
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;
        });
    }
    
    /**
     * ...       
     */
}


Sekarang, jika beberapa pengontrol perlu mendapatkan profil atau tarif pengguna, mereka dapat menggunakan kembali logika yang sama dari layanan. Dalam layanan, hal utama adalah tidak melupakan prinsip-prinsip SOLID dan bahwa setiap layanan bertanggung jawab atas area tanggung jawabnya. Dalam hal ini, satu layanan bertanggung jawab untuk bekerja dengan profil pengguna, dan layanan lain bertanggung jawab untuk bekerja dengan tarif.

Tetapi bagaimana jika sumber data berubah, misalnya, pengambilan dapat berubah menjadi websocket atau grps atau database, dan data nyata perlu diganti dengan data uji? Dan secara umum, mengapa logika bisnis perlu mengetahui sesuatu tentang sumber data? Untuk mengatasi masalah ini, ada lapisan Repositori.

Pola Repositori


Repositori - bertanggung jawab untuk komunikasi dengan data warehouse. Penyimpanan dapat berupa server, basis data, memori, penyimpanan lokal, penyimpanan sesi, atau penyimpanan lainnya. Tugasnya adalah untuk mengabstraksi lapisan Layanan dari implementasi penyimpanan spesifik.

Mari kita membuat permintaan jaringan dari layanan di repositori, sementara controller tidak berubah:
UserProfilService. Layanan untuk bekerja dengan profil pengguna
import { UserProfilRepository } from "./UserProfilRepository";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

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

UserProfilRepository. Layanan untuk bekerja dengan penyimpanan profil pengguna
export class UserProfilRepository {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

Layanan Tarif. Layanan untuk bekerja dengan tarif
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;
        });
    }
    
    /**
     * ...       
     */
}

Gudang Tarif. Repositori untuk bekerja dengan penyimpanan tarif
export class TariffRepository {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}


Sekarang cukup untuk menulis permintaan data sekali dan layanan apa pun akan dapat menggunakan kembali permintaan ini. Nanti kita akan melihat contoh bagaimana mendefinisikan ulang repositori tanpa menyentuh kode servis dan mengimplementasikan repositori mocha untuk pengujian.

Dalam layanan UserProfilService, mungkin tampaknya itu tidak diperlukan dan pengontrol dapat langsung mengakses repositori untuk data, tetapi ini tidak benar. Kapan saja, persyaratan dapat muncul atau berubah di lapisan bisnis, permintaan tambahan mungkin diperlukan, atau data dapat diperkaya. Oleh karena itu, bahkan ketika tidak ada logika di lapisan layanan, rantai Pengontrol - Layanan - Repositori harus dipertahankan. Ini merupakan kontribusi untuk hari esok Anda.

Sudah saatnya untuk mencari tahu seperti apa repositori yang akan ditetapkan, apakah semuanya benar. Lapisan Model bertanggung jawab untuk ini.

Model: DTO, Entitas, ViewModels


Model - bertanggung jawab atas deskripsi struktur yang digunakan aplikasi. Deskripsi seperti itu sangat membantu pengembang proyek baru untuk memahami apa yang bekerja dengan aplikasi. Selain itu, sangat nyaman untuk menggunakannya untuk membangun database atau memvalidasi data yang disimpan dalam model.

Model dibagi menjadi pola yang berbeda tergantung pada jenis penggunaan:
  • Entitas - bertanggung jawab untuk bekerja dengan database dan merupakan struktur yang mengulang tabel atau dokumen dalam database.
  • DTO (Data Transfer Object) - digunakan untuk mentransfer data di antara berbagai lapisan aplikasi.
  • ViewModel - berisi informasi yang disiapkan sebelumnya yang diperlukan untuk tampilan dalam tampilan.


Tambahkan model profil pengguna dan model lain ke aplikasi, dan beri tahu lapisan lain bahwa sekarang kita bekerja bukan dengan objek abstrak, tetapi dengan profil yang sangat spesifik:
UserPageController. Alih-alih ada, model yang dijelaskan digunakan.
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. Alih-alih, tentukan model yang dikembalikan
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();
    }
    
    /**
     * ...        
     */
}

Layanan Tarif. Alih-alih, tentukan model yang dikembalikan
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. Alih-alih, tentukan model yang dikembalikan
import { UserProfileDto } from "./UserProfileDto";

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

Gudang Tarif. Alih-alih, tentukan model yang dikembalikan
import { TariffDto } from "./TariffDto";

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

    /**
     * ...        
     */
}

UserProfileDto. Model dengan deskripsi data yang kami kerjakan
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;
    }

}

Tarif. Model dengan deskripsi data yang kami kerjakan
export class TariffDto {
    public ageFrom: number = 0;
    public ageTo: number = 0;
    public price: number = 0;
}


Sekarang tidak peduli apa lapisan aplikasi kita, kita tahu persis data apa yang kita kerjakan. Selain itu, karena deskripsi model, kami menemukan kesalahan dalam layanan kami. Dalam logika layanan, properti userProfile.age digunakan, yang sebenarnya tidak ada, tetapi memiliki tanggal lahir. Dan untuk menghitung umur, Anda harus memanggil metode model userProfile.getAge ().

Tapi ada satu masalah. Jika kami mencoba menggunakan metode dari model yang disediakan repositori saat ini, kami akan mendapatkan pengecualian. Masalahnya adalah metode response.json () dan JSON.parse ()Ini mengembalikan bukan model kami, tetapi objek JSON, yang sama sekali tidak terkait dengan model kami. Anda dapat memverifikasi ini jika Anda menjalankan perintah instance UserProfile dari UserProfileDto, Anda mendapatkan pernyataan yang salah. Untuk mengkonversi data yang diterima dari sumber eksternal ke model yang dijelaskan, ada proses deserialisasi data.

Deserialisasi data


Deserialization - proses mengembalikan struktur yang diperlukan dari urutan byte. Jika data berisi informasi yang tidak ditentukan dalam model, itu akan diabaikan. Jika ada informasi dalam data yang bertentangan dengan deskripsi model, kesalahan deserialisasi akan terjadi.

Dan hal yang paling menarik di sini adalah bahwa ketika merancang ES2015 dan menambahkan kata kunci kelas , mereka lupa untuk menambahkan deserialisasi ... Fakta bahwa dalam semua bahasa di luar kotak, di ES2015 mereka hanya lupa ...

Untuk menyelesaikan masalah ini, saya menulis perpustakaan TS-Serializable untuk deserialisasi , sebuah artikel tentang dapat dibaca di tautan ini . Tujuannya adalah mengembalikan fungsionalitas yang hilang.

Tambahkan dukungan deserialisasi dalam model dan deserialisasi itu sendiri ke repositori:
Gudang Tarif. Tambahkan proses deserialisasi
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); //  
    }
    
    /**
     * ...        
     */
}

Gudang Tarif. Tambahkan proses deserialisasi
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. Menambahkan Dukungan Deserialisasi
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;
    }

}

Tarif. Menambahkan Dukungan Deserialisasi
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;

}


Sekarang di semua lapisan aplikasi, Anda dapat benar-benar yakin bahwa kami bekerja dengan model yang kami harapkan. Dalam tampilan, pengontrol, dan lapisan lainnya, Anda dapat memanggil metode dari model yang dijelaskan.

Untuk apa Serializable dan jsonProperty?
Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .

Sekarang kami memiliki aplikasi yang hampir selesai. Sudah waktunya untuk menguji logika yang ditulis dalam lapisan Controller, Service, dan Models. Untuk melakukan ini, kita perlu mengembalikan data uji yang disiapkan khusus di lapisan Repositori alih-alih permintaan nyata ke server. Tetapi bagaimana cara mengganti Repositori tanpa menyentuh kode yang digunakan untuk produksi. Ada pola Injeksi Ketergantungan untuk ini.

Ketergantungan Injeksi Ketergantungan


Dependency Injection - menyuntikkan dependensi ke dalam Contoller, Service, lapisan Repositori dan memungkinkan Anda untuk menimpa dependensi ini di luar lapisan ini.

Dalam program ini, lapisan Pengendali tergantung pada lapisan Layanan, dan itu tergantung pada lapisan Repositori. Dalam bentuk saat ini, lapisan itu sendiri menyebabkan ketergantungan mereka melalui instantiation. Dan untuk mendefinisikan kembali ketergantungan, layer perlu mengatur ketergantungan ini dari luar. Ada banyak cara untuk melakukan ini, tetapi yang paling populer adalah melewati ketergantungan sebagai parameter dalam konstruktor.

Kemudian membuat program dengan semua dependensi akan terlihat seperti ini:
var programm = new IndexPageController(new ProfileService(new ProfileRepository()));

Setuju - terlihat mengerikan. Bahkan dengan mempertimbangkan bahwa hanya ada dua dependensi dalam program, itu sudah terlihat mengerikan. Apa yang harus dikatakan tentang program di mana ratusan dan ribuan dependensi.

Untuk menyelesaikan masalah, Anda memerlukan alat khusus, tetapi untuk ini Anda harus menemukannya. Jika kita beralih ke pengalaman platform lain, misalnya, Asp.Net Core, maka di sana pendaftaran dependensi terjadi pada tahap inisialisasi program dan terlihat seperti ini:
DI.register(IProfileService,ProfileService);

dan kemudian, saat membuat controller, framework itu sendiri akan membuat dan mengimplementasikan dependensi ini.

Tetapi ada tiga masalah signifikan:
  1. Saat mentranskripsikan Script dalam Javascript, tidak ada jejak yang tersisa dari antarmuka.
  2. Segala sesuatu yang masuk ke DI klasik tetap di dalamnya selamanya. Sangat sulit untuk membersihkannya selama refactoring. Dan dalam aplikasi web, Anda perlu menyimpan setiap byte.
  3. Hampir semua pustaka tampilan tidak menggunakan DI, dan desainer pengontrol sibuk dengan parameter.


Dalam aplikasi web, DI hanya digunakan dalam Angular 2+. Di Angular 1, saat mendaftarkan dependensi, alih-alih antarmuka, string digunakan; di InversifyJS, Simbol digunakan sebagai ganti antarmuka. Dan semua ini diimplementasikan sedemikian rupa sehingga lebih baik memiliki banyak hal baru seperti pada contoh pertama bagian ini daripada solusi ini.

Untuk mengatasi ketiga masalah tersebut, DI saya sendiri diciptakan, dan solusi untuknya membantu saya menemukan kerangka Java Spring dan dekorator autowired-nya. Deskripsi tentang cara kerja DI ini dapat ditemukan di artikel di tautan , dan repositori GitHub .

Saatnya untuk menerapkan DI yang dihasilkan dalam aplikasi kita.

Menyatukan semuanya


Untuk menerapkan DI pada semua lapisan, kami akan menambahkan dekorator refleksi, yang akan menyebabkan naskah untuk menghasilkan meta-informasi tambahan tentang jenis ketergantungan. Di controller di mana Anda perlu memanggil dependensi, kami akan menggantung dekorator autowired. Dan di tempat program diinisialisasi, kami menentukan di lingkungan mana ketergantungan akan diimplementasikan.

Untuk repositori UserProfilRepository, buat repositori yang sama, tetapi dengan data uji alih-alih permintaan yang sebenarnya. Hasilnya, kami mendapatkan kode berikut:
Main.ts. Lokasi Inisialisasi Program
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";

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

UserPageController. Ketergantungan melalui dekorator autowired
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. Memperkenalkan refleksi dan generasi ketergantungan
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();
    }

    /**
     * ...        
     */
}

Layanan Tarif. Memperkenalkan refleksi dan generasi ketergantungan
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. Memperkenalkan generasi refleksi
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. Repositori baru untuk pengujian
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); //   
    }

    /**
     * ...        
     */
}

Gudang Tarif. Memperkenalkan generasi refleksi
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);
        });
    }

    /**
     * ...        
     */
}


Sekarang, di mana saja dalam program ini ada peluang untuk mengubah implementasi logika apa pun. Dalam contoh kami, alih-alih permintaan profil pengguna nyata ke server di lingkungan pengujian, data uji akan digunakan.

Dalam kehidupan nyata, penggantian dapat ditemukan di mana saja, misalnya, Anda dapat mengubah logika dalam layanan, mengimplementasikan layanan lama dalam produksi, dan dalam refactoring itu sudah baru. Lakukan tes A / B dengan logika bisnis, ubah database berbasis dokumen menjadi relasional, dan umumnya ubah permintaan jaringan ke soket web. Dan semua ini tanpa menghentikan pengembangan untuk menulis ulang solusinya.

Saatnya untuk melihat hasil program. Ada layer View untuk ini.

Tampilan Penerapan


Lapisan tampilan bertanggung jawab untuk menyajikan data yang terkandung dalam lapisan pengontrol kepada pengguna. Dalam contoh, saya akan menggunakan Bereaksi untuk ini, tetapi sebagai gantinya bisa lain, misalnya Preact, Svelte, Vue, Mithril, WebComponent atau yang lainnya.

Untuk melakukan ini, cukup mewarisi pengontrol kami dari React.Component, dan tambahkan metode render ke sana dengan representasi tampilan:

Main.ts. Mulai menggambar komponen Bereaksi
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. Mewarisi dari React.Component dan menambahkan metode render
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>
            </>
        );
    }

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


Dengan menambahkan hanya dua baris dan template presentasi, controller kami berubah menjadi komponen reaksi dengan logika yang berfungsi.

Mengapa forceUpdate dipanggil bukannya setState?
forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .

Tetapi bahkan dalam implementasi seperti itu, ternyata kami terikat pada perpustakaan Bereaksi dengan siklus hidupnya, penerapan implementasi, dan prinsip pembatalan tampilan, yang bertentangan dengan konsep Arsitektur Bersih. Dan logika dan tata letaknya dalam file yang sama, yang menyulitkan kerja paralel dari penyetel dan pengembang.

Pemisahan Pengontrol dan Tampilan


Untuk mengatasi kedua masalah tersebut, kami menempatkan layer view ke dalam file terpisah, dan alih-alih komponen Bereaksi, kami membuat komponen dasar yang akan mengabstraksikan controller kami dari pustaka presentasi tertentu. Pada saat yang sama, kami menjelaskan atribut yang dapat diambil oleh komponen.

Kami mendapatkan perubahan berikut:
UserPageView. Mereka membawa tampilan ke file terpisah
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. Ambil tampilan dan Bereaksi terhadap file terpisah
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);
        }
    }

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

Komponen Base Komponen yang mengabstraksi kita dari kerangka kerja tertentu
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", {}, "  ");
        }
    }

}


Sekarang pandangan kita ada di file terpisah, dan controller tidak tahu apa-apa tentang itu, kecuali itu. Mungkin Anda tidak harus menyuntikkan tampilan melalui properti controller, tetapi melakukannya melalui dekorator seperti yang dilakukan Angular, tetapi ini adalah topik untuk beberapa pemikiran.

Komponen dasar juga mengandung abstraksi dari siklus hidup kerangka kerja. Mereka berbeda dalam semua kerangka kerja, tetapi mereka ada dalam semua kerangka kerja. Angular adalah ngOnInit, ngOnChanges, ngOnDestroy. Di Bereaksi dan Bertindak, ini adalah componentDidMount, shouldComponentUpdate, componentWillUnmount. Di Vue, ini dibuat, diperbarui, dihancurkan. Di Mithril itu oncreate, onupdate, onremove. Di WebComponents, ini terhubungCallback, atributChangedCallback, disconnectectedCallback. Demikian juga di setiap perpustakaan. Sebagian besar bahkan memiliki antarmuka yang sama atau mirip.

Selain itu, sekarang komponen perpustakaan dapat diperluas dengan logikanya sendiri untuk digunakan kembali selanjutnya di antara semua komponen. Misalnya, memperkenalkan alat untuk analisis, pemantauan, pencatatan, dll.

Kami melihat hasilnya


Tetap hanya untuk mengevaluasi apa yang terjadi. Seluruh program memiliki bentuk akhir sebagai berikut:
Main.ts. File dari mana program diluncurkan
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. Representasi salah satu komponen program.
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. Logika salah satu komponen untuk interaksi pengguna
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);
        }
    }

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

Komponen Base Kelas dasar untuk semua komponen program
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. Layanan untuk menggunakan kembali logika antar komponen agar bekerja dengan profil pengguna
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();
    }

    /**
     * ...        
     */
}

Layanan Tarif. Layanan untuk menggunakan kembali logika antar komponen untuk bekerja dengan tarif
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. Repositori untuk menerima profil dari server, memeriksa dan memvalidasinya
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;

}


Sebagai hasilnya, kami mendapatkan aplikasi skalabel modular dengan jumlah boilerplate yang sangat kecil (1 komponen dasar, 3 baris untuk mengimplementasikan dependensi per kelas) dan overhead yang sangat rendah (sebenarnya hanya untuk mengimplementasikan dependensi, yang lainnya adalah logika). Selain itu, kami tidak terikat dengan perpustakaan presentasi mana pun. Ketika Angular 1 meninggal, banyak yang mulai menulis ulang aplikasi di Bereaksi. Ketika pengembang untuk Angular 2 kehabisan, banyak perusahaan mulai menderita karena kecepatan pengembangan. Ketika Bereaksi mati lagi, Anda harus menulis ulang solusi yang terkait dengan kerangka dan ekosistemnya. Tetapi dengan Arsitektur Chita Anda bisa melupakan tentang mengikat kerangka kerja.

Apa keuntungan dari Redux?


Untuk memahami perbedaannya, mari kita lihat bagaimana Redux berperilaku ketika aplikasi tumbuh.
Redux

Seperti yang dapat Anda lihat dari diagram, dengan pertumbuhan aplikasi Redux diskalakan secara vertikal, Store dan jumlah Reduksi juga meningkat dan berubah menjadi hambatan. Dan jumlah overhead untuk membangun kembali Store dan menemukan Reducer yang tepat mulai melebihi muatan.

Anda dapat memeriksa rasio overhead ke payload pada aplikasi berukuran sedang dengan tes sederhana.
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

Untuk membuat ulang Store di 100 properti, dibutuhkan 8 kali lebih banyak waktu daripada logika itu sendiri. Dengan 1000 elemen, ini sudah 50 kali lebih banyak. Selain itu, satu tindakan pengguna dapat menelurkan seluruh rangkaian tindakan, yang panggilannya sulit ditangkap dan didebug. Anda tentu dapat berpendapat bahwa 0,04 ms untuk membuat ulang Toko sangat kecil dan tidak akan melambat. Tetapi 0,04 ms ada pada prosesor Core i7 dan untuk satu tindakan. Mengingat prosesor seluler yang lebih lemah dan fakta bahwa satu tindakan pengguna tunggal dapat menelurkan puluhan tindakan, semua ini mengarah pada fakta bahwa perhitungan tidak masuk ke dalam 16 ms dan perasaan dibuat bahwa aplikasi melambat.

Mari kita bandingkan bagaimana aplikasi Arsitektur Bersih tumbuh:
Arsitektur Murni

Seperti dapat dilihat karena pemisahan logika dan tanggung jawab antara lapisan, aplikasi berskala horizontal. Jika ada komponen yang perlu memproses data, itu akan beralih ke layanan yang sesuai tanpa menyentuh layanan yang tidak terkait dengan tugasnya. Setelah menerima data, hanya satu komponen yang akan digambar ulang. Rantai pemrosesan data sangat singkat dan jelas, dan untuk maksimum 4 lompatan kode, Anda dapat menemukan logika yang diperlukan dan men-debug-nya. Selain itu, jika kita menggambar analogi dengan dinding bata, maka kita dapat menghilangkan bata apa pun dari dinding ini dan menggantinya dengan yang lain tanpa mempengaruhi stabilitas dinding ini.

Bonus 1: Peningkatan fungsionalitas komponen kerangka kerja


Sebagai bonus, mereka mendapat kesempatan untuk menambah atau mengubah perilaku komponen perpustakaan presentasi. Misalnya, reaksi jika terjadi kesalahan dalam tampilan tidak membuat seluruh aplikasi, jika sedikit revisi dilakukan:
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}`
            );
        }
    }

}

Sekarang reaksi tidak akan menarik hanya komponen di mana kesalahan terjadi. Dengan cara sederhana yang sama, Anda dapat menambahkan pemantauan, analitik, dan nishtyaki lainnya.

Bonus 2: Validasi Data


Selain menggambarkan data dan mentransfer data antar lapisan, model memiliki margin peluang yang besar. Misalnya, jika Anda menghubungkan perpustakaan kelas-validator , maka dengan hanya menggantung dekorator, Anda dapat memvalidasi data dalam model ini, termasuk dengan sedikit perbaikan, Anda dapat memvalidasi formulir web.

Bonus 3: Membuat Entitas


Selain itu, jika Anda perlu bekerja dengan database lokal, Anda dapat menghubungkan pustaka typeorm dan model Anda akan berubah menjadi entitas yang dengannya database akan dihasilkan dan dijalankan.

Terimakasih atas perhatiannya


Jika Anda menyukai artikel atau pendekatan tersebut, sukai dan jangan takut untuk bereksperimen. Jika Anda seorang penganut Redux dan Anda tidak suka perbedaan pendapat, tolong jelaskan di komentar bagaimana Anda mengukur, menguji, dan memvalidasi data dalam aplikasi Anda.

All Articles