Bagaimana saya mempersiapkan wawancara # 2

Di bagian kedua, saya ingin memperbarui pemahaman saya tentang arsitektur Onion dan n-Tier, serta DI-frameworks (Autofac dan inti bersih bawaan). Tetapi melihat volume teks, saya menyadari bahwa n-Tier akan dijelaskan dengan sangat singkat, dan saya segera minta maaf.

Juga, saya akan mencoba mempertimbangkan komentar dari bagian pertama,







Arsitektur bawang



Misalkan kita sedang merancang aplikasi untuk mencatat buku mana yang telah kita baca, tetapi untuk akurasinya, kita ingin mencatat bahkan berapa halaman yang telah dibaca. Kami tahu bahwa ini adalah program pribadi yang kami perlukan di ponsel cerdas kami, seperti bot untuk telegram dan, mungkin, untuk desktop, jadi silakan pilih opsi arsitektur ini:



(Tg Bot, Aplikasi Telepon, Desktop) => Asp.net Web Api => Database



Buat proyek di Visual studio dari jenis Asp.net Core, selanjutnya kita memilih jenis proyek Web Api.

Apa bedanya dengan yang biasa?

Pertama, kelas pengontrol mewarisi dari kelas ControllerBase, yang dirancang untuk menjadi basis untuk MVC tanpa dukungan untuk mengembalikan tampilan (kode html).

Kedua, ia dirancang untuk mengimplementasikan layanan REST yang mencakup semua jenis permintaan HTTP, dan sebagai tanggapan atas permintaan Anda menerima json dengan indikasi eksplisit dari status respons. Selain itu, Anda akan melihat bahwa pengontrol yang dibuat secara default akan ditandai dengan atribut [ApiController], yang memiliki opsi berguna khusus untuk API.



Sekarang Anda perlu memutuskan bagaimana menyimpan data. Karena saya tahu bahwa saya membaca tidak lebih dari 12 buku setahun, file csv sudah cukup untuk saya, yang akan mewakili database.



Jadi saya membuat kelas yang mendeskripsikan buku:

Book.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApiTest
{
    public class Book
    {
        public int id { get; set; }
        public string name { get; set; }
        public string author { get; set; }
        public int pages { get; set; }
        public int readedPages { get; set; }
    }
}

      
      









Dan kemudian saya menjelaskan kelas untuk bekerja dengan database:

CsvDB.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WebApiTest
{
    public class CsvDB
    {
        const string dbPath = @"C:\\csv\books.csv";

        private List<Book> books;

        private void Init()
        {
            if (books != null)
                return;
            string[] lines = File.ReadAllLines(dbPath);
            books = new List<Book>();
            foreach(var line in lines)
            {
                string[] cells = line.Split(';');
                Book newBook = new Book()
                {
                    id = int.Parse(cells[0]),
                    name = cells[1],
                    author = cells[2],
                    pages = int.Parse(cells[3]),
                    readedPages = int.Parse(cells[4])
                };
                books.Add(newBook);
            }
        }

        public int Add(Book item)
        {
            Init();
            int nextId = books.Max(x => x.id) + 1;
            item.id = nextId;
            books.Add(item);
            return nextId;
        }

        public void Delete(int id)
        {
            Init();
            Book selectedToDelete = books.Where(x => x.id == id).FirstOrDefault();
            if(selectedToDelete != null)
            {
                books.Remove(selectedToDelete);
            }
        }

        public Book Get(int id)
        {
            Init();
            Book book = books.Where(x => x.id == id).FirstOrDefault();
            return book;
        }

        public IEnumerable<Book> GetList()
        {
            Init();
            return books;
        }

        public void Save()
        {
            StringBuilder sb = new StringBuilder();
            foreach(var book in books)
                sb.Append($"{book.id};{book.name};{book.author};{book.pages};{book.readedPages}");
            File.WriteAllText(dbPath, sb.ToString());
        }

        public bool Update(Book item)
        {
            var selectedBook = books.Where(x => x.id == item.id).FirstOrDefault();
            if(selectedBook != null)
            {
                selectedBook.name = item.name;
                selectedBook.author = item.author;
                selectedBook.pages = item.pages;
                selectedBook.readedPages = item.readedPages;
                return true;
            }
            return false;
        }
    }
}

      
      







