شبكة العمارة لتطبيقات الويب

أريد أن أشارككم نهجًا استخدمته لسنوات عديدة في تطوير التطبيقات ، بما في ذلك تطبيقات الويب. العديد من مطوري تطبيقات سطح المكتب والخادم والجوّال على دراية بهذا النهج. أمر أساسي عند إنشاء مثل هذه التطبيقات ، ومع ذلك ، يتم تمثيلها بشكل سيء للغاية على الويب ، على الرغم من أن هناك بالتأكيد أشخاصًا يريدون استخدام هذا النهج. بالإضافة إلى ذلك ، يتم كتابة محرر VS Code على هذا النهج .

العمارة البحتة

نتيجة لتطبيق هذا النهج ، سوف تتخلص من إطار عمل محدد. يمكنك بسهولة تبديل مكتبة العرض داخل التطبيق الخاص بك ، على سبيل المثال React و Preact و Vue و Mithril دون إعادة كتابة منطق الأعمال وحتى في معظم الحالات طرق العرض. إذا كان لديك تطبيق على Angular 1 ، يمكنك بسهولة ترجمته إلى Angular 2+ أو React أو Svelte أو WebComponents أو حتى مكتبة العروض التقديمية. إذا كان لديك تطبيق على Angular 2+ ، ولكن لا يوجد متخصصون فيه ، فيمكنك بسهولة نقل التطبيق إلى مكتبة أكثر شيوعًا دون إعادة كتابة منطق الأعمال. ولكن في النهاية ، ننسى تمامًا مشكلة الهجرة من الإطار إلى الإطار. أي نوع من السحر هذا؟

ما هي العمارة النظيفة


لفهم ذلك ، من الأفضل قراءة كتاب مارتن روبرت "العمارة النظيفة" ( بقلم روبرت سي مارتن "العمارة النظيفة" ). مقتطفات مقتضبة منه ترد في المقالة بالإشارة .

الأفكار الرئيسية المضمنة في العمارة:

  1. الاستقلال عن الإطار. العمارة لا تعتمد على وجود أي مكتبة. يتيح لك هذا استخدام إطار العمل كأداة ، بدلاً من الضغط على نظامك في حدوده.
  2. قابلية الاختبار. يمكن اختبار قواعد العمل بدون واجهة مستخدم أو قاعدة بيانات أو خادم ويب أو أي مكون خارجي آخر.
  3. الاستقلال عن واجهة المستخدم. يمكن تغيير واجهة المستخدم بسهولة دون تغيير بقية النظام. على سبيل المثال ، يمكن استبدال واجهة الويب بوحدة التحكم ، دون تغيير قواعد العمل.
  4. الاستقلال عن قاعدة البيانات. يمكنك استبدال Oracle أو SQL Server بـ MongoDB أو BigTable أو CouchDB أو أي شيء آخر. قواعد عملك لا تتعلق بقاعدة البيانات.
  5. الاستقلال عن أي خدمة خارجية. في الواقع ، قواعد عملك ببساطة لا تعرف أي شيء عن العالم الخارجي.

كانت الأفكار الموضحة في هذا الكتاب لسنوات عديدة الأساس لبناء تطبيقات معقدة في مختلف المجالات.

يتم تحقيق هذه المرونة من خلال تقسيم التطبيق إلى طبقات الخدمة ، والمستودع ، والنموذج. أضفت نهج MVC إلى الهندسة المعمارية النظيفة وحصلت على الطبقات التالية:

  • عرض - يعرض البيانات للعميل ، ويتخيل بالفعل حالة المنطق للعميل.
  • جهاز التحكم - مسؤول عن التفاعل مع المستخدم من خلال الإدخال / الإخراج (IO).
  • الخدمة - مسؤولة عن منطق الأعمال وإعادة استخدامه بين المكونات.
  • مستودع - مسؤول عن استقبال البيانات من مصادر خارجية ، مثل قاعدة البيانات ، واجهة برمجة التطبيقات ، التخزين المحلي ، إلخ.
  • النماذج - هي المسؤولة عن نقل البيانات بين الطبقات والأنظمة ، وكذلك منطق معالجة هذه البيانات.

يتم مناقشة الغرض من كل طبقة أدناه.

من هو العمارة النقية


لقد قطعت عملية تطوير الويب شوطًا طويلاً ، بدءًا من البرمجة النصية البسيطة إلى تطوير تطبيقات SPA الكبيرة. والآن أصبحت تطبيقات الويب كبيرة جدًا بحيث أصبح مقدار منطق الأعمال قابلاً للمقارنة أو حتى أعلى من تطبيقات الخادم وسطح المكتب والجوال.

