Organisasi kode dalam layanan microser dan pendekatan saya menggunakan arsitektur heksagonal dan DDD


Halo, Habr! Di Monolith, semua kode harus dalam gaya yang sama, dan dalam berbagai layanan mikro Anda dapat menggunakan berbagai pendekatan, bahasa pemrograman, dan kerangka kerja. Untuk layanan microser sederhana dengan 1 hingga 2 pengontrol dan 1 hingga 10 aksi, tidak ada gunanya memblokir lapisan abstraksi. Untuk layanan Microsoft yang rumit dengan kondisi yang berbeda dan logika transisi di antara keduanya, sebaliknya, lebih baik tidak malas pada awalnya. Saya ingin berbicara tentang pengalaman saya dengan mengatur kode dan menggunakan pendekatan DDD, Ports, dan Adapters untuk kedua kasus. Ada ringkasan singkat artikel: Juni - menulis kode di controller. Tengah - menulis banyak abstraksi. Senior - tahu kapan harus menulis kode di controller, dan kapan abstraksi diperlukan.Di sini Anda perlu segera mengakhiri I - Ports di C # adalah antarmuka atau abstrak, dan Adaptor adalah implementasi konkret (kelas, struct). Untuk referensi, saya sarankan membaca terjemahan DDD ini, Hexagonal, Bawang, Bersih, CQRS ... bagaimana saya menggabungkan semuanya dan artikel kesalahpahaman Arsitektur Bersih ini . Di sini, perlu diingat bahwa pendekatan ini digambarkan untuk monolit yang besar dan kompleks. Saya ingin berbicara tentang variasi pendekatan ini dalam layanan microser 80 persen di antaranya sederhana dan 20 kompleksitas sedang atau tinggi.

Terminologi


1) Adaptor Utama


Memperkenalkan antarmuka yang ramah pengguna untuk sistem panggilan eksternal . Panggil dan gunakan SecondaryApdapters and Logic. Mereka memberi tahu aplikasi untuk melakukan sesuatu. Titik masuk ke kode Anda. Perwakilan khas: ItemsService, CreateItemCommandHandler. Gunakan Logic dan SecondaryAdapters untuk pekerjaan mereka.

2) Adaptor Sekunder


Menyediakan antarmuka ke sistem eksternal yang disebut nyaman untuk digunakan oleh sistem kami. Dia menerima perintah dari aplikasi. Perwakilan khas: ItemsRepository, EmailSender, Logger, DistributedCache. Mereka menggunakan pustaka dan kerangka kerja seperti EntityFramework untuk pekerjaan mereka.

3) Logika


3.1) Entitas


Gabungkan data dan logika. Objek OOP yang khas. Barang, Uang, Pengguna. Ada juga ValueObjects dan Acara. Hanya objek lain dari lapisan Entitas yang digunakan. Secara teori, lapisan ini adalah inti dari aplikasi yang tidak bergantung pada siapa pun. Semua tergantung padanya.

3.2) Layanan Domain


Komputasi telanjang. Mereka diperlukan untuk logika yang tidak dapat dilampirkan ke satu entitas tertentu. Gunakan Entitas atau Layanan Domain lainnya. Jangan gunakan PrimaryAdapters (mereka selalu menggunakan semuanya sendiri) dan SecondaryAdapters. Biasanya ini semua jenis AmountCalculator, Validator dan banyak lagi.

Alur panggilan kode harus selalu dalam satu arah, PrimaryAdapters -> Logic and SecondaryAdapters.

Domain


Ini adalah area subjek di mana sistem sedang dikembangkan. Misalnya, untuk Domain Online Store, ini adalah E-Commerce.

Konteks terikat


