Console MVC Library cho .NET (2): truy vấn, router

Truy vấn, đề xuất cấu trúc truy vấn, cách xây dựng router

0

Trong phần 1 của loạt bài này chúng ta đã nói tới một số ưu thế của ứng dụng console, các vấn đề của lập trình enterprise (áp dụng chung cho các loại ứng dụng) và nhắc lại một số ý cơ bản của mô hình MVC. Chúng ta chưa đề cập gì đến việc xây dựng MVC framework cho console.

Loạt bài Xây dựng thư viện hỗ trợ ứng dụng Console:
Phần 1 – Giới thiệu chung
Phần 2 – Truy vấn, xây dựng router
Phần 3 – Hỗ trợ I/O với console, view, controller
Phần 4 – Ví dụ minh họa

Trong bài này chúng ta sẽ xây dựng thành phần đầu tiên và đặc biệt quan trọng: router. Thành phần này có thể áp dụng cho mọi ứng dụng console, không nhất thiết phải theo mô hình MVC.

Router và truy vấn

Chế độ hoạt động của ứng dụng console

Ứng dụng console thường hoạt động theo chế độ request/response (truy vấn/phản hồi). Nghĩa là người dùng nhập vào một truy vấn, ứng dụng thực hiện lệnh tương ứng và trả kết quả, sau đó lại tiếp tục chờ nghe truy vấn mới.

Chế độ này hoàn toàn giống như trong hệ thống web: trình duyệt gửi truy vấn; server xử lý truy vấn và trả cho client các tài liệu được yêu cầu; trình duyệt hiển thị kết quả.

Do đặc điểm của console, mỗi truy vấn đều là một chuỗi văn bản, kết quả nhận được cũng là văn bản. Trong quá trình thực hiện lệnh cho đến lúc nhận lại kết quả, giao diện sẽ “treo” và không thể tiếp nhận bất kỳ thông tin gì.

Vai trò của router

Theo nguyên tắc của MVC, tất cả yêu cầu của người dùng hoặc lệnh phát ra từ giao diện đều được chuyển cho controller xử lý. Điều này có nghĩa là phải có một cơ chế cho phép ánh xạ mỗi truy vấn của người dùng với việc thực thi một phương thức của controller.

Nhiệm vụ này được thực hiện bởi một class đặc biệt gọi là router.

Router trong các MVC framework có nhiệm vụ tiếp nhận truy vấn của người dùng, phân tích ra các thành phần chính, và gọi phương thức tương ứng của controller. Mỗi phương thức của controller thường được gọi tắt là một action (hành động). Một truy vấn thường gọi tới một action.

Khi xây dựng router đồng thời phải đưa ra quy tắc và cấu trúc của truy vấn.

Cấu trúc truy vấn

Trong framework này chúng ta sẽ sử dụng truy vấn với cấu trúc như sau:

lệnh ? khóa_1 = giá_trị_1 & khóa_2 = giá_trị_2

Cấu trúc này được tham khảo từ cách viết tham số trong truy vấn GET của HTTP.

Trong cấu trúc này, một truy vấn bao gồm hai thành phần:

  1. lệnh: là một chuỗi ký tự bất kỳ. Lệnh ở đây tương đương với Url của HTTP GET.
  2. các tham số: bao gồm các cặp “khóa = giá_trị”. Các cặp tham số tách nhau bởi ký tự “&”.

Lệnh và các tham số viết tách nhau bởi dấu “?” (giống truy vấn GET).

Như vậy, cấu trúc trên đảm bảo được việc truyền lệnh và tham số: phần lệnh cho chương trình biết cần làm gì (sẽ chạy phương thức nào); phần tham số cung cấp dữ liệu cho phương thức.

Ví dụ, nếu chúng ta muốn ra lệnh giải phương trình bậc hai có thể nghĩ ra một truy vấn như sau: solve ? a=1 & b=2 & c=3.

