تنظيم الكود في الخدمات المصغرة ونهجي في استخدام العمارة السداسية و DDD


مرحبا يا هابر! في Monolith ، يجب أن تكون جميع التعليمات البرمجية بنفس النمط ، وفي الخدمات الصغيرة المختلفة ، يمكنك استخدام أساليب مختلفة ولغات برمجة وأطر عمل مختلفة. بالنسبة للخدمات الصغيرة البسيطة التي تحتوي على 1 إلى 2 وحدة تحكم و 1 إلى 10 إجراءات ، لا يوجد أي معنى خاص في حظر طبقات التجريد. بالنسبة إلى الخدمات الدقيقة المعقدة ذات الحالات المختلفة ومنطق الانتقال بينها ، على العكس ، من الأفضل ألا تكون كسولًا في البداية. أريد أن أتحدث عن تجربتي في تنظيم التعليمات البرمجية واستخدام نهج DDD ، والمنافذ ، والمحولات في كلتا الحالتين. هناك ملخص موجز للمقال: يونيو - يكتب التعليمات البرمجية في وحدة التحكم. الأوسط - يكتب مجموعة من التجريد. Senior - يعرف متى يكتب الرمز في وحدة التحكم ، ومتى تكون هناك حاجة إلى التجريد.هنا تحتاج إلى وضع حد فوري لـ I - الموانئ في C # هي واجهة أو مجردة ، والمحولات هي تطبيقات ملموسة (فئة ، هيكل). كمرجع ، أوصي بقراءة هذه الترجمة من DDD ، Hexagonal ، Onion ، Clean ، CQRS ... كيف أقوم بتجميع كل شيء وهذه المقالة من سوء فهم الهندسة المعمارية . هنا ، فقط ضع في اعتبارك أن النهج موصوف من أجل كتلة كبيرة ومعقدة. أريد أن أتحدث عن اختلافات هذا النهج في الخدمات الدقيقة 80 في المئة منها بسيطة و 20 متوسطة أو عالية التعقيد.

المصطلح


1) الابتدائية


قدم واجهة سهلة الاستخدام لنظام الاتصال الخارجي . استدعاء واستخدام SecondaryApdapters والمنطق. يقولون التطبيق للقيام بشيء ما. نقاط الدخول إلى التعليمات البرمجية الخاصة بك. الممثلين النموذجيين: ItemsService و CreateItemCommandHandler. استخدم المنطق والثانوي للمبتدئين لعملهم.

2) المعالجات الثانوية


يوفر واجهة لنظام خارجي يسمى مناسب للاستخدام من قبل نظامنا. يتلقى أوامر من التطبيق. الممثلين النموذجيين: ItemsRepository ، EmailSender ، Logger ، DistributedCache. يستخدمون المكتبات والأطر مثل EntityFramework لعملهم.

3) المنطق


3.1) الكيانات


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

3.2) خدمات المجال


الحوسبة العارية. إنها ضرورية للمنطق الذي لا يمكن إرفاقه بكيان واحد محدد. استخدام الكيانات أو خدمات المجال الأخرى. لا تستخدم PrimaryAdapters (فهم دائمًا يستخدمون أنفسهم) و SecondaryAdapters. عادة هذا هو كل أنواع AmountCalculator ، Validator والمزيد.

يجب أن يكون تدفق استدعاء الرمز دائمًا في اتجاه واحد PrimaryAdapters -> Logic و SecondaryAdapters.

نطاق


هذا هو المجال الموضوعي الذي يتم تطوير النظام من أجله. على سبيل المثال ، بالنسبة لـ Domain Online Store ، هذه هي التجارة الإلكترونية.

نص مقيد


