Search This Blog

Showing posts with label Design Patterns. Show all posts
Showing posts with label Design Patterns. Show all posts

Sunday, July 10, 2016

Design Patterns trong các dự án thực tế - Template Method Pattern

Lời nói đầu

Trước khi tìm hiểu về Template Method Pattern, hãy đọc lại bài viết Design patterns là gì nếu bạn vẫn chưa biết design patterns là gì. Ngoài ra để giúp bạn dễ dàng nắm bắt nội dung bài viết, hãy chắc là bạn đã biết về Lập trình hướng đối tượng (OOP) và các tính chất, khái niệm của nó như bao đóng (encapsulation), trừu tượng (abstract), kế thừa (inheritance), đa hình (polymorphism), đối tượng (object), lớp (class), giao diện (interface)...
***
Template Method Pattern là gì?

Template Method Pattern là một trong các pattern thuộc nhóm behavioural pattern, nó định nghĩa ra một khung sườn hay một template, là các thao tác cần phải xử lí của một chức năng nào đó. Các thao tác này có thể được các class con kế thừa và xử lí lại mà không làm thay đổi, ảnh hưởng đến cấu trúc bên trong chức năng đó.

Áp dụng Template Method Pattern

Template Method Pattern được sử dụng khá nhiều trong mô hình Abstract class (cha) - Concrete Class (con) khi chúng ta muốn các Concrete class tự thực thi xử lí theo cách của nó, nhưng đồng thời vẫn đảm bảo tuận theo những ràng buộc nhất định từ Abstract class, ví dụ như ràng buộc về thứ tự các bước, hay ràng buộc về đầu vào, đầu ra...

Trong pattern này, Abstract class định nghĩa ra một template method để thực hiện một chức năng nào đó. Template method này sẽ gọi đến các method khác bên trong Abstract class để tạo dựng nên bộ khung. Nhưng có thể các method đó sẽ không được thực thi bên trong Abstract class, mà sẽ được override và thực thi lại bên trong các Concrete class.

Hãy xem hình vẽ ví dụ bên dưới

Chúng ta có một Abstract class Sport bao gồm một Template method là Play(). Bên trong Template method này sẽ bao gồm 3 method là Initialize() dùng để thiết lập các giá trị ban đầu, Start() để bắt đầu và End() để kết thúc. Chúng ta cũng có 2 Concrete class Football và BasketBall kế thừa từ Abstract class Sport và override 3 method Initialize(), Start() và End() để thực thi lại. Lúc này Template method Play() vẫn giữ được cấu trúc ban đầu nhưng đã được thực thi khác nhau tùy thuộc vào Concrete class.

Trong dự án của chúng ta, các bạn có thể đã gặp trường hợp cùng một chức năng, cùng một luồng xử lí nhưng từng class lại có những method đặt tên khác nhau, cách xử lí khác nhau, việc truy xuất dữ liệu khác nhau..v.v. Điều này gây trở ngại trong việc kiểm tra cũng như review hệ thống. Việc tuân theo những quy tắc, luồng (flow) nhất định khá là quan trọng, giúp cho việc kiểm tra, review trở nên thuận lợi hơn, đảm bảo luồng xử lí vận hành trơn tru, đồng nhất và không xảy ra ngoại lệ sanh lỗi.

Lấy ví dụ chúng ta có các Form nhập liệu. Mỗi khi mở Form lên thì việc chúng ta cần phải làm là:

- Khởi tạo giao diện theo design (InitializeComponent)

- Khởi tạo giao diện theo yêu cầu

- Phân quyền chức năng

- Hiển thị dữ liệu

.v.v

Các thao tác này hầu như các Form bắt buộc phải trải qua khi mở Form lên. Nếu thiếu một trong các thao tác này thì có thể dẫn đến một vài bug nào đó không mong muốn, chính vì thế ta nên làm sao để tất cả các Form đều phải trải qua tuần tự các bước như trên. Một điểm nữa là rõ ràng không có Form nào giống Form nào 100% các thao tác, có thể giao diện khác nhau, dữ liệu khác nhau..., vì vậy chúng ta cũng cần phải thiết lập các thao tác làm sao cho các Form có thể tùy biến lại.

Khi đã xác định được yêu cầu, thì việc tiếp theo là chúng ta phải tạo ra được một Abstract class, mà theo ví dụ ở đây là một BaseForm.


BaseForm của chúng ta sẽ có một Template method LoadForm, bên trong sẽ bao gồm các method khác dùng để thiết lập thông số, controls, dữ liệu...Chúng ta cũng có 2 ConcreteForm kế thừa thực thi lại các method.

Dưới đây là minh họa cho ví dụ của chúng ta

ĐẦU TIÊN tạo một BaseForm như hình vẽ. BaseForm của chúng ta gồm một GroupBox dùng để chứa các control. Hai button Save và Close dùng để lưu dữ liệu và đóng form.

BaseForm của chúng ta xử lí cho chế độ Add, Edit và View nên chúng ta cũng tạo một enum Mode
   public enum Mode  
   {  
     View,  
     Add,  
     Edit,  
   }  