بالنسبة للمطورين الذين يكتبون تطبيقات معقدة وكبيرة ، بالإضافة إلى نقل منطق الأعمال من الخادم إلى تطبيقات الويب لتوفير تكلفة الخوادم ، ستساعد Clean Clean في تنظيم الشفرة والقياس دون مشاكل على نطاق واسع.

في الوقت نفسه ، إذا كانت مهمتك هي مجرد تخطيط ورسوم متحركة للصفحات المقصودة ، فإن Clean Architecture ببساطة ليس لديها مكان لإدراجه. إذا كان منطق عملك على الواجهة الخلفية ومهمتك هي الحصول على البيانات ، وعرضها على العميل ومعالجة النقر على الزر ، فلن تشعر بمرونة الهندسة المعمارية النظيفة ، ولكنها يمكن أن تكون نقطة انطلاق ممتازة للنمو الهائل للتطبيق.

أين تطبق بالفعل؟


لا ترتبط الهندسة البحتة بأي إطار عمل أو نظام أساسي أو لغة برمجة معينة. لعقود ، تم استخدامه لكتابة تطبيقات سطح المكتب. يمكن العثور على تطبيقه المرجعي في أطر تطبيقات الخادم Asp.Net Core و Java Spring و NestJS. كما أنها تحظى بشعبية كبيرة عند كتابة تطبيقات iOs و Android. ولكن في تطوير الويب ، ظهر بشكل فاشل للغاية في الأطر الزاويّة.

نظرًا لأنني أنا لست فقط Typescript ، ولكن أيضًا مطور C # ، على سبيل المثال ، سأخذ التنفيذ المرجعي لهذه البنية لـ Asp.Net Core.

هنا تطبيق عينة مبسط:

تطبيق نموذجي على 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; }
    }


إذا كنت لا تفهم أنه لا يقول شيئًا سيئًا ، فسنقوم بتحليله في أجزاء كل جزء.

تم تقديم مثال لتطبيق Asp.Net Core ، ولكن بالنسبة لـ Java Spring و WinForms و Android و React ، ستكون البنية والتعليمة هي نفسها ، فقط اللغة والعمل مع طريقة العرض (إن وجدت) ستتغير.

تطبيق ويب


الإطار الوحيد الذي حاول استخدام العمارة النظيفة كان الزاوي. ولكن اتضح أنه مروع ، في 1 ، في 2+.

وهناك أسباب عديدة لذلك:

  1. إطار متجانسة الزاوية. وهذه هي مشكلته الرئيسية. إذا كنت لا تحب شيئًا فيه ، فعليك الاختناق به يوميًا ، ولا يوجد شيء يمكنك القيام به حيال ذلك. ليس فقط هناك الكثير من الأماكن التي بها مشاكل ، بل يتعارض أيضًا مع أيديولوجية العمارة النقية.
  2. DI. , Javascript.
  3. . JSX. , 6, . .
  4. . Rollup ES2015 2 4 , angular 9.
  5. والعديد من المشاكل. بشكل عام ، حتى لفات التكنولوجيا الحديثة الزاويّة مع تأخير لمدة 5 سنوات بالنسبة إلى React.

ولكن ماذا عن الأطر الأخرى؟ React و Vue و Preact و Mithril وغيرها عبارة عن مكتبات تمثيل حصريًا ولا تقدم أي بنية ... ولدينا بالفعل بنية ... يبقى تجميع كل شيء في وحدة واحدة!

نبدأ في إنشاء تطبيق


سننظر في Pure Architecture من خلال مثال على تطبيق وهمي أقرب ما يمكن من تطبيق ويب حقيقي قدر الإمكان. هذا هو مكتب في شركة التأمين يعرض ملف تعريف المستخدم والأحداث المؤمنة ومعدلات التأمين المقترحة وأدوات للعمل مع هذه البيانات.

النموذج الأولي للتطبيق

في المثال ، سيتم تنفيذ جزء صغير فقط من الوظيفة ، ولكن من خلاله يمكنك فهم مكان وكيفية وضع بقية الوظائف. لنبدأ في إنشاء التطبيق من طبقة وحدة التحكم ، وربط طبقة العرض في النهاية. وفي سياق الخلق ، نعتبر كل طبقة بمزيد من التفصيل.

نمط التحكم


