Cải tiến view (3): che giấu, ghi đè, kế thừa và generic

    0

    Trong bài học này chúng ta sẽ xem xét khái niệm và kỹ thuật ghi đè, che giấu phương thức, và cách sử dụng lớp generic trong kế thừa. Chúng ta sẽ vận dụng để tiếp tục cải tiến các lớp view. Ngoài ra, chúng ta sẽ tiếp tục xem xét một số vấn đề khác của kế thừa trước khi đi vào cải tiến các lớp giao diện vận dụng các kỹ thuật mới.

    Để hiểu được bài thực hành này, bạn cần nắm được các vấn đề liên quan đến kế thừa (cụ thể là vấn đề che giấu và ghi đè thành viên) và generic.

    Nhắc lại quan hệ giữa kế thừa và đa hình

    Trong lập trình hướng đối tượng, kế thừa và đa hình là hai nguyên lý khác nhau.

    Đa hình thiết lập mối quan hệ “” (is-a relationship) giữa kiểu cơ sở và kiểu dẫn xuất. Ví dụ, nếu chúng ta có lớp cơ sở Bird và lớp dẫn xuất Parrot thì một object của Parot cũng là object của Bird, kiểu Parrot cũng là kiểu Bird (đương nhiên rồi, vẹt là chim mà!). Mối quan hệ này nhìn rất giống như quan hệ kế thừa ở trên.

    Trong khi đó, kế thừa liên quan chủ yếu đến tái sử dụng code: code của lớp con thừa hưởng code của lớp cha. Một cách nói khác, đa hình liên quan tới quan hệ về ngữ nghĩa, còn kế thừa liên quan tới cú pháp.

    Trong các ngôn ngữ như C++, C#, Java, hai khái niệm này hầu như được đồng nhất, thể hiện ở chỗ:

    1. class con thừa hưởng các thành viên của class cha (kế thừa, tái sử dụng code);
    2. một object thuộc kiểu con có thể gán cho biến thuộc kiểu cha, tức là kiểu cơ sở có thể dùng để thay thế cho kiểu dẫn xuất (đa hình).

    Để áp dụng được cơ chế ghi đè, cả lớp cha và lớp con cần phải phối hợp:

    1. lớp cha phải cho phép phương thức được phép ghi đè bằng cách thêm từ khóa virtual trước khai báo phương thức;
    2. lớp con phải thông báo rõ việc ghi đè bằng cách thêm từ khóa override trước định nghĩa phương thức.

    Mặc định các phương thức của class không cho ghi đè mà chỉ cho phép che giấu.

    Tuy nhiên, các phương thức Equals, GetHashCode, ToString của lớp tổ tiên System.Object đều cho phép ghi đè ở lớp hậu duệ.

    Để xác định những phương thức nào cho phép ghi đè, chỉ cần viết từ khóa override trong thân class (bên ngoài phương thức).

    Ghi đè (override) phương thức
    Ghi đè (override) phương thức

    Che dấu được sử dụng chủ yếu để đảm bảo tương thích ngược giữa các class. Cơ chế này không được sử dụng nhiều trong thực tế.

    Ở phía khác, ghi đè được sử dụng rất phổ biến cùng với đa hình giúp tạo ra một class đại diện cho các biến thể khác nhau.

    Thực hành 1: cải tiến lớp ViewBase sử dụng ghi đè

    Bước 1. Thay đổi lớp ViewBase

    public class ViewBase
    {
        protected object Model;
        protected Router Router = Router.Instance;
    
        public ViewBase() { }
        public ViewBase(object model) => Model = model;
        // bổ sung phương thức virtual Render, cho phép ghi đè
        public virtual void Render() { }
        // chuyển phương thức RenderToFile sang virtual
        public virtual void RenderToFile(string path)
        {
            ViewHelp.WriteLine($"Saving data to file '{path}'");
            var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
            File.WriteAllText(path, json);
            ViewHelp.WriteLine("Done!");
        }
    }
    

    Ở bước này, chúng ta bổ sung phương thức Render với đánh dấu virtual để các lớp con có thể ghi đè phương thức này. Chúng ta cũng chuyển phương thức RenderToFile sang virtual để các lớp con nếu cần có thể ghi đè (ví dụ, để xuất sang một định dạng khác như xml hoặc plain text).

    Bước 2. Điều chỉnh khai báo của phương thức Render

    Thay đổi phương thức Render trên cả 4 lớp view như sau:

    public override void Render() …

    Bước điều chỉnh này chỉ đơn giản là thêm từ khóa override vào trước khai báo của phương thức Render trong từng lớp giao diện.

    Bước 3. Xây dựng lớp ControllerBase

    Tạo file ControllerBase.cs trong thư mục Framework cho lớp ControllerBase với code như sau:

    namespace Framework
    {  
        public class ControllerBase
        {        
            public virtual void Render(ViewBase view, string path = "", bool both = false)
            {
                if (string.IsNullOrEmpty(path)) { view.Render(); return; }
    
                if (both)
                {
                    view.Render();
                    view.RenderToFile(path);
                    return;
                }
                view.RenderToFile(path);
            }
        }
    }
    

    Lớp ControllerBase định nghĩa một phương thức Render giúp gọi tới phương thức Render hoặc RenderToFile của các lớp view một cách tiện lợi.

    Bước 4. Điều chỉnh lớp BookController

    namespace BookMan.ConsoleApp.Controllers
    {
        using DataServices;
        using Framework;
        using Views;
    
        internal class BookController : ControllerBase
        {
            protected Repository Repository;
    
            public BookController(SimpleDataAccess context)
            {
                Repository = new Repository(context);
            }
    
            public void Single(int id, string path = "")
            {
                var model = Repository.Select(id);
                Render(new BookSingleView(model), path);
            }
    
            public void Create()
            {
                Render(new BookCreateView());
            }
    
            public void List(string path = "")
            {
                var model = Repository.Select();
                Render(new BookListView(model), path);
            }
    
            public void Update(int id)
            {
                var model = Repository.Select(id);
                Render(new BookUpdateView(model));
            }
        }
    }
    

    Trong điều chỉnh này chúng ta vận dụng phương thức Render kế thừa từ ControllerBase giúp đơn giản hóa làm việc với các lớp view.

    Có thể để ý thấy rằng, phương thức Render kế thừa từ ControllerBase yêu cầu kiểu đầu vào là ViewBase nhưng chúng ta có thể cung cấp bất kỳ object nào của các lớp view kế thừa từ ViewBase. Điều này đạt được nhờ cơ chế đa hình kết hợp kế thừa mà chúng ta đã xem xét ở phần lý thuyết trên.

    Do cơ chế ghi đè, phương thức Render được gọi không phải là Render của lớp cha ViewBase mà là phương thức Render của từng lớp con. Chúng ta có thể thấy ghi đè là cơ chế rất mạnh giúp chúng ta chỉ cần viết code một lần cho kiểu cha nhưng có thể vận dụng cho các kiểu con và sử dụng được những đặc thù riêng của kiểu con. Nhờ cơ chế này chúng ta không cần viết code xử lý cho từng lớp con cụ thể.

    Thực hành 2: kết hợp kế thừa và generic

    Bước 1. Điều chỉnh file ViewBase.cs

    using System.IO;
    
    namespace Framework
    {
        public class ViewBase
        {
            protected Router Router = Router.Instance;
    
            public ViewBase() { }
    
            public virtual void Render() { }
        }
    
        public class ViewBase<T> : ViewBase
        {
            protected T Model;
            public ViewBase(T model) => Model = model;
    
            public virtual void RenderToFile(string path)
            {
                ViewHelp.WriteLine($"Saving data to file '{path}'");
                var json = Newtonsoft.Json.JsonConvert.SerializeObject(Model);
                File.WriteAllText(path, json);
                ViewHelp.WriteLine("Done!");
            }
        }
    }
    

    Ở bước này trong file ViewBase.cs chúng ta tách một phần lớp ViewBase ra thành một lớp generic riêng ViewBase<T> và cho lớp này kế thừa từ ViewBase.

    Trong lớp ViewBase<T> sẽ tập trung những phương thức phải sử dụng thông tin từ model. Kiểu dữ liệu của model sẽ được quyết định khi các lớp con kế thừa từ lớp ViewBase<T>.

    Bước 2. Điều chỉnh các lớp view

    Lớp BookListView

    using System;
    
    namespace BookMan.ConsoleApp.Views
    {
        using Framework;
        using Models;
    
        internal class BookListView : ViewBase<Book[]>
        {
            public BookListView(Book[] model) : base(model) { }
    
            public override void Render()
            {
                if (Model.Length == 0)
                {
                    ViewHelp.WriteLine("No book found!", ConsoleColor.Yellow);
                    return;
                }
    
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine("THE BOOK LIST");
                Console.ForegroundColor = ConsoleColor.Yellow;
    
                foreach (Book b in Model)
                {
                    ViewHelp.Write($"[{b.Id}]", ConsoleColor.Yellow);
                    ViewHelp.WriteLine($" {b.Title}", b.Reading ? ConsoleColor.Cyan : ConsoleColor.White);
                }
    
                ViewHelp.WriteLine($"{Model.Length} item(s)", ConsoleColor.Green);
            }
        }
    }
    

    Lớp BookSingleView

    using System;
    
    namespace BookMan.ConsoleApp.Views
    {
        using Framework;
        using Models;
    
        internal class BookSingleView : ViewBase<Book>
        {
            public BookSingleView(Book model) : base(model) { }
    
            public override void Render()
            {
                if (Model == null)
                {                
                    ViewHelp.WriteLine("NO BOOK FOUND. SORRY!", ConsoleColor.Red);
                    return; 
                }
    
                ViewHelp.WriteLine("BOOK DETAIL INFORMATION", ConsoleColor.Green);            
    
                Console.WriteLine($"Authors:     {Model.Authors}");
                Console.WriteLine($"Title:       {Model.Title}");
                Console.WriteLine($"Publisher:   {Model.Publisher}");
                Console.WriteLine($"Year:        {Model.Year}");
                Console.WriteLine($"Edition:     {Model.Edition}");
                Console.WriteLine($"Isbn:        {Model.Isbn}");
                Console.WriteLine($"Tags:        {Model.Tags}");
                Console.WriteLine($"Description: {Model.Description}");
                Console.WriteLine($"Rating:      {Model.Rating}");
                Console.WriteLine($"Reading:     {Model.Reading}");
                Console.WriteLine($"File:        {Model.File}");
                Console.WriteLine($"File Name:   {Model.FileName}");
            }
        }
    }
    

    Lớp BookUpdateView

    using System;
    
    namespace BookMan.ConsoleApp.Views
    {
        using Framework;
        using Models;
    
        internal class BookUpdateView : ViewBase<Book>
        {
            public BookUpdateView(Book model) : base(model)
            {
            }
    
            public override void Render()
            {
                ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green);
    
                var authors = ViewHelp.InputString("Authors", Model.Authors);
                var title = ViewHelp.InputString("Title", Model.Title);
                var publisher = ViewHelp.InputString("Publisher", Model.Publisher);
                var isbn = ViewHelp.InputString("Isbn", Model.Isbn);
                var tags = ViewHelp.InputString("Tags", Model.Tags);
                var description = ViewHelp.InputString("Description", Model.Description);
                var file = ViewHelp.InputString("File", Model.File);
                var year = ViewHelp.InputInt("Year", Model.Year);
                var edition = ViewHelp.InputInt("Edition", Model.Edition);
                var rating = ViewHelp.InputInt("Rate", Model.Rating);
                var reading = ViewHelp.InputBool("Reading", Model.Reading);
            }
        }
    }
    

    Chúng ta thấy, lớp generic cũng có thể đóng vai trò lớp cha để tạo ra các lớp con.

    Trong bước này chúng ta điều chỉnh để các lớp view (trừ lớp BookCreateView không cần dữ liệu từ BookController) kế thừa từ lớp cha generic ViewBase<T>, trong đó T được thay thế bằng các kiểu dữ liệu mà view chờ đợi từ controller: BookListView chờ đợi một mảng Book[], BookSingleViewBookUpdateView chờ đợi một object kiểu Book.

    Do đó, Book[]Book được sử dụng cho vị trí của T khi dùng ViewBase<T> làm lớp cơ sở. Các lớp con kế thừa từ ViewBase<T> trong trường hợp này lại không phải là lớp generic nữa, vì chúng ta đã cung cấp kiểu dữ liệu cụ thể cho lớp cha trước khi cho lớp con kế thừa.

    Lưu ý rằng ViewBaseViewBase<T> nhìn tương tự nhau nhưng đây là hai lớp khác nhau (thể hiện rằng chúng có thể kế thừa nhau). Thông thường các lớp non-generic và generic hay đi thành cặp với nhau.

    Bước 3. Điều chỉnh lớp ControllerBase

    namespace Framework
    {  
    
        public class ControllerBase
        {        
            public virtual void Render(ViewBase view) { view.Render(); }
    
            public virtual void Render<T>(ViewBase<T> view, string path = "", bool both = false)
            {
                if (string.IsNullOrEmpty(path)) { view.Render(); return; }
    
                if (both)
                {
                    view.Render();
                    view.RenderToFile(path);
                    return;
                }
    
                view.RenderToFile(path);
            }
        }
    }
    

    Bước điều chỉnh này giúp ControllerBase “thích nghi” với hai lớp ViewBaseViewBase<T>. Phương thức Render thứ nhất chỉ làm việc với object kiểu ViewBase; phương thức thứ hai thuộc loại generic và làm việc với object của ViewBase<T>.

    Bước 4. Dịch và chạy thử chương trình với tất cả các lệnh đã biết

    Kết quả thực hiện chương trình
    Kết quả thực hiện chương trình

    Kết luận

    Trong bài này chúng ta đã xem xét hai vấn đề quan trọng liên quan tới kế thừa: ghi đè và che giấu phương thức. Chúng ta cũng áp dụng các kỹ thuật này để tiếp tục cải tiến các lớp view.

    + Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
    + Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
    + Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
    Cảm ơn bạn!

    Subscribe
    Notify of
    guest
    0 Thảo luận
    Inline Feedbacks
    View all comments