يستخدم BoundedContext لتقسيم النظام إلى أجزاء معزولة بمسؤولية معينة. تتمثل إحدى طرق النجاح في تصميم النظام في إيجاد وإبراز جميع سياقاتها المقيدة فيه. في بنية الخدمات الصغيرة ، 1 خدمة صغيرة = 1 BoundContext. على سبيل المثال: قد يحتوي متجر على الإنترنت على عربة تسوق BoundedContext و BoundedContext التي تعمل مع الطلبات ، وتعمل BoundedContext مع الملفات. ويمكن تقسيم BoundContext الكبير إلى قطع صغيرة في الداخل. على سبيل المثال ، بالنسبة إلى BoundContext لسلة البضائع ، يمكنك إجراء تقسيم في سياق إضافة السلع إلى السلة وسياق إزالة البضائع من السلة. في monolith 1 ، يكون BoundContext الكبير عبارة عن وحدة واحدة. هنا من الضروري إبداء ملاحظة بأن جميع الكيانات تعيش في سياق معين ، أيعنصر من سياق سلة السلع والبند من سياق عرض البضائع ، يمكن أن تكون هذه فئات مختلفة مع مجالات وأساليب مختلفة. في وحدة متراصة ، يقومون ببساطة بتعيين بعضها البعض واستخدام بعض ItemDto التي تمررها EF للعمل مع قاعدة البيانات والتي تحتوي على حقول مشتركة بين الجميع ، أي إذا كان العنصر من سياق (الوحدة النمطية) للسلة لديه خاصية Property1 ، وعنصر من سياق واجهة المتجر من السلع لديها خاصية Property2 ثم سيكون ItemDto لكل من Property1 و Property2. في كل سياق ، سيكون هناك مستودع خاص به والذي سيسحب العنصر ، الذي هو بالفعل سمة مميزة لهذا السياق ، من قاعدة البيانات. هذا سهل في الخدمات الصغيرة. لكل منها قاعدة بيانات خاصة بها وجوهرها الخاص. كل خدمة عالمية لها فصولها الخاصة. ببساطةفي وحدة متراصة ، يقومون ببساطة بتعيين بعضها البعض واستخدام بعض ItemDto التي تمررها EF للعمل مع قاعدة البيانات والتي تحتوي على حقول مشتركة بين الجميع ، أي إذا كان العنصر من سياق (الوحدة النمطية) للسلة لديه خاصية Property1 ، وعنصر من سياق واجهة المتجر من السلع لديها خاصية Property2 ثم سيكون ItemDto لكل من Property1 و Property2. في كل سياق ، سيكون هناك مستودع خاص به والذي سيسحب العنصر ، الذي هو بالفعل سمة مميزة لهذا السياق ، من قاعدة البيانات. هذا سهل في الخدمات الصغيرة. لكل منها قاعدة بيانات خاصة بها وجوهرها الخاص. كل خدمة عالمية لها فصولها الخاصة. ببساطةفي وحدة متراصة ، يقومون ببساطة بتعيين بعضهم البعض واستخدام بعض ItemDto التي تمررها EF للعمل مع قاعدة البيانات والتي تحتوي على حقول مشتركة بين الجميع ، أي إذا كان العنصر من سياق (الوحدة النمطية) للسلة لديه خاصية Property1 ، وعنصر من سياق واجهة المتجر من السلع لديها خاصية Property2 ثم سيكون ItemDto لكل من Property1 و Property2. في كل سياق ، سيكون هناك مستودع خاص به والذي سيسحب العنصر ، الذي هو بالفعل سمة مميزة لهذا السياق ، من قاعدة البيانات. هذا سهل في الخدمات الصغيرة. لكل منها قاعدة بيانات خاصة بها وجوهرها الخاص. كل خدمة عالمية لها فصولها الخاصة. ببساطةفي كل سياق ، سيكون هناك مستودع خاص به والذي سيسحب العنصر ، الذي هو بالفعل سمة مميزة لهذا السياق ، من قاعدة البيانات. هذا سهل في الخدمات الصغيرة. لكل منها قاعدة بيانات خاصة بها وجوهرها الخاص. كل خدمة عالمية لها فصولها الخاصة. ببساطةفي كل سياق ، سيكون هناك مستودع خاص به والذي سيسحب العنصر ، الذي هو بالفعل سمة مميزة لهذا السياق ، من قاعدة البيانات. هذا سهل في الخدمات الصغيرة. لكل منها قاعدة بيانات خاصة بها وجوهرها الخاص. كل خدمة عالمية لها فصولها الخاصة. ببساطةتذكر أن Item and ItemsRepository من BoundedContext مختلفة يمكن أن تكون كائنات مختلفة بطرق وخصائص مختلفة. في كثير من الأحيان ، يتم انتهاك BoundedContext في الخدمات الدقيقة من خلال إدخال بعض المكتبات الشائعة. والنتيجة هي فئة تحتوي على العديد من الطرق أو الخصائص وتستخدم كل خدمة صغيرة من 1 إلى 2 فقط ما تحتاجه ، لذا يجب أن تكون حذرًا مع المكتبات المشتركة في الخدمات الدقيقة.