المراقب المالي - مسؤول عن تفاعل المستخدم مع التطبيق. يمكن أن يكون هذا نقرة على زر على صفحة ويب ، أو تطبيق سطح مكتب ، أو تطبيق محمول ، أو إدخال أمر في وحدة تحكم Linux ، أو طلب شبكة ، أو أي حدث IO آخر قادم إلى التطبيق.

أبسط جهاز تحكم في بنية نظيفة كما يلي:

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

}

وتتمثل مهمتها في تلقي حدث من المستخدم وبدء العمليات التجارية. في الحالة المثالية ، لا تعرف أداة التحكم أي شيء عن العرض ، ومن ثم يمكن إعادة استخدامها بين الأنظمة الأساسية ، مثل الويب أو React-Native أو Electron.

الآن دعنا نكتب وحدة تحكم لتطبيقنا. وتتمثل مهمتها في الحصول على ملف تعريف المستخدم والتعريفات المتاحة وتقديم أفضل تعريفة للمستخدم:

UserPageController. تحكم مع منطق الأعمال
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;
            });
        }
    }

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


حصلنا على وحدة تحكم منتظمة بدون بنية نظيفة ، إذا ورثناها من React. المكون نحصل على مكون يعمل بالمنطق. يكتب الكثير من مطوري تطبيقات الويب ، ولكن هذا النهج له العديد من العوائق الهامة. العامل الرئيسي هو عدم القدرة على إعادة استخدام المنطق بين المكونات. بعد كل شيء ، يمكن عرض التعرفة الموصى بها ليس فقط في حسابك الشخصي ، ولكن أيضًا على الصفحة المقصودة والعديد من الأماكن الأخرى لجذب العميل إلى الخدمة.

لكي تتمكن من إعادة استخدام المنطق بين المكونات ، من الضروري وضعه في طبقة خاصة تسمى الخدمة.

نمط الخدمة


الخدمة - مسؤولة عن منطق العمل الكامل للتطبيق. إذا احتاجت وحدة التحكم لاستلام بعض البيانات ومعالجتها وإرسالها - فإنها تفعل ذلك من خلال الخدمة. إذا كانت هناك عدة وحدات تحكم تحتاج إلى نفس المنطق ، فإنها تعمل مع الخدمة. لكن طبقة الخدمة نفسها لا يجب أن تعرف أي شيء عن طبقة التحكم والعرض والبيئة التي تعمل فيها.

دعنا ننقل المنطق من وحدة التحكم إلى الخدمة وننفذ الخدمة في وحدة التحكم:

UserPageController. تحكم بدون منطق عمل
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. خدمة للعمل مع ملف تعريف المستخدم
export class UserProfilService {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

خدمة التعرفة. خدمة للعمل مع التعريفات
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;
        });
    }
    
    /**
     * ...       
     */
}


الآن ، إذا احتاج العديد من وحدات التحكم إلى ملف تعريف مستخدم أو تعريفات ، فيمكنهم إعادة استخدام نفس المنطق من الخدمات. في الخدمات ، الشيء الرئيسي هو عدم نسيان مبادئ SOLID وأن كل خدمة مسؤولة عن مجال مسؤوليتها. في هذه الحالة ، تكون إحدى الخدمات مسؤولة عن العمل مع ملف تعريف المستخدم ، وخدمة أخرى مسؤولة عن العمل مع التعريفات.

ولكن ماذا لو تغير مصدر البيانات ، على سبيل المثال ، يمكن أن يتغير الجلب إلى websocket أو grps أو قاعدة البيانات ، ويجب استبدال البيانات الحقيقية ببيانات الاختبار؟ وبشكل عام ، لماذا يحتاج منطق الأعمال إلى معرفة شيء عن مصدر البيانات؟ لحل هذه المشاكل ، توجد طبقة مستودع.

نمط المستودع


مستودع - مسؤول عن الاتصال بمخزن البيانات. يمكن أن يكون التخزين خادمًا أو قاعدة بيانات أو ذاكرة أو تخزين محلي أو تخزين جلسة أو أي تخزين آخر. وتتمثل مهمتها في تلخيص طبقة الخدمة من تنفيذ التخزين المحدد.

دعنا نقدم طلبات الشبكة من الخدمات في المستودع ، بينما لا تتغير وحدة التحكم:
UserProfilService. خدمة للعمل مع ملف تعريف المستخدم
import { UserProfilRepository } from "./UserProfilRepository";

export class UserProfilService {

