كيف كنت أستعد للمقابلة رقم 2

في الجزء الثاني ، أردت تحديث فهمي لبنية Onion و n-Tier ، بالإضافة إلى إطارات DI (Autofac و net core المدمج). لكن بالنظر إلى حجم النص ، أدركت أنه سيتم وصف n-Tier بإيجاز شديد ، والذي أعتذر عنه على الفور.

سأحاول أيضًا مراعاة التعليقات الواردة في الجزء الأول ،







هندسة البصل



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



(Tg Bot ، تطبيق الهاتف ، سطح المكتب) => Asp.net Web Api => قاعدة بيانات



أنشئ مشروعًا في Visual studio من نوع Asp.net Core ، حيث نختار أيضًا نوع مشروع Web Api.

كيف تختلف عن المعتاد؟

أولاً ، ترث فئة وحدة التحكم من فئة ControllerBase ، المصممة لتكون الفئة الأساسية لـ MVC بدون دعم لإرجاع العروض (كود html).

ثانيًا ، تم تصميمه لتنفيذ خدمات REST التي تغطي جميع أنواع طلبات HTTP ، واستجابة للطلبات ، تتلقى json مع إشارة صريحة إلى حالة الاستجابة. سترى أيضًا أن وحدة التحكم الافتراضية سيتم تمييزها بالسمة [ApiController] ، والتي تحتوي على خيارات مفيدة خاصة لواجهة برمجة التطبيقات.



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



لذلك أقوم بإنشاء فصل يصف الكتاب:

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

      
      









ثم أصف الفصل للعمل مع قاعدة البيانات:

ملف 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;
        }
    }
}

      
      







ثم الأمر صغير ، لإضافة API لتتمكن من التفاعل معها:

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

      
      







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

رائع! لكن لا ، طلبت الزوجة أن تتمكن أيضًا من الوصول إلى مثل هذا الشيء الملائم.

ما الصعوبات التي تنتظرنا؟ أولاً ، تحتاج الآن إلى إضافة عمود لجميع الكتب يشير إلى معرف المستخدم. صدقني ، لن يكون الأمر مريحًا مع ملف csv. أيضا ، الآن تحتاج إلى إضافة المستخدمين أنفسهم! وحتى الآن هناك حاجة إلى نوع من المنطق حتى لا ترى زوجتي أنني انتهيت من قراءة مجموعة Dontsova الثالثة بدلاً من Tolstoy الموعودة.



دعنا نحاول توسيع هذا المشروع إلى المتطلبات المطلوبة:

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

بصراحة ، أردت أن أكتب مثالاً ، لكن عدد الأشياء التي لا أرغب في القيام بها قتل الرغبة بشدة:

إنشاء وحدة تحكم تكون مسؤولة عن التصريح وإرسال البيانات إلى المستخدم ؛

إنشاء مستخدم كيان جديد ومعالج له ؛

دفع المنطق إما إلى وحدة التحكم نفسها ، مما يجعلها منتفخة ، أو في فئة منفصلة ؛

إعادة كتابة منطق العمل مع "قاعدة البيانات" ، لأنه الآن أو ملفين csv ، أو الانتقال إلى قاعدة البيانات ...



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



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



مرجع التاريخ
«» .

« » 2008 . , , , . — , , , .







لذلك سنحاول تنفيذ تطبيقنا بأسلوب Onion لإظهار مزايا هذه الطريقة.



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



تعتمد بنية البصل بشكل كبير على انعكاس التبعية. تتفاعل واجهة المستخدم مع منطق الأعمال من خلال الواجهات.



مبدأ انعكاس التبعية
(Dependency Inversion Principle) , , . :

. .

. .






يتكون المشروع الكلاسيكي بهذا النمط من أربع طبقات:

  • مستوى كائن المجال (أساسي)
  • مستوى المستودع (الريبو)
  • مستوى الخدمة
  • طبقة الواجهة الأمامية (اختبار الويب / الوحدة) (Api)




جميع الطبقات موجهة نحو المركز (النواة). المركز مستقل.



مستوى كائن المجال



هذا هو الجزء المركزي من التطبيق الذي يصف الكائنات التي تعمل مع قاعدة البيانات.



لنقم بإنشاء مشروع جديد في الحل ، والذي سيكون له نوع الإخراج "Class Library". سميته WebApiTest.Core



لنقم بإنشاء فئة BaseEntity التي سيكون لها خصائص مشتركة للكائنات.

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







خارج القمة
, «id», , dateAdded, dateModifed ..



بعد ذلك ، دعنا ننشئ فئة كتاب ترث من 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; }

}





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



