Các mẫu Design pattern thông dụng

Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu mọi thứ về Design Pattern: định nghĩa, bối cảnh sử dụng design pattern… Mỗi Design Patter sẽ có code minh họa.

Design Pattern là một  kỹ thuật trong lập trình hướng đối tượng. Nó cung cấp cho bạn các “mẫu thiết kế” để giải quyết một vấn đề hay gặp trong thiết kế phần mềm. Các vấn đề mà bạn gặp phải trong quá trình thiết kế phần mềm, có thể bạn đã có giải pháp giải quyết. Tuy nhiên, giải pháp của bạn có thể chưa tối ưu, hoặc nó chưa được trừa tượng hóa để bạn có thể tái sử dụng sau này.

Design Pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, giúp thiết kế phần mềm linh hoạt, dễ dàng thay đổi và bảo trì hơn.

Tại sao phải sử dụng Design Pattern?

Mỗi người sẽ tự có câu trả lời riêng khi nói đến lý do sử dụng design pattern trong mã nguồn của họ. Tuy nhiên, có thể tóm gọn lại bằng 2 lý do chính:

  • Cung cấp một thuật ngữ tiêu chuẩn để mọi người đều hiểu khi đọc mã nguồn.
  • Tránh những sai lầm bị lặp đi lặp lại. Dự án có nhiều người làm, có thể nhiều người đều cùng gặp một vấn đề giống nhau nhưng lại có các cách giải quyết khác nhau. Design Pattern sẽ thống nhất phương án giải quyết và nó là cách giải quyết tối ưu nhất.

Phân loại Design Pattern

Theo quan điểm của các tác giả cuốn sách Design Patterns – Elements of Reusable Object-Oriented Software, design pattern chủ yếu  được dựa theo những nguyên tắc của lập trình hướng đối tượng. Hiện nay, có nhiều loại design pattern và được chia làm 3 dạng chính.

  • Creational Patterns: Các pattern giúp giải quyết các vấn đề liên quan tới khởi tạo đối tượng.
  • Structural Patterns: Gồm các patterns giải quyết cho việc xử lý các thành phần của đối tượng. Nó trả lời cho các câu hỏi như: Class đó chứa gì bên trong? Mối liên hệ giữa các class là gì?
  • Behavioral Patterns: Gồm các pattern giải quyết các vấn đề liên quan tới hành vi của các đối tượng, chính xác hơn là sự tương tác giữa các đối tượng.

5 design pattern mà bạn nên biết

Hiện tại có rất nhiều design patterns, do vậy để bạn có thể nắm vững được hết tất cả là một thử thách khó khăn. Thông thường, trong quá trình làm việc, khi có vấn đề phát sinh, bạn đi tìm giải pháp. Bạn nên tìm xem đã có design pattern nào giải quyết vấn đề của mình rồi hay chưa? Khi đó, bạn sẽ dần dần vừa thực hành vừa tìm hiểu design pattern đó.

Tuy nhiên, để có kiến thức nền tảng thì dưới đây là 5 design pattern mà bạn nên tìm hiểu trước.

1. Singleton Pattern

Singleton pattern có lẽ là pattern phổ biến nhất và cũng dễ dùng nhất. Đúng như tên gọi, nó chỉ cho phép tạo duy nhất một instance của đối tượng ở bất kỳ thời điểm nào.

Ví dụ ở thế giới thực đó chính là vị trị tổng thống, mỗi quốc gia chỉ cho phép duy nhất 1 người làm tổng thống mà thôi.

Có một vài điều cần lưu ý khi tạo singleton class:

  • Hàm constructor cần phải đặt private hoặc protected. Mục đích để ngăn các đối tượng khác tạo instance của singleton class qua từ khóa new.
  • Nhược điểm chính của Singleton pattern đó chính khó thực hiện Unit Test. Do đấy, hãy cân nhắc kỹ khi nào cần dùng đến singleton pattern.
  • Một số Java framework như Spring, các đối tượng được quản lý (gọi là bean), mặc định chúng là singleton.
  • Trong J2EE 7, bạn có thể sử dụng annotation @Singleton để tạo Singleton class.

Ví dụ cách triển khai Singleton pattern.

class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
public class ClassSingleton {
    public static void main(String[] args) {
        System.out.println("--- Singleton Pattern ---");
        Singleton single1 = Singleton.getInstance();
        Singleton single2 = Singleton.getInstance();
        if (single1.equals(single2)) {
            System.out.println("Unique Instance");
        }
    }
}

2. Prototype Pattern

Để có 1 cái nhìn cơ bản về prototype pattern, bạn xem ví dụ như sau:

Giả sử bạn đang thiết kế chương trình vẽ (vẽ các đối tượng hình học cơ bản: hình vuông, hình chữ nhật, …). Tất cả các đối tượng này đều có chung một thiết lập ban đầu (màu sắc, tọa độ,…).

Thay vì phải lặp đi lặp lại việc tạo các đối tượng, chúng ta có thể:

  • Tạo riêng một đối tượng chứa tất cả các thiết lập ban đầu.
  • Clone đối tượng đó thay cho việc tạo mới bằng toán tử new.