    private readonly userProfilRepository: UserProfilRepository =
        new UserProfilRepository();

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

UserProfilRepository. خدمة للعمل مع تخزين ملف تعريف المستخدم
export class UserProfilRepository {
    public async getUserProfile(): Promise<any> { //  
        const response = await fetch("./api/user-profile");
        return await response.json();
    }
    
    /**
     * ...        
     */
}

خدمة التعرفة. خدمة للعمل مع التعريفات
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;
        });
    }
    
    /**
     * ...       
     */
}

مستودع التعريفة. مستودع للعمل مع تخزين التعريفات
export class TariffRepository {
    //  
    public async getTariffs(): Promise<any> {
        const response = await fetch("./api/tariffs");
        return await response.json();
    }

    /**
     * ...        
     */
}


يكفي الآن كتابة طلب بيانات مرة واحدة وستتمكن أي خدمة من إعادة استخدام هذا الطلب. في وقت لاحق ، سنلقي نظرة على مثال لكيفية إعادة تعريف المستودع دون لمس رمز الخدمة وتنفيذ مستودع المخاوي للاختبار.

في خدمة UserProfilService ، قد يبدو أنه غير مطلوب ويمكن لوحدة التحكم الوصول مباشرة إلى مستودع البيانات ، ولكن هذا ليس كذلك. في أي وقت ، قد تظهر المتطلبات أو تتغير في طبقة الأعمال ، قد تكون هناك حاجة إلى طلب إضافي ، أو قد يتم إثراء البيانات. لذلك ، حتى في حالة عدم وجود منطق في طبقة الخدمة ، يجب الحفاظ على وحدة التحكم - الخدمة - سلسلة المستودع. هذه مساهمة لغدك.

لقد حان الوقت لمعرفة أي نوع من المستودعات يتم تعيينه ، سواء كانت صحيحة على الإطلاق. طبقة الطرازات مسؤولة عن ذلك.

النماذج: DTO والكيانات و ViewModels


النماذج - هي المسؤولة عن وصف الهياكل التي يعمل بها التطبيق. يساعد هذا الوصف إلى حد كبير مطوري المشاريع الجدد على فهم ما يعمل به التطبيق. بالإضافة إلى ذلك ، من السهل جدًا استخدامه لبناء قواعد البيانات أو التحقق من صحة البيانات المخزنة في النموذج.

تنقسم النماذج إلى أنماط مختلفة اعتمادًا على نوع الاستخدام:
  • الكيانات - هي المسؤولة عن العمل مع قاعدة البيانات وهي هيكل يكرر جدولًا أو مستندًا في قاعدة البيانات.
  • DTO (كائن نقل البيانات) - تُستخدم لنقل البيانات بين طبقات التطبيق المختلفة.
  • ViewModel - يحتوي على معلومات معدة مسبقًا ضرورية للعرض في العرض.


أضف نموذج ملف تعريف المستخدم والنماذج الأخرى إلى التطبيق ، واسمح للطبقات الأخرى بمعرفة أننا لا نعمل الآن مع كائن مجرد ، ولكن مع ملف تعريف محدد للغاية:
UserPageController. بدلاً من أي ، يتم استخدام النماذج الموصوفة.
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. بدلاً من أي ، حدد النموذج الذي تم إرجاعه
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();
    }
    
    /**
     * ...        
     */
}

خدمة التعرفة. بدلاً من أي ، حدد النموذج الذي تم إرجاعه
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. بدلاً من أي ، حدد النموذج الذي تم إرجاعه
import { UserProfileDto } from "./UserProfileDto";

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

مستودع التعريفة. بدلاً من أي ، حدد النموذج الذي تم إرجاعه
import { TariffDto } from "./TariffDto";

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

    /**
     * ...        
     */
}

UserProfileDto. نموذج مع وصف للبيانات التي نعمل معها
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;
    }

}

التعريفة. نموذج مع وصف للبيانات التي نعمل معها
export class TariffDto {
    public ageFrom: number = 0;
    public ageTo: number = 0;
    public price: number = 0;
}


الآن بغض النظر عن طبقة التطبيق التي نستخدمها ، نحن نعرف بالضبط البيانات التي نعمل معها. أيضًا ، نظرًا لوصف النموذج ، وجدنا خطأً في خدمتنا. في منطق الخدمة ، تم استخدام خاصية userProfile.age ، وهي في الواقع غير موجودة ، ولكن لها تاريخ ميلاد. ولحساب العمر ، يجب استدعاء طريقة نموذج userProfile.getAge ().