Rõ ràng cấu trúc này khá phù hợp, đơn giản và có phần quen thuộc.

Xây dựng Router

Chúng ta bắt tay vào code. Trước hết tạo một solution trống đặt tên là MvcConsole. Trong solution này tạo một project theo template Class Library (.NET framework) đặt tên là Framework.

Lớp hỗ trợ phân tích và lưu trữ tham số

Tạo một file mã nguồn mới Parameter.cs cho lớp Parameter với code như sau

Solution MvcConsole với một project Framework (Class Library)
Solution MvcConsole với một project Framework (Class Library)
using System;
using System.Collections.Generic;

namespace Framework
{    
    /// <summary>
    /// lưu các cặp khóa-giá trị người dùng nhập;
    /// chuỗi tham số cần viết ở dạng khóa=giá trị;
    /// nếu có nhiều tham số thì viết tách nhau bằng ký tự &
    /// </summary>
    public class Parameter
    {
        private readonly Dictionary<string, string> _pairs = new Dictionary<string, string>();

        /// <summary>
        /// nạp chồng phép toán indexing []; cho phép truy xuất giá trị theo kiểu biến[khóa] = giá_trị;
        /// </summary>
        /// <param name="key">khóa</param>
        /// <returns>giá trị tương ứng</returns>
        public string this[string key] // để nạp chồng phép toán indexing phải viết hai phương thức get,set
        {
            get {
                if (_pairs.ContainsKey(key))
                    return _pairs[key];
                else return null;
            } // phương thức get trả lại giá trị từ dictionary
            set => _pairs[key] = value; // phương thức set gán giá trị cho dictionary
        }

        /// <summary>
        /// Kiểm tra xem một khóa có trong danh sách tham số không
        /// </summary>
        /// <param name="key">khóa cần kiểm tra</param>
        /// <returns></returns>
        public bool ContainsKey(string key)
        {
            return _pairs.ContainsKey(key);
        }

        /// <summary>
        /// nhận chuỗi ký tự và phân tích, chuyển thành các cặp khóa-giá trị
        /// </summary>
        /// <param name="parameter">chuỗi ký tự theo quyt tắc khóa_1=giá_trị_1&khóa-2=giá_trị2</param>
        public Parameter(string parameter)
        {
            // cắt chuỗi theo mốc là ký tự &
            // kết quả của phép toán này là một mảng, mỗi phần tử là một chuỗi có dạng khóa = giá_trị
            var pairs = parameter.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var pair in pairs)
            {
                var p = pair.Split('='); // cắt mỗi phần tử lấy mốc là ký tự =
                if (p.Length == 2) // một cặp khóa = giá_trị đúng sau khi cắt sẽ phải có 2 phần
                {
                    var key = p[0].Trim(); // phần tử thứ nhất là khóa
                    var value = p[1].Trim(); // phần tử thứ hai là giá trị
                    this[key] = value; // lưu cặp khóa-giá trị này lại sử dụng phép toán indexing

                    // cũng có thể viết theo kiểu khác, trực tiếp sử dụng biến _pairs
                    // _pairs[key] = value;
                }
            }
        }
    }
}

Lớp hỗ trợ phân tích truy vấn

Tạo file mã nguồn Request.cs trực thuộc project Framework cho lớp Request. Viết code cho lớp Request như sau:

using System;
namespace Framework
{
    /// <summary>
    /// lớp xử lý truy vấn
    /// </summary>
    public class Request
    {
        /// <summary>
        /// thành phần lệnh của truy vấn
        /// </summary>
        public string Route { get; private set; }
        /// <summary>
        /// thành phần tham số của truy vấn
        /// </summary>
        public Parameter Parameter { get; private set; }
        public Request(string request)
        {
            Analyze(request);
        }
        /// <summary>
        /// phân tích truy vấn để tách ra thành phần lệnh và thành phần tham số
        /// </summary>
        /// <param name="request"></param>
        private void Analyze(string request)
        {
            // tìm xem trong chuỗi truy vấn có tham số hay không
            var firstIndex = request.IndexOf('?');
            // trườn hợp truy vấn không chứa tham số
            if (firstIndex < 0)
            {
                Route = request.ToLower().Trim();
            }
            // trường hợp truy vấn chứa tham số
            else
            {
                // nếu chuỗi lối (chỉ chứa tham số, không chứa route)
                if (firstIndex <= 1)
                {
                    throw new Exception("Invalid request parameter");
                }
                // cắt chuỗi truy vấn lấy mốc là ký tự ?
                // sau phép toán này thu được mảng 2 phần tử: thứ nhất là route, thứ hai là chuỗi parameter
                var tokens = request.Split(new[] { '?' }, 2, StringSplitOptions.RemoveEmptyEntries);
                // route là thành phần lệnh của truy vấn
                Route = tokens[0].Trim().ToLower();
                // parameter là thành phần tham số của truy vấn
                var parameterPart = request.Substring(firstIndex + 1).Trim();
                Parameter = new Parameter(parameterPart);
            }
        }
    }
}

Lớp Router

Với hai lớp hỗ trợ ở trên, chúng ta xây dựng lớp Router để thực hiện nhiệm vụ như đã mô tả. Tạo mới file mã nguồn Router.cs trực thuộc project cho lớp Router và code như sau:

using System;
using System.Collections.Generic;
using System.Text;

namespace Framework
{
    /* đây không phải là lệnh sử dụng không gian tên
     * mà là tạo biệt danh cho một kiểu dữ liệu
     * ở đây đang tạo một biệt danh cho kiểu Dictionary<string, ControllerAction>.
     * trong cả file này có thể sử dụng tên kiểu RoutingTable 
     * thay cho Dictionary<string, ControllerAction> 
     */
    using RoutingTable = Dictionary<string, ControllerAction>;
    /// <summary>
    /// delegate này đại diện cho tất cả các phương thức có:
    /// - kiểu ra là void,
    /// - danh sách tham số vào là (Parameter)
    /// </summary>
    /// <param name="parameter"></param>
    public delegate void ControllerAction(Parameter parameter = null);
    /// <summary>
    /// lớp cho phép ánh xạ truy vấn với phương thức
    /// </summary>
    public class Router
    {
        // nhóm 3 lệnh dưới đây biến Router thành một singleton
        private static Router _instance;
        private Router()
        {
            _routingTable = new RoutingTable();
            _helpTable = new Dictionary<string, string>();
        }
        // để ý: constructor là private
        // người sử dụng class thông qua property này để truy xuất các phương thức của class
        // chỉ khi nào _instance == null mới tạo object. Một khi đã tạo object, _instance sẽ
        // không có giá trị null nữa.
        // vì là biến static, _instance một khi được khởi tạo sẽ tồn tại suốt chương trình
        public static Router Instance => _instance ?? (_instance = new Router());
        // lưu ý: ở đây đang sử dụng alias của Dictionary<string, ControllerAction> cho ngắn gọn
        private readonly RoutingTable _routingTable;
        private readonly Dictionary<string, string> _helpTable;
        public string GetRoutes()
        {
            StringBuilder sb = new StringBuilder();
            foreach (var k in _routingTable.Keys)
                sb.AppendFormat("{0}, ", k);
            return sb.ToString().TrimEnd(',' ,' ');
        }
        public string GetHelp(string key)
        {
            if (_helpTable.ContainsKey(key))
                return _helpTable[key];
            else
                return "Documentation not ready yet!";
        }
        /// <summary>
        /// đăng ký một route mới, mỗi route ánh xạ một chuỗi truy vấn với một phương thức
        /// </summary>
        /// <param name="route"></param>
        /// <param name="action"></param>
        public void Register(string route, ControllerAction action, string help = "")
        {
            // nếu _routingTable đã chứa route này thì bỏ qua
            if (!_routingTable.ContainsKey(route))
            {
                _routingTable[route] = action;
                _helpTable[route] = help;
            }
        }
        /// <summary>
        /// phân tích truy vấn và gọi phương thức tương ứng với chuỗi truy vấn
        /// <para>chuỗi truy vấn bao gồm hai phần: route và parameter, phân tách bởi ký tự ?</para>
        /// </summary>
        /// <param name="request">chuỗi truy vấn, bao gồm hai phần: 
        /// route, paramete; phân tách bởi ký tự ?</param>
        public void Forward(string request)
        {
            var req = new Request(request);
            if (!_routingTable.ContainsKey(req.Route))
                throw new Exception("Command not found!");
            if (req.Parameter == null)
                _routingTable[req.Route]?.Invoke();
            else
                _routingTable[req.Route]?.Invoke(req.Parameter);
        }
    }
}