View Code (F7) để cập nhật code-behind của BaseForm
   public partial class BaseForm : Form  
   {  
     public Mode Mode { get; set; }  

     public BaseForm()  
     {  
       InitializeComponent();  
       this.Load += BaseForm_Load;  
     }  

     /// <summary>  
     /// Form_Load event  
     /// </summary>  
     void BaseForm_Load(object sender, EventArgs e)  
     {  
       //invoke template method  
       this.LoadForm();  
     }  

     /// <summary>  
     /// Template method  
     /// </summary>  
     public void LoadForm()  
     {  
       this.InitControl();  
       this.SetControl();  
       this.SetAuthority();  
       this.SetData();  
     }  

     /// <summary>  
     /// Initialize control  
     /// </summary>  
     public virtual void InitControl()   
     {  
       this.AcceptButton = this.btnSave;  
       this.CancelButton = this.btnClose;  
       this.btnSave.Click += btnSave_Click;  
       this.btnClose.Click += btnClose_Click;  
     }  

     /// <summary>  
     /// Set control's status  
     /// </summary>  
     public virtual void SetControl() { } 
 
     /// <summary>  
     /// Set control's data
     /// </summary>  
     public virtual void SetData() { }  

     /// <summary>  
     /// Check authority and set button's status  
     /// </summary>  
     public virtual void SetAuthority()  
     {  
       this.btnSave.Enabled = HasAuthority("Search");  
     }  

     /// <summary>  
     /// Check authoriry  
     /// </summary>  
     private bool HasAuthority(string authority)  
     {  
       return true;  
     }
   }  

Đây là Template method LoadForm của chúng ta
     /// <summary>  
     /// Template method  
     /// </summary>  
     public void FormLoad()  
     {  
       this.InitControl();  
       this.SetControl();  
       this.SetAuthority();  
       this.SetData();  
     }  

và sẽ được gọi trong sự kiện Form_Load.
     /// <summary>  
     /// Form_Load event  
     /// </summary>  
     void BaseForm_Load(object sender, EventArgs e)  
     {  
       //invoke template method  
       this.FormLoad();  
     }  

Các method bên trong cần được khai báo hoặc là abstract hoặc là virtual. Mục đích là để các Concrete class có thể override và xử lí lại nếu cần thiết. Trong ví dụ này vì BaseForm không thiết lập abstract nên Kiên để tất cả method đều là virtual, nhưng thông thường với một số method mà chỉ có thể xử lí ở Concrete class thì nên được khai báo là abstract, còn nếu method có thể vừa có xử lí ở base class, vừa có xử lí ở concrete class thì nên khai báo là virtual.

     /// <summary>  
     /// Initialize control  
     /// </summary>  
     public virtual void InitControl()   
     {  
       this.AcceptButton = this.btnSave;  
       this.CancelButton = this.btnClose;  
       this.btnSave.Click += btnSave_Click;  
       this.btnClose.Click += btnClose_Click;  
     }  

     /// <summary>  
     /// Set control's status  
     /// </summary>  
     public virtual void SetControl() { } 
 
     /// <summary>  
     /// Set control's data
     /// </summary>  
     public virtual void SetData() { }  

     /// <summary>  
     /// Check authority and set button's status  
     /// </summary>  
     public virtual void SetAuthority()  
     {  
       this.btnSave.Enabled = HasAuthority("Add");  
     }  

Chúng ta có 2 method xử lí hoàn toàn ở Concrete Form:
- SetControl(): dùng để ẩn/hiện các input control tùy theo Mode (Add, Edit, View)
- SetData(): dùng để thiết lập dữ liệu ban đầu cho các input control.
Ngoài ra có 2 method vừa xử lí ở BaseForm, vừa xử lí ở Concrete Form:
- InitControl(): dùng để thiết lập, khởi tạo các giá trị ban đầu cho Form.
- SetAuthority(): dùng để kiểm tra quyền để ẩn/hiện các button.

TIẾP THEO chúng ta tạo ConcreteForm1 kế thừa từ BaseForm này và chỉ design thêm 2 bộ control Employee Name như hình vẽ. Hãy nhớ thiết lập modifier cho các control trên BaseForm là Protected.



View Code (F7) và bổ sung đoạn code bên dưới
 namespace TemplateMethodPattern  
 {  
   public partial class ConcreteForm1 : BaseForm  
   {  
     public ConcreteForm1()  
     {  
       InitializeComponent();  
       base.Mode = TemplateMethodPattern.Mode.Add;  
     }  

     /// <summary>  
     /// Implement BaseForm's InitControl method  
     /// </summary>  
     public override void InitControl()  
     {  
       this.txtEmpVN.TabIndex = 1;  
       this.txtEmpEN.TabIndex = 2;  
       this.txtEmpVN.Focus();  
     }  

     /// <summary>  
     /// Implement BaseForm's SetControl method  
     /// </summary>  
     public override void SetControl()  
     {  
       if (base.Mode == TemplateMethodPattern.Mode.View)  
       {  
         this.txtEmpEN.Enabled = false;  
         this.txtEmpVN.Enabled = false;  
       }  
       else  
       {  
         this.txtEmpEN.Enabled = true;  
         this.txtEmpVN.Enabled = true;  
       }  
     }  

     /// <summary>  
     /// Implement BaseForm's SetData method  
     /// </summary>  
     public override void SetData()  
     {  
       if (base.Mode == TemplateMethodPattern.Mode.Add)  
       {  
         this.txtEmpVN.Text = string.Empty;  
         this.txtEmpEN.Text = string.Empty;  
       }  
       else  
       {  
         this.txtEmpVN.Text = "kien.chu";  
         this.txtEmpEN.Text = "ken";  
       }  
     }  
   }  
 }  