Maka masalahnya kecil, untuk menambahkan API agar dapat berinteraksi dengannya:

BookController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WebApiTest.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class BookController : ControllerBase
    {

        private CsvDB db;

        public BookController()
        {
            db = new CsvDB();
        }

        [HttpGet]
        public IEnumerable<Book> GetList() => db.GetList();

        [HttpGet("{id}")]
        public Book Get(int id) => db.Get(id);

        [HttpDelete("{id}")]
        public void Delete(int id) => db.Delete(id);

        [HttpPut]
        public bool Put(Book book) => db.Update(book);
    }
}

      
      







Dan yang tersisa hanyalah menambahkan UI, yang akan memudahkan. Dan semuanya bekerja!

Keren! Tetapi tidak, sang istri meminta agar dia juga memiliki akses ke hal yang nyaman seperti itu.

Kesulitan apa yang menanti kita? Pertama, sekarang Anda perlu menambahkan kolom untuk semua buku yang akan menunjukkan ID pengguna. Percayalah, ini tidak akan nyaman dengan file csv. Selain itu, sekarang Anda perlu menambahkan pengguna itu sendiri! Dan bahkan sekarang semacam logika diperlukan agar istri saya tidak melihat bahwa saya menyelesaikan membaca koleksi ketiga Dontsova alih-alih Tolstoy yang dijanjikan.



Mari kita coba memperluas proyek ini ke persyaratan yang diperlukan:

Kemampuan untuk membuat akun pengguna, yang akan dapat menyimpan daftar bukunya dan menambahkan berapa banyak yang dia baca.

Sejujurnya, saya ingin menulis sebuah contoh, tetapi sejumlah hal yang tidak ingin saya lakukan secara tajam membunuh keinginan:

Penciptaan pengontrol yang akan bertanggung jawab untuk mengotorisasi dan mengirim data ke pengguna;

Pembuatan Pengguna entitas baru, serta penangannya;

Mendorong logika ke dalam controller itu sendiri, yang akan membuatnya membengkak, atau menjadi class yang terpisah;

Menulis ulang logika bekerja dengan "database", karena sekarang atau dua file csv, atau pergi ke database ...



Hasilnya, kami mendapat monolit besar, yang sangat "menyakitkan" untuk berkembang. Ini memiliki sekumpulan besar tautan ketat dalam aplikasi. Objek yang terikat erat bergantung pada objek lain; ini berarti bahwa mengubah satu objek dalam aplikasi yang berpasangan erat sering kali memerlukan perubahan sejumlah objek lainnya. Ini tidak sulit jika aplikasinya kecil, tetapi terlalu sulit untuk membuat perubahan dalam aplikasi tingkat perusahaan.



Ikatan yang lemah berarti bahwa dua objek tidak bergantung, dan satu objek dapat menggunakan yang lain tanpa bergantung padanya. Jenis hubungan ini bertujuan untuk mengurangi interdependensi antar komponen sistem guna mengurangi risiko bahwa perubahan pada satu komponen akan memerlukan perubahan pada komponen lainnya.



Referensi sejarah
ยซยป .

ยซ ยป 2008 . , , , . โ€” , , , .







Oleh karena itu, kami akan mencoba mengimplementasikan aplikasi kami dalam gaya Onion untuk menunjukkan keuntungan dari metode ini.



Arsitektur bawang adalah pembagian aplikasi menjadi beberapa lapisan. Apalagi, ada satu level independen, yang berada di pusat arsitektur.