Thử nghiệm router

Để kiểm tra hoạt động của router vừa xây dựng, tạo thêm một project nữa trong solution và đặt tên là SampleApp (thuộc template ConsoleApp .NET framework). Đặt SampleApp làm startup project. Tham chiếu SampleApp sang thư viện Framework.

Solution với hai project: thư viện Framework và console app SampleApp (startup project). SampleApp tham chiếu sang thư viện Framework.
Solution với hai project: thư viện Framework và console app SampleApp (startup project). SampleApp tham chiếu sang thư viện Framework.

Viết code cho Program.cs như sau:

using System;

namespace BookMan.ConsoleApp
{
    using Framework;
    internal class Program
    {
        private static void Main()
        {
            Console.Title = "Sample App";
            Console.OutputEncoding = System.Text.Encoding.UTF8;

            Router.Instance.Register("about", About);
            Router.Instance.Register("help", Help);
            Router.Instance.Register("action", SomeAction, "action ? param = <a string value here>");

            while (true)
            {
                Console.Write("# Request >>> ");
                string request = Console.ReadLine();
                try
                {
                    Router.Instance.Forward(request);
                }
                catch(Exception e)
                {
                    Console.WriteLine($">>> ERROR: {e.Message}");
                }

                Console.WriteLine();
            }
        }

        private static void SomeAction(Parameter p)
        {
            Console.WriteLine($"You've entered the string: {p["param"]}");
        }

        private static void About(Parameter p)
        {
            Console.WriteLine("This is the about section");
        }

        private static void Help(Parameter p)
        {
            if (p == null)
            {
                Console.WriteLine("SUPPORTED COMMANDS:");
                Console.WriteLine(Router.Instance.GetRoutes());
                Console.WriteLine("type: help ? cmd= <command> to get command details");
                return;
            }
            Console.BackgroundColor = ConsoleColor.DarkBlue;
            var command = p["cmd"].ToLower();
            Console.WriteLine(Router.Instance.GetHelp(command));
            Console.ResetColor();
        }
    }
}

Kết quả chạy thử nghiệm

Giờ có thể đăng ký bất kỳ lệnh gì với Router. Không còn switch-case và if nữa.
Giờ có thể đăng ký bất kỳ lệnh gì với Router. Không còn switch-case và if nữa.

Chúng ta có thể thấy, Router giúp đơn giản hóa rất nhiều thủ tục thêm mới phương thức và lệnh tương ứng. Việc đăng ký lệnh mới thực hiện tập trung giúp dễ quản lý code hơn. Chúng ta cũng rất dễ dàng nhận chuỗi tham số từ lệnh người dùng.

Ở bài sau chúng ta tiếp tục xây dựng nhóm lớp view.

Mã nguồn cung cấp ở bài cuối cùng


Loạt bài Xây dựng thư viện hỗ trợ ứng dụng Console:
Phần 1 – Giới thiệu chung
Phần 2 – Truy vấn, xây dựng router
Phần 3 – Hỗ trợ I/O với console, view, controller
Phần 4 – Ví dụ minh họa

Bình luận

avatar
  Đăng ký theo dõi  
Thông báo về