Lúc này trong ConcreteForm1 chúng ta chỉ cần override các method đã khai báo là virtual trên BaseForm về và xử lí theo cách chúng ta muốn mà không cần quan tâm đến các bước thực hiện tuần tự thế nào, nơi nào sử dụng các method này.v.v..Khi chúng ta Run chương trình từ ConcreteForm1, nó vẫn sẽ xác định được event nào cần gọi, các bước sẽ được xử lí ra sao...

Trên đây là giới thiệu về Template Method Pattern và một vài ví dụ đơn giản về nó. Nếu các bạn có gì trao đổi hay thắc mắc thì có thể để lại comment bên dưới bài viết này.
Share to be shared!

Bài trước - Facade Pattern


Design Patterns trong các dự án thực tế - Facade Pattern

Lời nói đầu

Trước khi tìm hiểu về Facade Pattern, hãy đọc lại bài viết Design patterns là gì nếu bạn vẫn chưa biết design patterns là gì. Ngoài ra để giúp bạn dễ dàng nắm bắt nội dung bài viết, hãy chắc là bạn đã biết về Lập trình hướng đối tượng (OOP) và các tính chất, khái niệm của nó như bao đóng (encapsulation), trừu tượng (abstract), kế thừa (inheritance), đa hình (polymorphism), đối tượng (object), lớp (class), giao diện (interface)...
***
Facade Pattern là gì?

Facade Pattern là một trong các pattern quan trọng nhất và hay nhất trong thiết kế phần mềm, thuộc nhóm structural pattern. Nó đóng vai trò che dấu đi tất cả những sự phức tạp, sự lằng nhằng của một chức năng nào đó trong hệ thống và cung cấp một giao diện, một class với một cách thức sử dụng đơn giản và hiệu quả hơn rất nhiều.

Lấy ví dụ: mỗi buổi sáng bạn đi làm và bạn muốn phải tắt hết các thiết bị điện vì dạo này giá điện cao quá. Bạn sẽ làm gì? Bạn phải kiểm tra và tắt quạt, tắt tivi, tắt đèn..., rồi lỡ may quên tắt máy lạnh thì cuối tháng lãnh đủ. 
Những việc cần làm mỗi lần rời nhà được minh họa dưới đây
 public LeaveHome  
 {  
    Televison.TurnOff();  
    AirConditioner.TurnOff();  
    Light.TurnOff();  
 }  

Bạn có muốn giải quyết nhanh và đơn giản hơn không? Hãy cúp cầu dao điện. Tất cả chỉ trong một nốt nhạc và mọi vấn đề được giải quyết, bạn cũng không cần phải quan tâm đến cách nó hoạt động thế nào (phải che dấu vì nguy hiểm mà). Đó là cách mà Facade Pattern hoạt động. 
  public LeaveHome   
  {   
    CircuitBreaker.TurnOff();    
  }   

  /// <summary>  
  /// CircuitBreaker Facade  
  /// </summary>  
  public CircuitBreaker   
  {   
    /// <summary>  
    /// Facade method  
    /// </summary>  
    public static void TurnOff()   
    {   
      Televison.TurnOff();   
      AirConditioner.TurnOff();   
      Light.TurnOff();     
    }   
  }   

Áp dụng Facade Pattern

Trong thiết kế phần mềm cũng vậy, đặc biệt là đối với những bạn giữ vai trò key member trong dự án hoặc giả sử là bạn muốn trở thành như vậy thì việc đưa ra những cách xử lí đơn giản, tiết kiệm chi phí (thời gian, công sức) cho hệ thống nhưng vẫn đảm bảo luồng xử lí vẫn hoạt động tốt là rất quan trọng.

Kiên lấy ví dụ trong thực tế trong dự án khi bạn phát triển một màn hình, trong đó có một combobox hiển thị danh sách nhân viên công ty, bạn sẽ phải làm gì? 

- Viết câu truy vấn dữ liệu.

- Kiểm tra xem có cần thêm bớt gì dữ liệu hay không (thêm điều kiện, sắp xếp hay thêm dòng trống.v.v..)

- Format lại dữ liệu theo ngôn ngữ cần hiển thị (nếu có)

- Rồi đổ lên control

..bla..bla, 