Arsitektur Onion sangat bergantung pada Dependency Inversion. Antarmuka pengguna berinteraksi dengan logika bisnis melalui antarmuka.



Prinsip Pembalikan Ketergantungan
(Dependency Inversion Principle) , , . :

. .

. .






Proyek klasik dalam gaya ini memiliki empat lapisan:

  • Tingkat Objek Domain (Inti)
  • Tingkat penyimpanan (Repo)
  • Tingkat layanan
  • Lapisan Ujung Depan (Tes Web / Unit) (Api)




Semua lapisan diarahkan ke tengah (Inti). Pusatnya independen.



Tingkat Objek Domain



Ini adalah bagian utama dari aplikasi yang mendeskripsikan objek yang bekerja dengan database.



Mari buat proyek baru dalam solusi, yang akan memiliki tipe keluaran "Perpustakaan Kelas". Saya menamakannya WebApiTest.Core



Mari kita buat kelas BaseEntity yang akan memiliki properti umum dari objek.

BaseEntity.cs
    public class BaseEntity
    {
        public int id { get; set; }
    }
      
      







Off-top
, ยซidยป, , dateAdded, dateModifed ..



Selanjutnya, mari buat kelas Buku yang mewarisi dari BaseEntity

Book.cs
public class Book: BaseEntity

{

public string name { get; set; }

public string author { get; set; }

public int pages { get; set; }

public int readedPages { get; set; }

}





Untuk aplikasi kita, ini sudah cukup untuk saat ini, jadi mari kita lanjutkan ke level berikutnya.



Tingkat repositori



Sekarang mari kita lanjutkan ke implementasi level repositori. Buat proyek Perpustakaan Kelas bernama WebApiTest.Repo

Kita akan menggunakan Injeksi Ketergantungan, jadi kita akan melewatkan parameter melalui konstruktor untuk membuatnya lebih fleksibel. Jadi, kami membuat antarmuka repositori umum untuk operasi entitas sehingga kami dapat mengembangkan aplikasi yang digabungkan secara longgar. Potongan kode di bawah ini adalah untuk antarmuka IRepository.

IRepository.cs
    public interface IRepository <T> where T : BaseEntity
    {
        IEnumerable<T> GetAll();
        int Add(T item);
        T Get(int id);
        void Update(T item);
        void Delete(T item);
        void SaveChanges();
    }
      
      











Sekarang, mari mengimplementasikan kelas repositori untuk melakukan operasi database pada entitas yang mengimplementasikan IRepository. Repositori ini berisi konstruktor dengan parameter pathToBase, jadi ketika kita membuat turunan dari repositori, kita meneruskan jalur ke file sehingga kelas tahu dari mana mengambil data.