التبعيات بين الطبقات




خيار للخدمات الصغيرة البسيطة التي قانون باريتو 80 في المئة


تخلص من الطبقة PrimaryAdatapters و SecondaryAdapters. اترك طبقة المنطق فقط. بتعبير أدق ، في تطبيق ASP.NET النموذجي الذي نستخدمه - PimaryAdater هو جهاز تحكم ، و SecondaryAdapter هو DbContext من EF. إذا أصبحت وحدة التحكم كبيرة فجأة ، فقمنا بتقطيعها إلى قطع باستخدام جزئي. هذا أفضل بكثير من تربية التجريد غير الضروري وقضاء الوقت عليها. على سبيل المثال ، قامت Microsoft بذلك لـ BasketMicrotservice في مثال تطبيق EShop على حاويات عامل الميناء. نعم ، فقط اكتب الرمز إلى وحدة التحكم. فقط لا تكتب رمز السباغيتي. مهمتذكر أن طبقة المنطق تبقى معنا ولا نزال نستخدم الكيانات مع منطقها وخدمات المجال مع حساباتها وليس فقط كتابة جدار من رمز السباغيتي في وحدة التحكم. نحن فقط نتخلص من ItemService النموذجي و ItemRepository. إن ItemValidator القديم الجيد ، و ItemMapper ، و AmountCalculator ، وما إلى ذلك بهذه الروح لا يزالون معنا. نظرًا لأنه لا يزال لدينا طبقة المنطق ، يمكننا التبديل إلى خيار أكثر تعقيدًا في أي وقت من خلال التفاف تجريدات إضافية باستخدام ItemsService و ItemsRepository.

مثال


خدمة تقوم بحساب السعر النهائي للمنتج بخصم. من الناحية النظرية ، يعد جزءًا من متجر المجال عبر الإنترنت ويمثل كتالوج منتجات BoundedContext. من أجل البساطة ، تخطيت كل منطق التحقق. يمكن وصفه في نوع من ItemValidator و DiscountValidator.

1) مجلد المنطق:
1.1) مجلد الكيانات:

الخصم:

    public class Discount
    {
        [Key]
        public Guid Id { get; set; }

        public decimal Value { get; set; }
    }

مال:

    [Owned]
    public class Money
    {
        public decimal Amount { get; set; }

        public Money Apply(Discount discount)
        {
            var amount = Amount * (1 - discount.Value);
            return new Money()
            {
                Amount = amount
            };
        }
    }

المنتج:

    public class Item
    {
        [Key]
        public Guid Id { get; set; }

        public Money Price { get; set; } = new Money();
    }

1.2) حاسبة DomainServices

سعر المنتج ، بما في ذلك الخصومات:

    public interface IPriceCalculator
    {
        Money WithDiscounts(Item item, IEnumerable<Discount> discounts);
    }

     public sealed class PriceCalculator:IPriceCalculator
    {
        public Money WithDiscounts(Item item, IEnumerable<Discount> discounts)
        {
            return discounts.Aggregate(item.Price, (m, d) => m.Apply(d));
        }
    }

2) DbContext

    public class ItemsDbContext : DbContext
    {
        public ItemsDbContext(DbContextOptions<ItemsDbContext> options) : base(options)
        {

        }

        public DbSet<Item> Items { get; set; }

        public DbSet<Discount> Discounts { get; set; }
    }

3) تحكم

    [ApiController]
    [Route("api/v1/items")]
    public class ItemsController : ControllerBase
    {
        private readonly ItemsDbContext _context;
        private readonly IPriceCalculator _priceCalculator;

        public ItemsController(ItemsDbContext context, IPriceCalculator priceCalculator)
        {
            _context = context;
            _priceCalculator = priceCalculator;
        }

        [HttpGet("{id}/price-with-discounts")]
        public async Task<decimal> GetPriceWithDiscount(Guid id)
        {
            var item = await _context.Items.FindAsync(id);
            var discounts = await _context.Discounts.ToListAsync();
            return _priceCalculator.WithDiscounts(item, discounts).Amount;
        }
    }