Kiên đoán là ai cũng có thể làm việc này và có thể bạn chỉ mất chừng 10 - 15 phút để hoàn thành. Nhưng hãy cùng nhau suy nghĩ rộng ra thêm một chút các vấn đề sau để thấy vì sao Facade Pattern lại quan trọng:

1. Trùng lặp code. Hãy tưởng tượng cũng có 20 chức năng khác cũng muốn hiển thị danh sách nhân viên trong combobox, vậy là bạn có 20 đoạn code lằng nhằng lặp đi lặp lại chỉ để xử lí cùng một vấn đề. Đây là một code smell mà bạn cần tránh.

2. Tốn chi phí. 20 chức năng x 15 phút = 300 phút. Đương nhiên khi bạn copy & paste thì sẽ nhanh hơn. Nhưng chắc chắn sẽ không nhanh môt Facade mất 10' hoàn thành và 5' để tạo ra cách thức truy cập. Chưa kể bạn phải mất thời gian để giải thích về chức năng đó cho developer phải lấy dữ liệu từ table nào, đi qua layer nào, xử lí ra sao...Hay nếu sau này có thay đổi thì cũng làm cho bạn mất khá nhiều thời gian.

3. Thiếu sót chức năng. Bạn muốn combobox của bạn có thêm dòng trống để select all, hay bạn muốn dữ liệu hiển thị đúng theo ngôn ngữ, hoặc là yêu cầu muốn sắp xếp theo column nào đó. Nếu team của bạn hiểu rõ yêu cầu, giao tiếp tốt với nhau thì ok, còn không bạn đã có bug rồi đấy.

4. Khó review. Mỗi người một style, một phong cách code khác nhau. Thiếu comment hay comment không hiểu được. Vi phạm coding standard của dự án.v.v..Bạn chọn vẫn kiên nhẫn review hết hay bỏ qua không review?

5. Gặp vấn đề khi có thay đổi. Ok bạn đã hoàn thành 20 chức năng với 100% yêu cầu và sự hài lòng của khách hàng. Đột nhiên vào một ngày đẹp trời, khách hàng nói rằng :"Tôi không cần hiển thị theo ngôn ngữ nữa, chỉ hiển thị tên tiếng Anh thôi". Bạn có nhớ hết tất cả các màn hình sử dụng combobox này ko? Bạn có nhớ hết class nào viết chức năng này không?

Viết có một chức năng bé tí tẹo mà suy nghĩ xa xôi, phức tạp nhỉ. Vâng đúng rồi đấy, Design pattern được đưa ra dựa theo kinh nghiệm, là kĩ năng giải quyết vấn đề mà ở trường không có dạy cho chúng ta.

Vậy ta giải quyết thế nào trong trường hợp này? Thật ra bản thân Facade Pattern khá là đơn giản, hãy xem lại bài toán cúp cầu dao, bạn chỉ cần gom tất cả những việc cần làm vào một nơi, đồng thời cung cấp một lời gọi, có thể là một method hay một property để xử lí chúng. Dưới đây là các Class Diagram mà Kiên minh họa đơn giản cho trước và sau khi áp dụng Facade Pattern cho chức năng hiển thị danh sách employee lên combobox này:

Giả sử trước khi áp dụng Facade Pattern, chúng ta có 3 Screen sử dụng combobox để hiển thị danh sách nhân viên. Để làm được điều đó, chúng ta viết 3 method SelectData() ở mỗi Screen để gọi đến class EmployeeService thực thi câu truy vấn. Trong EmployeeService có 3 method xử lí cùng một yêu cầu nhưng được viết riêng cho 3 Screen đó. Rồi có Screen lại viết thiếu chức năng như Screen1 thiếu chức năng thêm một dòng trống để select all, Screen2 lại thiếu chức năng hiển thị dữ liệu theo ngôn ngữ. Có khá là nhiều vấn đề nhỉ.


Áp dụng Facade Pattern, chúng ta làm theo nguyên tắc tạo ra một Facade để gom tất cả xử lí vào đó. Facade của chúng ta là một custom control EmployeeCombobox kế thừa trực tiếp từ System.Windows.Forms.Combobox đồng thời cung cấp một facade method FillData(). Các method khác của EmployeeCombobox như ChangeLanguage() và SelectData() đã được che dấu đi, các Screen không cần quan tâm đến nữa. Lúc này EmployeeService cũng chỉ còn duy nhất một method dùng chung là SelectData(). Hãy nhìn các màn hình, lúc này nó không còn những xử lí rườm rà nữa, mà chỉ cần gọi đến facade method FillData() của EmployeeCombobox để hiển thị danh sách nhân viên.

Dưới đây là phần Kiên minh họa cho ví dụ này.

Đầu tiên: ta tạo một custom control EmployeeCombobox là một Component class