ولكن هناك مشكلة واحدة. إذا حاولنا استخدام الطرق من النموذج الذي قدمه المستودع الحالي ، فسوف نحصل على استثناء. الشيء هو أن أساليب response.json () و JSON.parse ()لا تُرجع نموذجنا ، ولكن كائن JSON ، الذي لا يرتبط بأي شكل بنموذجنا. يمكنك التحقق من ذلك إذا قمت بتنفيذ الأمر userProfile مثيل UserProfileDto ، فستحصل على بيان خطأ. من أجل تحويل البيانات الواردة من مصدر خارجي إلى النموذج الموصوف ، هناك عملية لإلغاء تسلسل البيانات.

إلغاء تسلسل البيانات


Deserialization - عملية استعادة البنية اللازمة من سلسلة من البايت. إذا كانت البيانات تحتوي على معلومات غير محددة في النماذج ، فسيتم تجاهلها. إذا كانت هناك معلومات في البيانات تتعارض مع وصف النموذج ، فسيحدث خطأ في إلغاء التسلسل.

والشيء الأكثر إثارة للاهتمام هنا هو أنه عند تصميم ES2015 وإضافة الكلمة الأساسية للصف ، نسوا إضافة إزالة التسلسل ... حقيقة أنهم في جميع اللغات خارج الصندوق ، في ES2015 ببساطة نسيوا ...

لحل هذه المشكلة ، كتبت مكتبة TS-Serializable لإزالة التسلسل ، مقالة حول أي يمكن قراءتها على هذا الرابط . والغرض منه إرجاع الوظيفة المفقودة.

أضف دعم إلغاء التسلسل في النموذج وإزالة التسلسل نفسه إلى المستودع:
مستودع التعريفة. أضف عملية إزالة التسلسل
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); //  
    }
    
    /**
     * ...        
     */
}

مستودع التعريفة. أضف عملية إزالة التسلسل
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); //  
        });
    }

    /**
     * ...        
     */
}

الملف الشخصي إضافة دعم إلغاء التسلسل
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;
    }

}

التعريفة. إضافة دعم إلغاء التسلسل
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;

}


الآن في جميع طبقات التطبيق ، يمكنك التأكد تمامًا من أننا نعمل مع النماذج التي نتوقعها. في العرض ، وحدة التحكم ، والطبقات الأخرى ، يمكنك استدعاء طرق النموذج الموصوف.

ما هي المسلسل و jsonProperty؟
Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .

الآن لدينا تطبيق انتهى تقريبا. حان الوقت لاختبار المنطق المكتوب في طبقات وحدة التحكم والخدمة والنماذج. للقيام بذلك ، نحتاج إلى إرجاع بيانات اختبار معدة خصيصًا في طبقة المستودع بدلاً من طلب حقيقي إلى الخادم. ولكن كيفية استبدال المستودع دون لمس الرمز الذي يدخل حيز الإنتاج. هناك نمط حقن التبعية لذلك.

حقن التبعية - حقن التبعية


حقن التبعية - يضخ التبعيات في طبقات Contoller ، Service ، Repository ، ويسمح لك بتجاوز هذه التبعيات خارج هذه الطبقات.

في البرنامج ، تعتمد طبقة جهاز التحكم على طبقة الخدمة ، وتعتمد على طبقة المستودع. في الشكل الحالي ، تسبب الطبقات نفسها تبعياتها من خلال الاستنساخ. ولإعادة تعريف التبعية ، تحتاج الطبقة إلى ضبط هذا الاعتماد من الخارج. هناك العديد من الطرق للقيام بذلك ، ولكن الأكثر شيوعًا هو تمرير التبعية كمعلمة في المنشئ.

ثم سيبدو إنشاء برنامج بكل التبعيات كما يلي:
var programm = new IndexPageController(new ProfileService(new ProfileRepository()));

أوافق - يبدو فظيعًا. حتى مع الأخذ في الاعتبار أن هناك تبعيتين فقط في البرنامج ، فإنه يبدو مروعًا بالفعل. ماذا تقول عن البرامج التي فيها المئات والآلاف من التبعيات.

لحل المشكلة ، تحتاج إلى أداة خاصة ، ولكن عليك إيجادها لهذا الغرض. إذا انتقلنا إلى تجربة المنصات الأخرى ، على سبيل المثال ، Asp.Net Core ، عندها يحدث تسجيل التبعيات في مرحلة التهيئة من البرنامج ويبدو شيئًا مثل هذا:
DI.register(IProfileService,ProfileService);