BoundedContext digunakan untuk membagi sistem menjadi bagian-bagian yang terisolasi dengan tanggung jawab tertentu. Salah satu cara untuk sukses dalam desain sistem adalah menemukan dan menyorot semua BoundedContexts di dalamnya. Dalam arsitektur microservice, 1 microservice = 1 BoundedContext. Misalnya: Toko online mungkin memiliki Keranjang Belanja BoundedContext dan BoundedContext bekerja dengan pesanan, BoundedContext bekerja dengan file. Dan satu BoundedContext besar dapat dipecah menjadi bagian-bagian kecil di dalamnya. Misalnya, untuk Konteks Terbatas dari keranjang barang, Anda dapat membuat divisi ke dalam konteks menambahkan barang ke keranjang dan konteks mengeluarkan barang dari keranjang. Dalam monolit 1, BoundedContext besar adalah satu Modul. Di sini perlu untuk membuat pernyataan bahwa semua Entitas hidup dalam konteks tertentu yaituItem dari konteks keranjang barang dan Item dari konteks showcase barang, ini dapat menjadi kelas yang berbeda dengan bidang dan metode yang berbeda. Dalam sebuah monolit, mereka hanya memetakan satu sama lain dan menggunakan beberapa ItemDto yang dilewati EF untuk bekerja dengan database dan yang memiliki bidang yang sama untuk semua, yaitu, jika Item dari konteks (modul) Keranjang memiliki properti Property1, dan Item dari konteks etalase toko barang memiliki properti Property2 maka ItemDto akan memiliki Property1 dan Property2. Dalam setiap konteks akan ada repositori sendiri yang akan mengeluarkan Item, yang sudah menjadi karakteristik dari konteks ini, dari database. Dalam layanan microser, ini mudah. Masing-masing memiliki database sendiri dan esensinya sendiri. Setiap layanan dunia memiliki kelasnya sendiri. Secara sederhanaDalam sebuah monolit, mereka hanya memetakan satu sama lain dan menggunakan beberapa ItemDto yang dilewati EF untuk bekerja dengan database dan yang memiliki bidang yang sama untuk semua, yaitu, jika Item dari konteks (modul) Keranjang memiliki properti Property1, dan Item dari konteks etalase toko barang memiliki properti Property2 maka ItemDto akan memiliki Property1 dan Property2. Dalam setiap konteks akan ada repositori sendiri yang akan menarik dari database Item yang sudah menjadi karakteristik dari konteks ini. Dalam layanan microser, ini mudah. Masing-masing memiliki database sendiri dan esensinya sendiri. Setiap layanan dunia memiliki kelasnya sendiri. Secara sederhanaDalam sebuah monolit, mereka hanya memetakan satu sama lain dan menggunakan beberapa ItemDto yang dilewati EF untuk bekerja dengan database dan yang memiliki bidang yang sama untuk semua, yaitu, jika Item dari konteks (modul) Keranjang memiliki properti Property1, dan Item dari konteks etalase toko barang memiliki properti Property2 maka ItemDto akan memiliki Property1 dan Property2. Dalam setiap konteks akan ada repositori sendiri yang akan mengeluarkan Item, yang sudah menjadi karakteristik dari konteks ini, dari database. Dalam layanan microser, ini mudah. Masing-masing memiliki database sendiri dan esensinya sendiri. Setiap layanan dunia memiliki kelasnya sendiri. Secara sederhanaDalam setiap konteks akan ada repositori sendiri yang akan mengeluarkan Item, yang sudah menjadi karakteristik dari konteks ini, dari database. Dalam layanan microser, ini mudah. Masing-masing memiliki database sendiri dan esensinya sendiri. Setiap layanan dunia memiliki kelasnya sendiri. Secara sederhanaDalam setiap konteks akan ada repositori sendiri yang akan menarik dari database Item yang sudah menjadi karakteristik dari konteks ini. Dalam layanan microser, ini mudah. Masing-masing memiliki database sendiri dan esensinya sendiri. Setiap layanan dunia memiliki kelasnya sendiri. Secara sederhanaingat bahwa Item dan ItemsRepository dari BoundedContext yang berbeda dapat berupa objek yang berbeda dengan metode dan properti yang berbeda. Seringkali, BoundedContext dalam layanan microser dilanggar oleh pengenalan beberapa perpustakaan umum. Hasilnya adalah kelas yang memiliki beberapa lusin metode atau properti dan masing-masing layanan microser menggunakan 1 - 2 hanya yang dibutuhkan, jadi Anda harus berhati-hati dengan pustaka bersama dalam layanan microser.