Sau đó cho kế thừa từ System.Windows.Forms.Combobox và bổ sung code như bên dưới
 namespace FacadePattern  
 {  
   /// <summary>  
   /// Custom control  
   /// </summary>  
   public partial class EmployeeCombobox : System.Windows.Forms.ComboBox  
   {  
     /// <summary>  
     /// Allow add null row or not  
     /// </summary>  
     public bool HasNull { get; set; }  

     /// <summary>  
     /// Facade method  
     /// </summary>  
     public void FillData()  
     {  
       //Select data  
       this.SelectData();  

       //Add null row  
       this.AddNewRow();  

       //Change language  
       this.ChangeLanguage();  
     }  

     /// <summary>  
     /// Select data from database  
     /// and set to DataSource  
     /// </summary>  
     private void SelectData()  
     {  
       EmployeeService employeeService = new EmployeeService();  
       base.DataSource = employeeService.SelectData();  
     }  

     /// <summary>  
     /// Display value based on language  
     /// </summary>  
     private void ChangeLanguage()  
     {  
       base.ValueMember = "ID";  
       switch (UserSession.Language)  
       {  
         case "en":  
           base.DisplayMember = "NameEN";  
           break;  
         case "vi":  
           base.DisplayMember = "NameVN";  
           break;  
         default:  
           base.DisplayMember = "NameEN";  
           break;  
       }  
     }  

     /// <summary>  
     /// Add null row  
     /// </summary>  
     private void AddNewRow()  
     {  
       if (this.HasNull)  
       {  
         DataRow row = ((DataTable)this.DataSource).NewRow();  
         ((DataTable)this.DataSource).Rows.InsertAt(row, 0);  
       }  
     }  
   }  
 }  

Ta có thể tạo các thuộc tính để bên ngoài truy cập và cấu hình các thông số cần thiết như là cho phép có dòng trống, sort theo column nào, sort giảm hay tăng, hay là các điều kiện tìm kiếm...
     /// <summary>  
     /// Allow to add null row or not  
     /// </summary>  
     public bool HasNull { get; set; }  


Đây là Facade method FillData() mà chúng ta tạo ra để cung cấp giao diện thay thế cho các xử lí bên trong.
     /// <summary>  
     /// Facade method  
     /// </summary>  
     public void FillData()  
     {  
       //Select data  
       this.SelectData();  

       //Add null row  
       this.AddNewRow();  

       //Change language  
       this.ChangeLanguage();
     }

Các xử lí thông thường mà chúng ta cần phải làm đã được che dấu đi (private method) 
     /// <summary>  
     /// Select data from database  
     /// and set to DataSource  
     /// </summary>  
     private void SelectData()  
     {  
       EmployeeService employeeService = new EmployeeService();  
       base.DataSource = employeeService.SelectData();  
     }  

     /// <summary>  
     /// Display value based on language  
     /// </summary>  
     private void ChangeLanguage()  
     {  
       base.ValueMember = "ID";  
       switch (UserSession.Language)  
       {  
         case "en":  
           base.DisplayMember = "NameEN";  
           break;  
         case "vi":  
           base.DisplayMember = "NameVN";  
           break;  
         default:  
           base.DisplayMember = "NameEN";  
           break;  
       }  
     }  

     /// <summary>  
     /// Add null row  
     /// </summary>  
     private void AddNewRow()  
     {  
       if (this.HasNull)  
       {  
         DataRow row = ((DataTable)base.DataSource).NewRow();  
         ((DataTable)base.DataSource).Rows.InsertAt(row, 0);  
       }  
     }  
   }  

Tiếp theo Kiên tạo EmployeeService đơn giản cho ví dụ này, nó cung cấp method SelectData() và trả về danh sách Employee sẽ được gọi từ custom control EmployeeCombobox. Bạn có thể tạo DAO hay Service, viết SqlQuery, Store Procedure hay LINQ đều được.
   public class EmployeeService  
   {  
     public System.Data.DataTable SelectData()  
     {  
       DataTable dtResult = new DataTable();  
       using (FacadePatternEntities context = new FacadePatternEntities())  
       {  
         dtResult = context.Employees  
           .Select(emp => new  
           {  
             emp.ID,  
             emp.NameEN,  
             emp.NameVN,  
           })  
           .ToDataTable();  
       }  
       return dtResult;  
     }  
   }  

Cuối cùng ta tạo Screen1 là một WindowForm để kiểm tra chức năng. Chúng ta cũng thấy custom control EmployeeCombobox sẽ được tạo ra trong Toolbox sau khi chúng ta build chương trình. Hãy kéo nó vào và thiết lập các thông số cần thiết như hình bên dưới.

View Code (F7) và bổ sung các đoạn code sau đây

 namespace FacadePattern  
 {  
   public partial class Screen1 : Form  
   {  
     public Screen1()  
     {  
       InitializeComponent();  
       UserSession.Language = "vi";  
       this.Load += Screen1_Load;  
     }  

     void Screen1_Load(object sender, EventArgs e)  
     {  
       //Init properties  
       this.cboEmployee.HasNull = true;

       //Invoke facade method  
       this.cboEmployee.FillData();  
     }  
   }  
 }  


Chúng ta cấu hình các thông số cần thiết đã tạo ra Facade
//Init properties  
this.cboEmployee.HasNull = true;