خيار الخدمات الصغيرة المعقدة التي 20 في المائة ومعظم الأحاديات


هنا نستخدم نهجًا قياسيًا مع أقصى عزل للطبقات عن بعضها البعض. قم بإضافة فئة لـ PrimaryAdapter (ItemService) و SecondaryAdapter (ItemRepository). في مجلد المنطق ، يبقى كل شيء كما كان من قبل.

مثال


1) مجلد SecondaryAdapters

    public interface IItemsRepository
    {
        Task<Item> Get(Guid id);
    }

    public sealed class ItemsRepository : IItemsRepository
    {
        private readonly ItemsDbContext _context;

        public ItemsRepository(ItemsDbContext context)
        {
            _context = context;
        }

        public async Task<Item> Get(Guid id)
        {
            var item = await _context.Items.FindAsync(id);
            return item;
        }
    }

    public interface IDiscountsRepository
    {
        Task<List<Discount>> Get();
    }

    public sealed class DiscountsRepository : IDiscountsRepository
    {
        private readonly ItemsDbContext _context;

        public DiscountsRepository(ItemsDbContext context)
        {
            _context = context;
        }

        public Task<List<Discount>> Get()
        {
            return _context.Discounts.ToListAsync();
        }
    }


2) مجلد PrimaryAdapters

    public interface IItemService
    {
        Task<decimal> GetPriceWithDiscount(Guid id);
    }

    public class ItemService : IItemService
    {
        private readonly IItemsRepository _items;
        private readonly IDiscountsRepository _discounts;
        private readonly IPriceCalculator _priceCalculator;

        public ItemService(IItemsRepository items, IDiscountsRepository discounts, IPriceCalculator priceCalculator)
        {
            _items = items;
            _discounts = discounts;
            _priceCalculator = priceCalculator;
        }

        public async Task<decimal> GetPriceWithDiscount(Guid id)
        {
            var item = await _items.Get(id);
            var discounts = await _discounts.Get();
            return _priceCalculator.WithDiscounts(item, discounts).Amount;
        }
    }

تستخدم وحدة التحكم لدينا الآن خدمة IItemService الخاصة بنا

    [ApiController]
    [Route("api/v1/items")]
    public class ItemsController : ControllerBase
    {
        private readonly IItemService _service;

        public ItemsController(IItemService service)
        {
            _service = service;
        }

        [HttpGet("{id}/price-with-discounts")]
        public async Task<decimal> GetPriceWithDiscount(Guid id)
        {
            var result = await _service.GetPriceWithDiscount(id);
            return result;
        }
    }

تمت إضافة الكثير من التعليمات البرمجية الإضافية. بدلاً من ذلك ، زيادة مرونة النظام. الآن أصبح من السهل تغيير Contoller و ASP.NET Core بشكل عام إلى شيء آخر أو تغيير DbContext و EntityFramework Core إلى شيء آخر أو إضافة التخزين المؤقت إلى Redis. النهج الأول يفوز في الخدمات الصغيرة البسيطة والمتجانسة البسيطة والبسيطة. الثاني في جميع الحالات الأخرى ، عندما تحتاج إلى إضافة وإنهاء هذا الرمز لأكثر من عام. حسنًا ، في النهج الثاني ، بالإضافة إلى عنصر الكيان والخصم ، من المفيد إنشاء DTOs منفصلة تستخدم DbContex EF و DTOs منفصلة (الطرازات) التي سيستخدمها متحكم ASP.NET ، أي ItemDto و ItemModel. هذا سيجعل نماذج المجال أكثر استقلالية. وأخيرًا ، مثال على تطبيق بسيط كتبته باستخدام نهج TestRuvds الثاني. في الواقع ، هو فائض هنا ، وكل هذه التجريدات هنا كمثال.

مثال على التنفيذ


VirtualServersModule

شكر وتقدير


شكرا كانكس و AndrewDemb عن الأخطاء النحوية الموجودة.

All Articles