وبعد ذلك ، عند إنشاء وحدة التحكم ، سيقوم الإطار نفسه بإنشاء وتنفيذ هذه التبعية.

ولكن هناك ثلاث مشاكل كبيرة:
  1. عند نقل الكتابة النصية في Javascript ، لا يوجد أي أثر متبقي للواجهات.
  2. كل شيء وقع في DI الكلاسيكي يبقى فيه إلى الأبد. من الصعب جدًا تنظيفه أثناء إعادة الهيكلة. وفي تطبيق ويب ، تحتاج إلى حفظ كل بايت.
  3. لا تستخدم مكتبات العرض تقريبًا DI ، ومصممو وحدة التحكم مشغولون بالمعلمات.


في تطبيقات الويب ، يتم استخدام DI فقط في Angular 2+. في Angular 1 ، عند تسجيل التبعيات ، بدلاً من الواجهة ، تم استخدام سلسلة ؛ في InversifyJS ، يتم استخدام الرمز بدلاً من الواجهة. وكل هذا يتم تنفيذه بشكل فظيع لدرجة أنه من الأفضل أن يكون لديك الكثير من الجديد كما هو الحال في المثال الأول من هذا القسم من هذه الحلول.

لحل جميع المشاكل الثلاث ، تم اختراع DI الخاص بي ، وساعدني الحل في العثور على إطار عمل Java Spring والديكور المستقل. وصف للكيفية التي يمكن أن يعمل هذا DI العثور عليها في المادة على الرابط و مستودع جيثب .

حان الوقت لتطبيق DI الناتج في طلبنا.

ضع كل شيء معا


لتنفيذ DI على جميع الطبقات ، سنضيف مُزيِّن انعكاسي ، مما سيؤدي إلى قيام Typcript بإنشاء معلومات تعريفية إضافية حول أنواع التبعيات. في وحدة التحكم حيث تحتاج إلى استدعاء التبعيات ، سنقوم بتعليق الديكور ذاتي التزويد. وفي المكان الذي تتم فيه تهيئة البرنامج ، نحدد في أي بيئة سيتم تنفيذ التبعية.

بالنسبة لمستودع UserProfilRepository ، قم بإنشاء نفس المستودع ، ولكن باستخدام بيانات الاختبار بدلاً من الطلب الفعلي. ونتيجة لذلك ، نحصل على الكود التالي:
الرئيسية. موقع تهيئة البرنامج
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";

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

UserPageController. الاعتماد من خلال الديكور 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. إدخال التفكير والتوليد التبعي
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();
    }

    /**
     * ...        
     */
}

خدمة التعرفة. إدخال التفكير والتوليد التبعي
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. إدخال جيل التفكير
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);
    }

    /**
     * ...        
     */
}

مستودع MockUserProfil. مستودع جديد للاختبار
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); //   
    }

    /**
     * ...        
     */
}

مستودع التعريفة. إدخال جيل التفكير
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);
        });
    }

    /**
     * ...        
     */
}


الآن ، في أي مكان في البرنامج هناك فرصة لتغيير تنفيذ أي منطق. في مثالنا ، بدلاً من طلب ملف تعريف مستخدم حقيقي إلى الخادم في بيئة الاختبار ، سيتم استخدام بيانات الاختبار.

في الحياة الواقعية ، يمكن العثور على الاستبدالات في أي مكان ، على سبيل المثال ، يمكنك تغيير المنطق في الخدمة ، وتنفيذ الخدمة القديمة في الإنتاج ، وفي إعادة هيكلة جديدة بالفعل. قم بإجراء اختبارات A / B باستخدام منطق الأعمال ، وقم بتغيير قاعدة البيانات المستندة إلى المستندات إلى العلاقات ، وقم عمومًا بتغيير طلب الشبكة إلى مآخذ الويب. وكل هذا دون إيقاف التطوير لإعادة كتابة الحل.

حان الوقت لرؤية نتيجة البرنامج. هناك طبقة عرض لهذا الغرض.

تنفيذ العرض


إن طبقة العرض مسؤولة عن تقديم البيانات الموجودة في طبقة وحدة التحكم للمستخدم. في المثال ، سأستخدم React لهذا ، ولكن يمكن أن يكون مكانه أي مكان آخر ، على سبيل المثال Preact أو Svelte أو Vue أو Mithril أو WebComponent أو أي دولة أخرى.

للقيام بذلك ، ببساطة ترث وحدة التحكم الخاصة بنا من React.Component ، وأضف طريقة تقديم لها مع تمثيل العرض:

الرئيسية. يبدأ رسم مكون رد فعل
import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
import { UserPageController } from "./UserPageController";
import React from "react";
import { render } from "react-dom";

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

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

UserPageController. يرث من React.Component ويضيف طريقة تقديم
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>
            </>
        );
    }

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


بإضافة سطرين فقط ونموذج عرض تقديمي ، تحولت وحدة التحكم الخاصة بنا إلى مكون تفاعل مع منطق العمل.

لماذا يتم استدعاء forceUpdate بدلاً من setState؟
forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .

ولكن حتى في مثل هذا التنفيذ ، اتضح أننا ربطنا بمكتبة التفاعل بدورة حياتها ، وعرض التنفيذ ، ومبدأ إبطال الرأي ، الذي يتعارض مع مفهوم العمارة النظيفة. والمنطق والتخطيط موجودان في نفس الملف ، مما يعقد العمل الموازي للطباع والمطور.

فصل المراقب والعرض


لحل كلتا المشكلتين ، نضع طبقة العرض في ملف منفصل ، وبدلاً من مكون React ، نصنع المكون الأساسي الذي سيُلخص وحدة التحكم الخاصة بنا من مكتبة عرض تقديمي معينة. في نفس الوقت ، نصف السمات التي يمكن أن يأخذها المكون.

نحصل على التغييرات التالية:
UserPageView. أخذوا الرأي إلى ملف منفصل
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. انظر إلى ملف منفصل وتفاعل معه
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 مكون يجردنا من إطار معين
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", {}, "  ");
        }
    }

}


الآن وجهة نظرنا في ملف منفصل ، ووحدة التحكم لا تعرف شيئًا عنها ، باستثناء أنها كذلك. ربما لا يجب حقن العرض من خلال خاصية وحدة التحكم ، ولكن قم بذلك من خلال الديكور كما تفعل Angular ، ولكن هذا موضوع لبعض الفكر.

يحتوي المكون الأساسي أيضًا على تجريد من دورة حياة الإطار. إنهم مختلفون في جميع الأطر ، لكنهم في جميع الأطر. الزاوي هو ngOnInit ، ngOnChanges ، ngOnDestroy. في React و Preact ، هذا هو مكون رقم تعريف ، يجب أن يكون مكونًا تحديثًا ، مكون ، جزء من المبلغ. في Vue ، يتم إنشاء هذا وتحديثه وتدميره. في Mithril هو oncreate ، onupdate ، onremove. في WebComponents ، هذا متصل: CallCallback ، attribChangedCallback ، disconnectedCallback. وهكذا في كل مكتبة. معظمها لديه نفس الواجهة أو ما شابه ذلك.

بالإضافة إلى ذلك ، يمكن الآن توسيع مكونات المكتبة بمنطقها الخاص لإعادة استخدامها لاحقًا بين جميع المكونات. على سبيل المثال ، تقديم أدوات للتحليل والرصد وتسجيل الدخول ، إلخ.

نحن ننظر إلى النتيجة


يبقى فقط لتقييم ما حدث. يحتوي البرنامج بأكمله على الشكل النهائي التالي:
الرئيسية. الملف الذي تم تشغيل البرنامج منه
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. تمثيل أحد مكونات البرنامج.
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. منطق أحد مكونات تفاعل المستخدم
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 الفئة الأساسية لجميع مكونات البرنامج
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. خدمة لإعادة استخدام المنطق بين المكونات للعمل مع ملف تعريف المستخدم
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();
    }

    /**
     * ...        
     */
}

خدمة التعرفة. خدمة لإعادة استخدام المنطق بين المكونات للعمل مع التعريفات
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. مستودع لتلقي ملف تعريف من الخادم والتحقق منه والتحقق منه
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;

}


ونتيجة لذلك ، حصلنا على تطبيق معياري قابل للتطوير مع كمية صغيرة جدًا من الصفيحة (مكون أساسي واحد ، و 3 أسطر لتنفيذ التبعيات لكل فئة) ونفقات عامة منخفضة جدًا (في الواقع فقط لتنفيذ التبعيات ، كل شيء آخر هو منطق). أيضا ، نحن لسنا مرتبطين بأي مكتبة عرض. عندما مات Angular 1 ، بدأ العديد من إعادة كتابة التطبيقات في React. عندما نفد مطورو Angular 2 ، بدأت العديد من الشركات تعاني بسبب سرعة التطوير. عندما تموت React مرة أخرى ، سيكون عليك إعادة كتابة الحلول المرتبطة بإطارها ونظامها البيئي. ولكن مع Chita Architecture ، يمكنك نسيان ربط الإطار.