Sau này khi cần đổ dữ liệu cho combobox cho bất kì màn hình nào, chúng ta chỉ việc thông qua Facade method FillData() để xử lí mà không cần quan tâm cách nó xử lí như thế nào nữa.
//Invoke facade method  
this.cboEmployee.FillData();

Trên đây là giới thiệu và ví dụ về Facade Pattern - một trong những Pattern được sử dụng nhiều nhất trong thiết kế phần mềm. Nếu các bạn muốn trao đổi hay có thắc mắc với Kiên, có thể để lại comment bên dưới bài viết này.

Share to be shared! 

Bái trước - Adapter Pattern


Wednesday, July 6, 2016

Design Patterns trong các dự án thực tế - Adapter Pattern

Lời nói đầu

Trước khi tìm hiểu về Adapter Pattern, hãy đọc lại bài viết Design patterns là gì nếu bạn vẫn chưa biết design patterns là gì. Ngoài ra để giúp bạn dễ dàng nắm bắt nội dung bài viết, hãy chắc là bạn đã biết về Lập trình hướng đối tượng (OOP) và các tính chất, khái niệm của nó như bao đóng (encapsulation), trừu tượng (abstract), kế thừa (inheritance), đa hình (polymorphism), đối tượng (object), lớp (class), giao diện (interface)...
***
Adapter Pattern là gì?

Adapter Pattern là một dạng pattern khá đơn giản và phổ biến, nó đóng vai trò như là một bộ chuyển đổi hay là cầu nối giữa các interface không tương thích với nhau, tích hợp các tính năng của chúng và tạo thành một interface có những chức năng mà ta mong muốn.

Kiên lấy ví dụ từ thực tế, dây nguồn máy tính của bạn có 3 chấu (chân) cắm điện. vậy làm sao nếu ổ điện của bạn chỉ có 2 lỗ cắm? Lúc này hoặc là bạn tốn chi phí nhiều hơn để mua ổ điện khác, hoặc là bạn sẽ cần một cục chuyển đổi (Adapter) từ ba chấu thành hai chấu và vấn đề được giải quyết. Hay chắc hẳn các bạn vẫn còn nhớ những cái đầu đọc các thẻ nhớ từ điện thoại vào máy tính. Tất cả đó chính là cách mà Adapter Pattern hoạt động.

Vậy Adapter Pattern sẽ có những thành phần gì? Kiên lấy lại ví dụ dây nguồn máy tính. Kiên muốn máy tính được sạc pin, Kiên sẽ cầm DÂY NGUỒN (Target) để SẠC PIN (Function) vì rõ ràng nó có chức năng đó. Nhưng DÂY NGUỒN (Target) lại không tương thích với Ổ ĐIỆN (Adaptee), vậy thì phải có một BỘ CHUYỂN ĐỔI (Adapter) để mọi thứ vẫn hoạt động tốt rồi.

Khi nào áp dụng Adapter Pattern?

Trong các dự án của chúng ta đôi khi muốn sử dụng các thư viện bên thứ ba (3rd party library) hoặc plugin, dll có sẵn trong các framework vì đa phần là nó khá hữu ích, được xây dựng sẵn, chúng ta chỉ việc tích hợp vào hệ thống và sử dụng ngay lập tức. Nhưng không phải library nào cũng dễ sử dụng, plug and play mà cần bạn phải cấu hình, khai báo và nhiều thứ khác nữa. Đó là khi bạn cần phải custom lại để chúng trở nên dễ sử dụng và phù hợp hơn cho chúng ta.

Hoặc trong một tình huống khác, bạn tích hợp một library hay plugin và sử dụng các chức năng của nó ở rất nhiều nơi trong hệ thống chúng ta. Một thời gian sau, chúng ta muốn nâng cấp phiên bản mới của library hay plugin đó, nhưng một vài chức năng, method của nó đã được bỏ đi hoặc thay đổi. Các bạn hình dung điều gì sẽ xảy ra? Có thể là hàng trăm, hàng ngàn lỗi xảy ra khi chúng ta build solution, và chúng ta phải tốn chi phí để fix lại. Và nếu trong tương lai chúng lại có bất kì sự thay đổi nào khác, ai mà biết được. Đó là lúc bạn nên sử dụng Adapter Pattern để wrap chúng lại, thực thi theo cách chúng ta mong muốn, và nếu có sự thay đổi library hay plugin thì chúng ta cũng có thể nhanh chóng tìm và fix nó lại.

OK xong màn lý thuyết buồn ngủ. Chúng ta thử lấy lại ví dụ ghi log trong bài viết Singleton Pattern. Chúng ta có nhiều library hỗ trợ tốt trong việc ghi log hệ thống, việc của chúng ta là nên tận dụng chúng hơn là "thiết kế lại cái bánh xe". Trong bài viết này Kiên sẽ sử dụng Adapter Pattern thông qua việc tích hợp log4net library để làm hoàn thiện hơn chức năng ghi log của chúng ta.

Để minh họa cho ví dụ này, các bạn có thể sử dụng lại ví dụ trong bài Singleton Pattern hoặc tạo mới project. 