CsvRepository.cs
public class CsvRepository<T> : IRepository<T> where T : BaseEntity
    {
        private List<T> list;
        private string dbPath;
        private CsvConfiguration cfg = new CsvConfiguration(CultureInfo.InvariantCulture)
        {
            HasHeaderRecord = false,
            Delimiter = ";"
        };
        public CsvRepository(string pathToBase)
        {
            dbPath = pathToBase;
            using (var reader = new StreamReader(pathToBase)) {
                using (var csv = new CsvReader(reader, cfg)) {
                    list = csv.GetRecords<T>().ToList(); }
            }
        }

        public int Add(T item)
        {
            if (item == null)
                throw new Exception("Item is null");
            var maxId = list.Max(x => x.id);
            item.id = maxId + 1;
            list.Add(item);
            return item.id;
        }

        public void Delete(T item)
        {
            if (item == null)
                throw new Exception("Item is null");
            list.Remove(item);
        }

        public T Get(int id)
        {
            return list.SingleOrDefault(x => x.id == id);
        }

        public IEnumerable<T> GetAll()
        {
            return list;
        }

        public void SaveChanges()
        {
            using (TextWriter writer = new StreamWriter(dbPath, false, System.Text.Encoding.UTF8))
            {
                using (var csv = new CsvWriter(writer, cfg))
                {
                    csv.WriteRecords(list);
                }
            }
        }

        public void Update(T item)
        {
            if(item == null)
                throw new Exception("Item is null");
            var dbItem = list.SingleOrDefault(x => x.id == item.id);
            if (dbItem == null)
                throw new Exception("Cant find same item");
            dbItem = item;
        }
      
      









Kami telah mengembangkan entitas dan konteks yang diperlukan untuk bekerja dengan database.



Tingkat layanan



Kami sekarang membuat lapisan ketiga dari arsitektur bawang, yang merupakan lapisan layanan. Saya menamakannya WebApiText.Service. Lapisan ini berinteraksi dengan aplikasi web dan proyek repositori.



Kami membuat antarmuka bernama IBookService. Antarmuka ini berisi tanda tangan dari semua metode yang diakses oleh lapisan luar pada objek Buku.

IBookService.cs

public interface IBookService
    {
        IEnumerable<Book> GetBooks();
        Book GetBook(int id);
        void DeleteBook(Book book);
        void UpdateBook(Book book);
        void DeleteBook(int id);
        int AddBook(Book book);
    }
      
      







Sekarang mari kita implementasikan di kelas BookService

BookService.cs

public class BookService : IBookService
    {
        private IRepository<Book> bookRepository;
        public BookService(IRepository<Book> bookRepository)
        {
            this.bookRepository = bookRepository;
        }

        public int  AddBook(Book book)
        {
            return bookRepository.Add(book);
        }

        public void DeleteBook(Book book)
        {
            bookRepository.Delete(book);
        }
        public void DeleteBook(int id)
        {
            var book = bookRepository.Get(id);
            bookRepository.Delete(book);
        }

        public Book GetBook(int id)
        {
            return bookRepository.Get(id);
        }

        public IEnumerable<Book> GetBooks()
        {
            return bookRepository.GetAll();
        }

        public void UpdateBook(Book book)
        {
            bookRepository.Update(book);
        }
    }

      
      









Tingkat antarmuka eksternal





Sekarang kita membuat lapisan terakhir arsitektur bawang, yang, dalam kasus kita, adalah antarmuka eksternal, yang akan berinteraksi dengan aplikasi eksternal (bot, desktop, dll.). Untuk membuat lapisan ini, kami membersihkan proyek WebApiTest.Api kami dengan menghapus kelas Book dan membersihkan BooksController. Proyek ini memberikan kesempatan untuk operasi dengan database entitas, serta pengontrol untuk melakukan operasi ini.



Karena konsep injeksi ketergantungan adalah inti dari aplikasi ASP.NET Core, sekarang kita perlu mendaftarkan semua yang telah kita buat untuk digunakan dalam aplikasi.



Injeksi ketergantungan



Dalam aplikasi kecil di ASP.NET MVC, kita relatif dapat dengan mudah mengganti satu kelas dengan yang lain, daripada menggunakan satu konteks data, gunakan yang lain. Akan tetapi, pada aplikasi yang besar hal ini sudah akan menjadi masalah untuk dilakukan, apalagi jika kita memiliki lusinan pengontrol dengan ratusan metode. Dalam situasi ini, mekanisme seperti injeksi ketergantungan dapat membantu kami.



Dan jika sebelumnya di ASP.NET 4 dan versi sebelumnya yang lain perlu menggunakan berbagai kontainer IoC eksternal untuk menginstal dependensi, seperti Ninject, Autofac, Unity, Windsor Castle, StructureMap, maka ASP.NET Core sudah memiliki wadah injeksi dependensi bawaan, yang mana diwakili oleh antarmuka IServiceProvider. Dan dependensi itu sendiri juga disebut layanan, itulah sebabnya kontainer bisa disebut penyedia layanan. Penampung ini bertanggung jawab untuk memetakan dependensi ke jenis tertentu dan untuk memasukkan dependensi ke berbagai objek.



Pada awalnya, kami menggunakan tautan keras untuk menggunakan CsvDB di pengontrol.

private CsvDB db;

        public BookController()
        {
            db = new CsvDB();
        }
      
      





Pada pandangan pertama, tidak ada yang salah dengan ini, tetapi, misalnya, skema koneksi database telah berubah: alih-alih Csv, saya memutuskan untuk menggunakan MongoDB atau MySql. Selain itu, Anda mungkin perlu mengubah satu kelas ke kelas lain secara dinamis.



Dalam kasus ini, tautan keras mengikat pengontrol ke implementasi spesifik dari repositori. Kode ini lebih sulit untuk dipertahankan dan lebih sulit untuk diuji seiring pertumbuhan aplikasi Anda. Oleh karena itu, disarankan untuk beralih dari menggunakan komponen yang digabungkan secara kaku ke yang digabungkan secara longgar.



Dengan menggunakan berbagai teknik injeksi ketergantungan, Anda dapat mengelola siklus hidup layanan yang Anda buat. Layanan yang dihasilkan oleh Depedency Injection dapat berupa salah satu dari jenis berikut:



  • Transient: . , . ,
  • Scoped: . , .
  • Singleton: ,


Metode AddTransient (), AddScoped () dan AddSingleton () yang sesuai digunakan untuk membuat setiap jenis layanan dalam penampung inti .net yang disematkan.



Kita bisa menggunakan kontainer standar (penyedia layanan), tetapi tidak mendukung penerusan parameter, jadi saya harus menggunakan perpustakaan Autofac.



Untuk melakukan ini, tambahkan dua paket ke proyek melalui NuGet: Autofac dan Autofac.Extensions.DependencyInjection.

Sekarang kita mengubah metode ConfigureServices di file Startup.cs menjadi:

ConfigureServices
 public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            var builder = new ContainerBuilder();// 
            builder.RegisterType<CsvRepository<Book>>()// CsvRepository
                .As<IRepository<Book>>() //  IRepository
                .WithParameter("pathToBase", @"C:\csv\books.csv")//  pathToBase
                .InstancePerLifetimeScope(); //Scope
            builder.RegisterType<BookService>()
                .As<IBookService>()
                .InstancePerDependency(); //Transient 
            builder.Populate(services); // 
            var container = builder.Build();
            return new AutofacServiceProvider(container);
        }
      
      