Ketergantungan antar lapisan




Opsi untuk layanan microser sederhana yang undang-undang Pareto 80 persen


Buang layer PrimaryAdatapters dan SecondaryAdapters. Sisakan hanya lapisan Logika. Lebih tepatnya, dalam aplikasi ASP.NET yang biasa kita gunakan - PimaryAdater adalah Controller, dan SecondaryAdapter adalah DbContext dari EF. Jika Controller tiba-tiba menjadi terlalu besar, maka kita potong-potong menggunakan parsial. Ini jauh lebih baik daripada membiakkan abstraksi yang tidak perlu dan menghabiskan waktu untuk itu. Sebagai contoh, Microsoft melakukan ini untuk layanan BasketMicrotservice dalam contoh aplikasi EShop pada wadah buruh pelabuhan. Ya, cukup tulis kode ke controller. Hanya saja, jangan menulis kode spageti. Pentingingat bahwa lapisan Logik tetap bersama kami dan kami masih menggunakan Entitas dengan logika mereka dan DomainServices dengan perhitungan mereka dan tidak hanya menulis dinding kode spaghetti di controller. Kami hanya membuang ItemService dan ItemRepository yang khas. ItemValidator tua yang baik, ItemMapper, AmountCalculator, dll. Dalam semangat ini masih tetap bersama kami. Karena kita masih memiliki lapisan Logika yang terbentuk, kita dapat beralih ke opsi yang lebih rumit kapan saja dengan membungkus abstraksi tambahan dengan ItemsService, ItemsRepository.

Contoh


Layanan yang menghitung harga akhir suatu produk dengan diskon. Secara teori, ini adalah bagian dari Domain Online Store dan mewakili katalog produk BoundedContext. Untuk kesederhanaan, saya melewatkan semua logika validasi. Itu dapat dijelaskan dalam semacam ItemValidator dan DiscountValidator.

1) Folder logika:
1.1) Folder entitas:

Diskon:

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

        public decimal Value { get; set; }
    }

Uang:

    [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
            };
        }
    }

Produk:

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

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

1.2) Folder DomainServices

Kalkulator harga produk , termasuk diskon:

    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) Pengendali

    [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;
        }
    }


Opsi untuk layanan microser kompleks yang 20 persen dan untuk sebagian besar monolit


Di sini kami menggunakan pendekatan standar dengan isolasi maksimum lapisan dari satu sama lain. Tambahkan kelas untuk PrimaryAdapter (ItemService) dan untuk SecondaryAdapter (ItemRepository). Dalam folder Logika, semuanya tetap seperti sebelumnya.

Contoh


1) Folder 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) folder 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;
        }
    }

Pengontrol kami sekarang menggunakan IItemService kami

    [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;
        }
    }

Menambahkan banyak kode tambahan. Sebaliknya, peningkatan fleksibilitas sistem. Sekarang lebih mudah untuk mengubah Contoller dan umumnya ASP.NET Core ke sesuatu yang lain atau mengubah DbContext dan EntityFramework Core ke sesuatu yang lain atau menambahkan caching ke Redis. Pendekatan pertama menang dalam layanan microser sederhana dan monolit yang sangat sederhana dan kecil. Yang kedua dalam semua kasus lain, ketika Anda perlu menambahkan dan menyelesaikan kode ini selama lebih dari satu tahun. Nah, dalam pendekatan kedua, selain Item Entitas dan Diskon, akan berguna untuk membuat DTO terpisah yang akan menggunakan DbContex EF dan memisahkan DTO (Model) yang akan digunakan oleh Pengontrol ASP.NET dengan menggunakan I.e. ItemDto dan ItemModel. Ini akan membuat model domain lebih mandiri. Dan akhirnya, contoh aplikasi sederhana yang saya tulis menggunakan pendekatan 2nd TestRuvds. Faktanya, dia berlebihan di sini, dan semua abstraksi ini ada di sini sebagai contoh.

Contoh implementasi


Modul VirtualServers

Ucapan Terima Kasih


terima kasih canxes dan AndrewDemb untuk kesalahan tata bahasa ditemukan.

All Articles