Cách làm của chúng ta nói nôm na cho dễ hiểu sẽ là: chọn library và tích hợp, tạo adapter class, wrap library và biến những chức năng của nó thành của adapter theo cách mà chúng ta muốn.

Bước 1: Chọn log4net library và tích hợp nó vào project của chúng ta.

Để tích hợp log4net, bạn có thể Add References hoặc sử dụng Nuget để tự động làm việc này

Nếu sử dụng Nuget, bạn vào Tool -> Nuget Package Manager -> Package Manager Console

Cửa sổ Package Manager Console sẽ xuất hiện như hình bên dưới. Bạn hãy gõ vào đó dòng lệnh: INSTALL-PACKAGE log4net, sau đó nhấn Enter. Xuất hiện dòng thông báo Successfully added xxx to zzz là bạn đã thêm library thành công.


















Chúng ta kiểm tra lại danh sách References của chúng ta xem đã có log4net chưa.



Việc tiếp theo chúng ta cần làm là cấu hình log4net, biến nó thành Adaptee. Chúng ta cũng sử dụng lại class Logger đã xây dựng ở bài trước đóng vai trò như là một Adapter, một Wrapper class. 

Chúng ta cần cấu hình log4net. Hãy mở file App.config hay Web.config tùy thuộc vào dự án của mình và bổ sung các đoạn code bên dưới.


 <?xml version="1.0" encoding="utf-8" ?>  
 <configuration>  
     <configSections>  
         <!--CONFIGURE SECTION LOG4NET-->  
         <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />  
     </configSections>  
     <startup>  
         <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />  
     </startup>  

     <!--START CONFIGURE LOG4NET-->  
     <log4net debug="true">  
        <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">  
           <file value="${LOCALAPPDATA}\myLog\" />  
           <appendToFile value="true" />  
           <rollingStyle value="Date" />  
           <datePattern value="yyyyMMdd'.log'" />  
           <staticLogFileName value="false" />  
           <filter type="log4net.Filter.LevelRangeFilter">  
              <acceptOnMatch value="true" />  
              <levelMin value="DEBUG" />  
              <levelMax value="FATAL" />  
           </filter>  
           <layout type="log4net.Layout.PatternLayout">  
              <conversionPattern value=" - Level : %level%n - Date: %d%n - Message: %message%n------%n" />  
           </layout>  
       </appender>  
       <root>  
         <level value="ALL" />  
         <appender-ref ref="RollingLogFileAppender" />  
       </root>  
  </log4net>  
  <!--END CONFIGURE LOG4NET-->  
 </configuration>  

Hãy nhìn qua file config của chúng ta. Khai báo một configSection có name và type là log4net

     <configSections>  
         <!--CONFIGURE SECTION LOG4NET-->  
         <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />  
     </configSections>  

Khai báo configSection của chúng ta.
     <!--START CONFIGURE LOG4NET-->  
     <log4net debug="true">  
...
     </log4net>  

Bên trong dấu "..." sẽ có khá nhiều thông số mà dựa vào đó log4net sẽ hỗ trợ chúng ta tận răng. Chúng ta hãy xem một vài thông số bên dưới:

Đường dẫn lưu file log sẽ nằm trong thư mục AppData\MyLog của user hiện tại.
<file value="${LOCALAPPDATA}\myLog\" /> 

Cho phép file ghi thêm thông tin vào. Nếu value = false thì mỗi lần ghi log nó sẽ chép đè thông tin cũ.
<appendToFile value="true" />

Đây là format của mỗi lần ghi thông tin. Đoạn "%message%" chính là thông tin của chúng ta đưa vào. Những chỗ khác là các thông tin thêm được support bởi log4net ví dụ như level hay date
<layout type="log4net.Layout.PatternLayout">  
   <conversionPattern value=" - Level : %level%n - Date: %d%n - Message: %message%n------%n" />  
</layout> 

Để chi tiết hơn các bạn có thể tìm hiểu thêm trên trang chủ của log4net.

Bước 2: tạo Adapter Logger của chúng ta, wrap log4net lại và sử dụng các chức năng của nó.

- Đầu tiên ta tạo một interface ILog chỉ có một method WriteLog cho Adapter Logger và một enum LogLevel gồm các Level mà log4net hỗ trợ.
 namespace AdapterPattern  
 {  
   public enum LogLevel  
   {  
     Error,  
     Info,  
     Debug,  
     Warn,  
     Fatal  
   }  
   public interface ILog  
   {  
     bool WriteLog(string message, LogLevel level);  
   }  
 }  