Dengan cara ini kami telah mengikat semua implementasi ke antarmuka mereka.



Mari kembali ke proyek WebApiTest.Api kita.

Semua yang tersisa adalah mengubah BooksController.cs

BooksController.cs
[Route("[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private IBookService service;
        public BooksController(IBookService service)
        {
            this.service = service;
        }

        [HttpGet]
        public ActionResult<IEnumerable<Book>> Get()
        {
            return new JsonResult(service.GetBooks());
        }

        [HttpGet("{id}")]
        public ActionResult<Book> Get(int id)
        {
            return new JsonResult(service.GetBook(id));
        }

        [HttpPost]
        public void Post([FromBody] Book item)
        {
            service.AddBook(item);
        }

        [HttpPut("{id}")]
        public void Put([FromBody] Book item)
        {
            service.UpdateBook(item);
        }

        [HttpDelete("{id}")]
        public void Delete(int id)
        {
            service.DeleteBook(id);
        }
    }
      
      









Tekan F5, tunggu browser terbuka, buka / books dan ...

[{"name":"Test","author":"Test","pages":100,"readedPages":0,"id":1}]
      
      







Hasil:



Dalam teks ini, saya ingin memperbarui semua pengetahuan saya tentang pola arsitektur Onion, serta tentang injeksi ketergantungan, menggunakan Autofac.

Saya pikir tujuan tercapai, terima kasih telah membaca;)



n-Tier
n- .

โ€” . . , .

. ( ). , . . , - .




All Articles