مستوى المستودع



الآن دعنا ننتقل إلى تنفيذ مستوى المستودع. قم بإنشاء مشروع مكتبة الفصل الدراسي يسمى WebApiTest.Repo سنستخدم حقن

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











الآن ، دعنا ننفذ فئة المستودع لإجراء عمليات قاعدة البيانات على كيان يقوم بتنفيذ IRepository. يحتوي هذا المستودع على مُنشئ مع معلمة pathToBase ، لذلك عندما نقوم بإنشاء مثيل من المستودع ، فإننا نمرر مسار الملف حتى يعرف الفصل من أين يحصل على البيانات.

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









لقد قمنا بتطوير الكيان والسياق المطلوبين للعمل مع قاعدة البيانات.



مستوى الخدمة



نقوم الآن بإنشاء الطبقة الثالثة من بنية البصل ، وهي طبقة الخدمة. سميته WebApiText.Service. تتفاعل هذه الطبقة مع كل من تطبيقات الويب ومشاريع المستودعات.



نقوم بإنشاء واجهة باسم IBookService. تحتوي هذه الواجهة على توقيع جميع الطرق التي تم الوصول إليها بواسطة الطبقة الخارجية في كائن الكتاب.

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







الآن دعنا نطبقها في فئة 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);
        }
    }

      
      









مستوى الواجهة الخارجية





نقوم الآن بإنشاء الطبقة الأخيرة من بنية البصل ، والتي ، في حالتنا ، هي الواجهة الخارجية ، والتي ستتفاعل معها التطبيقات الخارجية (الروبوت ، سطح المكتب ، إلخ). لإنشاء هذه الطبقة ، نقوم بتنظيف مشروع WebApiTest.Api الخاص بنا عن طريق إزالة فئة الكتاب وتنظيف BooksController. يوفر هذا المشروع فرصة لعمليات قاعدة بيانات الكيانات ، بالإضافة إلى وحدة تحكم لتنفيذ هذه العمليات.



نظرًا لأن مفهوم Dependency Injection أساسي لتطبيق ASP.NET Core ، نحتاج الآن إلى تسجيل كل ما أنشأناه للاستخدام في التطبيق.



حقن التبعية



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



وإذا كان في وقت سابق في ASP.NET 4 والإصدارات السابقة الأخرى ، كان من الضروري استخدام العديد من حاويات IoC الخارجية لتثبيت التبعيات ، مثل Ninject و Autofac و Unity و Windsor Castle و StructureMap ، فإن ASP.NET Core لديها بالفعل حاوية حقن تبعية مضمنة ، والتي ممثلة بواجهة IServiceProvider. وتسمى التبعيات نفسها أيضًا الخدمات ، ولهذا السبب يمكن تسمية الحاوية بمزود خدمة. هذه الحاوية مسؤولة عن تعيين التبعيات لأنواع محددة وعن إدخال التبعيات في كائنات مختلفة.



في البداية ، استخدمنا الارتباط الثابت لاستخدام CsvDB في وحدة التحكم.

private CsvDB db;

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





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



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



باستخدام مجموعة متنوعة من تقنيات حقن التبعية ، يمكنك إدارة دورة حياة الخدمات التي تنشئها. يمكن أن تكون الخدمات التي تم إنشاؤها بواسطة Depedency Injection من أحد الأنواع التالية:



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


يتم استخدام أساليب AddTransient () و AddScoped () و AddSingleton () المقابلة لإنشاء كل نوع من الخدمات في الحاوية الأساسية net.



يمكننا استخدام حاوية قياسية (مزود خدمة) ، لكنها لا تدعم تمرير المعلمة ، لذلك سأضطر إلى استخدام مكتبة Autofac.



للقيام بذلك ، أضف حزمتين إلى المشروع عبر NuGet: Autofac و Autofac.Extensions.DependencyInjection.

نقوم الآن بتغيير طريقة ConfigureServices في ملف Startup.cs إلى:

تكوين الخدمات
 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);
        }
      
      









بهذه الطريقة قمنا بربط جميع التطبيقات بواجهاتها.



دعنا نعود إلى مشروع WebApiTest.Api الخاص بنا.

كل ما تبقى هو تغيير 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);
        }
    }
      
      









اضغط على F5 ، وانتظر حتى يفتح المتصفح ، وانتقل إلى / books و ...

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







النتيجة:



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

أعتقد أن الهدف قد تحقق ، شكرًا على القراءة ؛)



ن الطبقة
n- .

— . . , .

. ( ). , . . , - .




All Articles