- Tiếp theo là cập nhật lại class Logger của chúng ta. Các bạn để ý đây cũng có thể được xem như là một quá trình Refactoring, tức là bạn sửa đổi, nâng cấp, cải tiến code nhưng vẫn không ảnh hưởng lời gọi đến nó. Ở đây Kiên vẫn giữ lại hầu hết cấu trúc class Logger mà chúng ta đã xây dựng ở bài viết Singleton Pattern.  
 namespace AdapterPattern  
 {  
   public class Logger : ILog  
   {  
     /// <summary>   
     /// thread-safety variable   
     /// </summary>   
     private static readonly object lockObject = new object();  
     private static Logger instance = null;  

     /// <summary>  
     /// Adaptee  
     /// </summary>  
     private log4net.ILog myLog4net;  

     Logger()  
     {  
       Configure();  
     }  

     /// <summary>   
     /// Single instance   
     /// </summary>   
     public static Logger Instance  
     {  
       get  
       {  
         //implement simple thread-safety   
         lock (lockObject)  
         {  
           if (instance == null)  
           {  
             instance = new Logger();  
           }  
           return instance;  
         }  
       }  
     }  

     /// <summary>   
     /// Configure the log file   
     /// </summary>   
     private void Configure()  
     {  
       log4net.Config.XmlConfigurator.Configure();  
       myLog4net = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);  
     }   

     /// <summary>  
     /// Implement interface's method  
     /// </summary>  
     /// <param name="level"></param>  
     /// <param name="ex"></param>  
     public bool WriteLog(string message, LogLevel level = LogLevel.Debug)  
     {  
       if (myLog4net != null)  
       {  
         switch (level)  
         {  
           case LogLevel.Error:  
             if (myLog4net.IsErrorEnabled)  
               myLog4net.Error(message);  
             break;  
           case LogLevel.Debug:  
             if (myLog4net.IsDebugEnabled)  
               myLog4net.Debug(message);  
             break;  
           case LogLevel.Fatal:  
             if (myLog4net.IsFatalEnabled)  
               myLog4net.Fatal(message);  
             break;  
           case LogLevel.Info:  
             if (myLog4net.IsInfoEnabled)  
               myLog4net.Info(message);  
             break;  
           case LogLevel.Warn:  
             if (myLog4net.IsWarnEnabled)  
               myLog4net.Warn(message);  
             break;  
         }  
         return true;  
       }  
       return false;  
     }  
   }  
 }  

Hãy chú ý những đoạn tô đậm để xem cách chúng ta tích hợp Adaptee log4net vào Adapter Logger.

Chúng ta cho Adapter Logger kế thừa từ interface ILog.
public class Logger : ILog  

Khai báo một biến log4net có kiểu trả về là một interface log4net.ILog. Chúng ta trả về Interface để giảm sự phụ thuộc giữa các class.
     /// <summary>  
     /// Adaptee  
     /// </summary>  
     private log4net.ILog myLog4net;  


Chúng ta gọi các method mà log4net dùng để cấu hình theo các thông số đã được thiết lập trong file App.config

     Logger()  
     {  
       Configure();  
     } 
     /// <summary>   
     /// Configure the log file   
     /// </summary>   
     private void Configure()  
     {  
       log4net.Config.XmlConfigurator.Configure();  
       myLog4net = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);  
     } 

Cuối cùng là ta thực thi method WriteLog được khai báo trong interface ILog và sử dụng các method mà log4net hỗ trợ (myLog4net.Error, myLog4net.Debug...) thay thế cho đoạn code sử dụng Stream trong bài trước.
     /// <summary>  
     /// Implement interface's method  
     /// </summary>  
     /// <param name="level"></param>  
     /// <param name="ex"></param>  
     public bool WriteLog(string message, LogLevel level = LogLevel.Debug)  
     {  
       if (myLog4net != null)  
       {  
         switch (level)  
         {  
           case LogLevel.Error:  
             if (myLog4net.IsErrorEnabled)  
               myLog4net.Error(message);  
             break;  
           case LogLevel.Debug:  
             if (myLog4net.IsDebugEnabled)  
               myLog4net.Debug(message);  
             break;  
           case LogLevel.Fatal:  
             if (myLog4net.IsFatalEnabled)  
               myLog4net.Fatal(message);  
             break;  
           case LogLevel.Info:  
             if (myLog4net.IsInfoEnabled)  
               myLog4net.Info(message);  
             break;  
           case LogLevel.Warn:  
             if (myLog4net.IsWarnEnabled)  
               myLog4net.Warn(message);  
             break;  
         }  
         return true;  
       }  
       return false;  
     }  
   }  

Cuối cùng vẫn giữ nguyên cơ chế gọi đến Logger.

   public partial class Form1 : Form  
   {  
     public Form1()  
     {  
       InitializeComponent();  
       this.Load += Form1_Load;  
       this.FormClosing += Form1_FormClosing;  
     }  
     void Form1_FormClosing(object sender, FormClosingEventArgs e)  
     {  
       Logger.Instance.WriteLog("Form1_FormClosing");  
     }  
     void Form1_Load(object sender, EventArgs e)  
     {  
       Logger.Instance.WriteLog("Form1_Load");  
     }  
   }  

Và đây là nội dung file Log được ghi ra:


Vậy là chúng ta kết thúc bài viết về Adapter Pattern. Nếu có thắc mắc hay trao đổi gì, các bạn có thể comment bên dưới bài viết này.

Share to be shared!

Bài trước - Singleton Pattern