ما هي ميزة Redux؟


من أجل فهم الفرق ، دعنا نرى كيف يتصرف Redux عندما ينمو التطبيق.
إعادة

كما ترى من الرسم البياني ، مع نمو تطبيقات Redux عموديًا ، يزداد المتجر وعدد المخفضات أيضًا ويتحول إلى عنق الزجاجة. ويبدأ مقدار النفقات العامة لإعادة بناء المتجر وإيجاد المخفض المناسب في تجاوز الحمولة.

يمكنك التحقق من نسبة النفقات العامة إلى الحمولة على تطبيق متوسط ​​الحجم باستخدام اختبار بسيط.
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

استغرق إعادة إنشاء المتجر في 100 عقار 8 مرات أكثر من المنطق نفسه. مع 1000 عنصر ، هذا بالفعل أكثر 50 مرة. بالإضافة إلى ذلك ، يمكن أن ينتج عن إجراء مستخدم واحد سلسلة كاملة من الإجراءات ، والتي يصعب التقاط نداءها وتصحيحها. يمكنك أن تجادل بالتأكيد أن 0.04 مللي ثانية لإعادة إنشاء المتجر صغير جدًا ولن يتباطأ. ولكن 0.04 مللي ثانية على معالج Core i7 ولعمل واحد. بالنظر إلى ضعف المعالجات المحمولة وحقيقة أن إجراء مستخدم واحد يمكن أن يولد عشرات الإجراءات ، كل هذا يؤدي إلى حقيقة أن الحسابات لا تتناسب مع 16 مللي ثانية ويتم إنشاء شعور بأن التطبيق يتباطأ.

دعونا نقارن كيف ينمو تطبيق Clean Architecture:
العمارة البحتة

كما يمكن رؤيته بسبب فصل المنطق والمسؤوليات بين الطبقات ، فإن التطبيق يتطور أفقياً. إذا احتاج أي مكون إلى معالجة البيانات ، فسوف يتحول إلى الخدمة المقابلة دون لمس الخدمات غير المتعلقة بمهمته. بعد استلام البيانات ، سيتم إعادة رسم مكون واحد فقط. سلسلة معالجة البيانات قصيرة جدًا وواضحة ، ولحد أقصى 4 قفزات رمز ، يمكنك العثور على المنطق الضروري وتصحيحه. بالإضافة إلى ذلك ، إذا رسمنا قياسًا بجدار من الطوب ، فيمكننا إزالة أي لبنة من هذا الجدار واستبداله بجدار آخر دون التأثير على استقرار هذا الجدار.

المكافأة 1: وظائف محسنة لمكونات الإطار


المكافأة هي القدرة على استكمال أو تغيير سلوك مكونات مكتبة العرض التقديمي. على سبيل المثال ، لا يؤدي رد الفعل في حالة حدوث خطأ في العرض إلى عرض التطبيق بالكامل ، إذا تم إجراء مراجعة بسيطة:
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}`
            );
        }
    }

}

الآن لن يقوم التفاعل برسم المكون الذي حدث الخطأ فيه فقط. بنفس الطريقة البسيطة ، يمكنك إضافة المراقبة والتحليلات وغيرها من nishtyaki.

المكافأة 2: التحقق من صحة البيانات


بالإضافة إلى وصف البيانات ونقل البيانات بين الطبقات ، تتمتع النماذج بهامش كبير من الفرص. على سبيل المثال ، إذا قمت بتوصيل مكتبة مدقق الفصل ، فبمجرد تعليق الديكور ، يمكنك التحقق من صحة البيانات في هذه النماذج ، بما في ذلك مع القليل من التحسين ، يمكنك التحقق من صحة نماذج الويب.

المكافأة 3: إنشاء كيانات


بالإضافة إلى ذلك ، إذا كنت بحاجة إلى العمل مع قاعدة بيانات محلية ، فيمكنك توصيل مكتبة typeorm وستتحول نماذجك إلى كيانات سيتم من خلالها إنشاء قاعدة البيانات وتشغيلها.

شكرا للانتباه


إذا أعجبك المقال أو النهج ، مثل ولا تخشى التجربة. إذا كنت من أتباع Redux ولا تحب المعارضة ، فيرجى توضيح في التعليقات كيفية قياس البيانات واختبارها والتحقق منها في طلبك.

All Articles