Đối tượng mà chứa các thiết lập ban đầu được gọi là prototype, và cách thiết kế code kiểu này gọi là prototype pattern.

Tóm lại: Về bản chất, prototype pattern là thiết kế cho phép khởi tạo một đối tượng bằng cách sao chép từ một đối tượng khác đã tồn tại thay vì sử dụng toán tử new.

Đối tượng mới là một bản sao có thể giống hoàn toàn hoặc được biến đổi một vài thuộc tính so với đối tượng gốc. Đặc biệt, bạn có thể thoải mái thay đổi dữ liệu trên đối tượng bản sao mà không làm ảnh hưởng đến đối tượng gốc. Nếu bạn chỉ copy đối tượng theo cách thông thường thì với ngôn ngữ lập trình như Java chẳng hạn, khi bạn thay đổi dữ liệu ở đối tượng bản sao thì bản gốc cũng bị thay đổi theo. Vì bạn mới chỉ copy địa chỉ tham chiếu mà thôi.

Ví dụ cách triển khai Singleton pattern.

Tạo 1 abstract class implementing Clonable interface.

Shape.java

public abstract class Shape implements Cloneable {
   
    private String id;
   protected String type;
    
    abstract void draw();
   
   public String getType(){
        return type;
   }
    
    public String getId() {
      return id;
   }
   
    public void setId(String id) {
       this.id = id;
    }
   
    public Object clone() {
      Object clone = null;
    
      try {
         clone = super.clone();
        
     } catch (CloneNotSupportedException e) {
       e.printStackTrace();
   }
      
      return clone;
    }
}

Tạo concrete classes extending của class trên.

Rectangle.java

public class Rectangle extends Shape {

public Rectangle(){
  type = "Rectangle";
}

@Override
public void draw() {
   System.out.println("Inside Rectangle::draw() method.");
}
}

Square.java

public class Square extends Shape {

   public Square(){
     type = "Square";
   }

   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}

Circle.java

public class Circle extends Shape {

   public Circle(){
     type = "Circle";
   }

   @Override
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}

Tạo một lớp để có được các lớp cụ thể từ cơ sở dữ liệu và lưu trữ chúng trong một Hashtable.

ShapeCache.java

import java.util.Hashtable;

public class ShapeCache {
	
   private static Hashtable shapeMap  = new Hashtable();

   public static Shape getShape(String shapeId) {
      Shape cachedShape = shapeMap.get(shapeId);
      return (Shape) cachedShape.clone();
   }

   // for each shape run database query and create shape
   // shapeMap.put(shapeKey, shape);
   // for example, we are adding three shapes
   
   public static void loadCache() {
      Circle circle = new Circle();
      circle.setId("1");
      shapeMap.put(circle.getId(),circle);

      Square square = new Square();
      square.setId("2");
      shapeMap.put(square.getId(),square);

      Rectangle rectangle = new Rectangle();
      rectangle.setId("3");
      shapeMap.put(rectangle.getId(), rectangle);
   }
}

PrototypePatternDemo sử dụng lớp ShapeCache để nhận các bản sao của các hình dạng được lưu giữ trong một Hashtable

PrototypePatternDemo.java

public class PrototypePatternDemo {
   public static void main(String[] args) {
      ShapeCache.loadCache();

      Shape clonedShape = (Shape) ShapeCache.getShape("1");
      System.out.println("Shape : " + clonedShape.getType());		

      Shape clonedShape2 = (Shape) ShapeCache.getShape("2");
      System.out.println("Shape : " + clonedShape2.getType());		

      Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
      System.out.println("Shape : " + clonedShape3.getType());		
   }
}

3. Builder Pattern

Chúng ta cùng xem xét ví dụ thực tế sau đây.

Giả sử bạn đi ăn tối với bạn gái tại một nhà hàng “sang chảnh”. Nhân viên bồi bàn đưa cho bạn menu với rất nhiều món ăn hấp dẫn, nào là món khai vị, món chính và món tráng miệng. Bạn có thể tùy chọn ăn đủ cả 3 loại món trên,hoặc chỉ 2 trong 3 loại món. Ví dụ, bạn thích ăn luôn món chính rồi tráng miệng sau. Còn người yêu bạn lại chọn ăn từ từ, ăn khai vị cho sang, rồi mới ăn món chính và kết thúc là tráng miệng.

Trong thiết kế phần mềm cũng có nhiều tình huống tương tự như thế xảy ra. Bạn có thể xây dựng một đối tượng bằng cách tập hợp từ các tùy chọn có sẵn. Hoặc bạn cần tạo đối tượng theo nhiều cách khác nhau. Đây là lúc Builder pattern được áp dụng.

Với bạn nào từng lập trình Android sẽ gặp rất nhiều Builder pattern, điển hình nhất chính là lúc tạo dialog trong Android.

Ví dụ cách triển khai Builder Pattern:

public class BuilderPattern {
    static class Coffee {
        private Coffee(Builder builder) {
            this.type = builder.type;
            this.sugar = builder.sugar;
            this.milk = builder.milk;
            this.size = builder.size;
        }
        private String type;
        private boolean sugar;
        private boolean milk;
        private String size;
        public static class Builder {
            private String type;
            private boolean sugar;
            private boolean milk;
            private String size;
            
            public Builder(String type) {
                this.type = type;
            }
            public Builder sugar(boolean value) {
                this.sugar = value;
                return this;
            }
            public Builder milk(boolean value) {
                this.milk = value;
                return this;
            }
            public Builder size(String value) {
                this.size = value;
                return this;
            }
            public Coffee build() {
                return new Coffee(this);
            }
        }
        @Override
        public String toString() {
            return String.format("Coffee [type=%s, sugar=%s, milk=%s, size=%s]", this.type, sugar, milk, size);
        }
    }
    public static void main(String[] args) {
        Coffee coffee = new BuilderPattern.Coffee.Builder("Mocha").milk(true).sugar(false).size("Large").build();
        System.out.println(coffee);
    }
}

Những ưu điểm của Builder Pattern là:

  • Đơn giản hóa việc tạo đối tượng
  • Mã nguồn dễ đọc hơn rất nhiều
  • Không cho phép sửa đổi các giá trị.

4. Proxy Pattern

Trong đời sống thực, bạn cũng gặp rất nhiều vấn đề liên quan tới proxy. Ví dụ như: Thẻ ATM là một proxy cho tài khoản ngân hàng của bạn. Bất cứ khi nào bạn thực hiện giao dịch bằng thẻ ATM, số tiền tương ứng sẽ bị trừ trong tài khoản ngân hàng.

Tương tự trong lập trình phần mềm cũng vậy. Bạn sẽ gặp phải tình huống cần tương tác với đối tượng từ xa (remote). Trong tình huống như vậy, bạn tạo một đối tượng proxy, với mục đích để tương tác với tất cả các nguồn từ bên ngoài. Thay vì bạn phải làm việc trực tiếp với đối tượng ở xa thì bạn làm việc với đối tượng proxy (đã được định nghĩa lại cho phù hợp mục đích sử dụng).

Proxy sẽ che giấu sự phức tạp liên quan tới việc giao tiếp với đối tượng thực.

Mã nguồn ví dụ:

//Image.java
public interface Image {
 
    void showImage();
 
}
//RealImage.java
public class RealImage implements Image {
    private String url;
 
    public RealImage(String url) {
        this.url = url;
        System.out.println("Image loaded: " + this.url);
    }
 
    @Override
    public void showImage() {
        System.out.println("Image showed: " + this.url);
    }
}
//ProxyImage.java
public class ProxyImage implements Image {
    private Image realImage;
    private String url;
 
    public ProxyImage(String url) {
        this.url = url;
        System.out.println("Image unloaded: " + this.url);
    }
 
    @Override
    public void showImage() {
        if (realImage == null) {
            realImage = new RealImage(this.url);
        } else {
            System.out.println("Image already existed: " + this.url);
        }
        realImage.showImage();
    }
}
//Client.java
public class Client {
    public static void main(String[] args) {
        System.out.println("Init proxy image: ");
        ProxyImage proxyImage = new ProxyImage("https://vntalking.com/favicon.ico");
         
        System.out.println("---");
        System.out.println("Call real service 1st: ");
        proxyImage.showImage();
         
        System.out.println("---");
        System.out.println("Call real service 2nd: ");
        proxyImage.showImage();
    }
}

5. Observer Pattern

Có rất nhiều ví dụ trong thực tế cần sử dụng tới Observer pattern. Đơn giản như việc đăng ký theo dõi (subcriber) một kênh Youtube. Bất cứ khi nào kênh Youtube đó có video mới thì bạn sẽ nhận được thông báo.

Có hai thành phần chính khi implement một Observer design pattern:

  • Registration: Nơi các đối tượng quan tâm đăng ký để nhận thông báo.
  • Notification: Nơi các đối tượng quan tâm nhận thông báo.

Bạn sẽ hiểu hơn khi đọc đoạn mã ví dụ sau:

public class Observer Pattern {
    static class Notifier {
        List  listeners = new ArrayList<> ();
        void register(Listener listener) {
            listeners.add(listener);
        }
        void raise() {
            for (Listener listener: listeners) {
                listener.announce();
            }
        }
    }
    static class Listener {
        private String name;
        Listener(String name) {
            this.name = name;
        }
        void announce() {
            System.out.println(name + " notified");
        }
    }
    public static void main(String[] args) {
        Notifier notifier = new Notifier();
        notifier.register(new Listener("event 1"));
        notifier.register(new Listener("event 2"));
        notifier.register(new Listener("event 3"));
        notifier.raise();
    }
}

Kết luận

Như vậy là chúng ta đã có cái nhìn tổng quan về Design Pattern. Mỗi một design pattern là một cách tiếp cận để giải quyết vấn đề trong một bối cảnh nhất định. Hy vọng các khái niệm này sẽ giúp ích cho các bạn trong công việc của minh.