🐴with Java

Java Singleton (Creational Design Patterns), Adapter Pattern (Structural Design Patterns), Chain of Responsibility Pattern (Behavioral Design Patterns)

Thông sốSingleton PatternPrototype PatternFactory PatternBuilder Pattern

Mục đích

Đảm bảo chỉ có một instance của lớp và cung cấp một điểm truy cập toàn cục đến nó.

Tạo ra các đối tượng mới bằng cách sao chép một instance của đối tượng đã tồn tại (prototype) thay vì tạo mới từ đầu.

Tạo một interface cho việc tạo đối tượng, nhưng để cho lớp con quyết định lớp nào sẽ được khởi tạo.

Tách rời việc xây dựng một đối tượng phức tạp từ phần đại diện của nó, để cùng một quy trình xây dựng có thể tạo ra các đối tượng khác nhau.

Số lượng đối tượng

Chỉ có một instance duy nhất.

Số lượng đối tượng không giới hạn và dựa trên đối tượng prototype.

Số lượng đối tượng không giới hạn và tạo ra dựa trên yêu cầu.

Số lượng đối tượng không giới hạn và tạo ra dựa trên yêu cầu.

Đồng bộ

Đồng bộ là một yếu tố quan trọng để đảm bảo chỉ có một instance.

Đồng bộ không cần thiết trừ khi đối tượng prototype chứa trạng thái chia sẻ.

Đồng bộ không cần thiết trừ khi có yêu cầu đặc biệt.

Đồng bộ không cần thiết trừ khi có yêu cầu đặc biệt.

Thực thi

Thực thi trong quá trình tải hoặc khi được gọi lần đầu.

Thực thi khi cần tạo đối tượng mới từ đối tượng prototype.

Thực thi mỗi khi cần tạo đối tượng mới.

Thực thi mỗi khi cần tạo đối tượng mới với cấu trúc phức tạp.

Hiệu suất

Hiệu suất tốt do chỉ cần tạo một instance.

Hiệu suất có thể kém hơn do cần sao chép đối tượng mỗi khi tạo mới.

Hiệu suất phụ thuộc vào quá trình tạo đối tượng mới.

Hiệu suất phụ thuộc vào quá trình xây dựng đối tượng.

Mở rộng

Khó khăn trong việc mở rộng.

Dễ dàng mở rộng bằng cách tạo các prototype khác nhau.

Dễ dàng mở rộng bằng cách thêm các lớp con mới vào Factory.

Dễ dàng mở rộng bằng cách thêm các Builder mới để xây dựng các đối tượng khác nhau.

Ứng dụng

Data Connection Pool, Ghi Log, Application Config Manager

Clone User mới đăng nhập để sử dụng cho các yêu cầu khác, Clone 1 Mail Object để Send cho nhiều người, Caching đối tượng

Trong một ứng dụng web, bạn có thể sử dụng Builder để xây dựng đối tượng User với các thuộc tính như tên, tuổi, địa chỉ email và số điện thoại. Bạn có thể sử dụng Builder để thiết lập các thuộc tính bắt buộc như tên và địa chỉ email, trong khi các thuộc tính tùy chọn như tuổi và số điện thoại có thể được thiết lập theo ý muốn.

Template Pattern

Template Pattern nói rằng “Định nghĩa một bộ khung của một thuật toán trong một chức năng, chuyển giao việc thực hiện nó cho các lớp con. Mẫu Template Method cho phép lớp con định nghĩa lại cách thực hiện của một thuật toán, mà không phải thay đổi cấu trúc thuật toán“.

Điều này có nghĩa là Template method giúp cho chúng ta tạo nên một bộ khung (template) cho một vấn đề đang cần giải quyết. Trong đó các đối tượng cụ thể sẽ có cùng các bước thực hiện, nhưng trong mỗi bước thực hiện đó có thể khác nhau. Điều này sẽ tạo nên một cách thức truy cập giống nhau nhưng có hành động và kết quả khác nhau.

Template Method Pattern được sử dụng khá nhiều trong mô hình Abstract – Concrete Class. 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 thực hiện, hay ràng buộc về dữ liệu đầu vào, đầu ra, …

Trong Template method pattern, 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.

Các thành phần tham gia Template Method Pattern:

  • Abstract Class :

    • Định nghĩa các phương thức trừu tượng cho từng bước có thể được điều chỉnh bởi các lớp con.

    • Cài đặt một phương thức duy nhất điều khiển thuật toán và gọi các bước riêng lẻ đã được cài đặt ở các lớp con.

  • Concrete Class : là một thuật toán cụ thể, cài đặt các phương thức của AbstractClass. Các thuật toán này ghi đè lên các phươ ng thức trừu tượng để cung cấp các triển khai thực sự. Nó không thể ghi đè phương thức duy nhất đã được cài đặt ở AbstractClass (templateMethod).

Singleton Pattern

Chức năng

Singleton đảm bảo chỉ duy nhất môt new instance được tạo ra và nó sẽ cung cấp cho bạn một method để truy cập đến thực thể đó.

Nguyên tắc

  1. private constructor để hạn chế truy cập từ class bên ngoài

  2. đặt private static variable đảm bảo biến chỉ được khởi tạo trong class.

  3. có một method public để return instance được khởi tạo ở trên.

Các cách cài đặt

recommend cách 2 cho single-thread và Bill Pugh Singleton Implementation cho multi-thread (đạt performance cao nhất).

1. Eager initialization

Cách này khá đơn giản nhưng nó có thể được khởi tạo mà không bao giờ được dùng tới.

public class EagerInitializedSingleton {

   private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

   //private constructor to avoid client applications to use constructor
   private EagerInitializedSingleton(){}

   public static EagerInitializedSingleton getInstance(){
       return instance;
   }
}

2. Static block initialization

Tương tự eager nhưng cung cấp thêm static block để handle

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    //static block initialization for exception handling
    static{
        try{
            instance = new StaticBlockSingleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance(){
        return instance;
    }
}

3. Lazy initialization

Khắc phục được nhược điểm của 2 cách trên, chỉ khi nào geInstance được gọi thì instance mới được khởi tạo. Tuy nhiên, cách này chỉ sử dụng tốt trong trường hợp đơn luồng, trường hợp nếu có 2 luồng cùng chạy và cùng gọi hàm getInstance tại cùng một thời điểm thì đương nhiên chúng ta có ít nhất 2 thể hiện của instance.

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

    public static LazyInitializedSingleton getInstance(){
        if(instance == null){
            instance = new LazyInitializedSingleton();
        }
        return instance;
        }
}

4. Thread Safe initialization

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance(){
        if(instance == null){
            instance = new ThreadSafeSingleton();
        }
        return instance;
     }

Cách đơn giản nhất là chúng ta gọi phương thức synchronized của hàm getInstance() và như vậy hệ thống đảm bảo rằng tại cùng một thời điểm chỉ có thể có 1 luồng có thể truy cập vào hàm getInstance(), và đảm bảo rằng chỉ có duy nhất 1 thể hiện của class Tuy nhiên một menthod synchronized sẽ chạy rất chậm và tốn hiệu năng vì vậy chúng ta cần cải tiến nó đi 1 chút.

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;
    private ThreadSafeSingleton(){}

    public static ThreadSafeSingleton getInstance(){
        if(instance == null){
            synchronized(ThreadSafeSingleton.class){
                if(instance == null){
                   instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
     }
}

Thay vì chúng ta Thread Safe cả menthod getInstance() chúng ta chỉ Thread Safe một đoạn mã quan trọng.

5. Bill Pugh Singleton Implementation

Với cách làm này bạn sẽ tạo ra static nested class với vai trò 1 Helper khi muốn tách biệt chức năng cho 1 class function rõ ràng hơn.

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper{
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}

Mẫu thiết kế này đảm bảo sự an toàn khi sử dụng đa luồng bởi vì đối tượng Singleton được tạo vào thời điểm JVM tải lớp tĩnh (static class). Dưới đây là giải thích chi tiết:

  1. private BillPughSingleton(){}: Định nghĩa hàm tạo (contructor) là private để ngăn không cho các lớp khác trực tiếp tạo đối tượng BillPughSingleton từ bên ngoài.

  2. private static class SingletonHelper: Định nghĩa một lớp static nội (inner class) tên là SingletonHelper. Lớp này có nhiệm vụ tạo và sử dụng đối tượng BillPughSingleton.

  3. private static final BillPughSingleton INSTANCE = new BillPughSingleton();: Trong lớp SingletonHelper, tạo một đối tượng BillPughSingleton duy nhất là INSTANCE với từ khóa final. Điều này đảm bảo đối tượng được tạo ngay khi SingletonHelper được nạp vào bộ nhớ và khởi tạo là an toàn đa luồng (Thread Safe).

  4. public static BillPughSingleton getInstance(): Phương thức này cung cấp cho các lớp khác cách truy cập vào đối tượng Singleton. Nó trả về biến INSTANCE bên trong lớp SingletonHelper.

Cách triển khai này cho phép thực hiện Singleton Pattern với tính năng Khởi tạo lười biếng (Lazy Initialization) và Thread Safe. Do đó, nó phù hợp và hiệu quả khi sử dụng trong môi trường đa luồng.

6. Using Reflection to destroy Singleton Pattern

Singleton Pattern đảm bảo rằng chỉ có một phiên bản của một đối tượng được tạo trong toàn bộ ứng dụng. Tuy nhiên, việc sử dụng Reflection (phản ánh) có thể phá vỡ nguyên tắc này bằng cách tạo ra thêm đối tượng Singleton mới không mong muốn. Đây là cách thức phá vỡ Singleton Pattern thông qua Reflection:

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        BillPughSingleton instanceOne = BillPughSingleton.getInstance();
        BillPughSingleton instanceTwo = null;

        try {
            Constructor[] constructors = BillPughSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                constructor.setAccessible(true);
                instanceTwo = (BillPughSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
}

7. Enum Singleton

Cách tiếp cận này được coi là một trong những phương pháp đơn giản và hiệu quả nhất để đảm bảo tính Thread Safe và không cần phải sử dụng synchronization. Ví dụ:

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething(){
        //do something
    }
}
  1. public enum EnumSingleton: Định nghĩa một kiểu liệt kê (enum) tên là EnumSingleton. Mỗi kiểu liệt kê đều được coi là một lớp tĩnh và bản thân chúng tự động là Singleton.

  2. INSTANCE;: Tạo một thuộc tính enum duy nhất tên là INSTANCE. Đây là đối tượng Singleton duy nhất của cấu trúc này.

  3. public static void doSomething(): Đây là một phương thức tĩnh (static method) để thực hiện các thao tác mong muốn. Nếu bạn muốn sử dụng các biến hoặc phương thức phi tĩnh, bạn có thể thêm chúng vào cấu trúc enum và sử dụng thông qua INSTANCE.

Cách triển khai này đảm bảo rằng chỉ có một đối tượng Singleton được tạo, và tính Thread Safe luôn được đảm bảo. Cách thức này được đề nghị bởi Joshua Bloch trong cuốn sách "Effective Java" và được coi là cách triển khai Singleton Pattern tốt nhất đến thời điểm hiện tại.

8. Serialization and Singleton

Serialization là một kỹ thuật sắp xếp đối tượng cần lưu trữu một cách tuần tự. Dưới đây là quá trình đọc ghi dữ liệu khi tích hợp singleton.

public class SerializableSingletonClass implements Serializable{
    private static final long serialVersionUID = 1L;
    private int value;
    private String name;

    private SerializableSingletonClass(int value, String name) {
        if( value < 0 ) throw new IllegalArgumentException("Value may not be less than 0");
        this.value = value;
        this.name = Validate.notNull(name, "Name may not be null");
    }

    private static class SerializableSingletonHolder{
        public static final SerializableSingletonClass INSTANCE;
        static {
            INSTANCE = new SerializableSingletonClass(0, "default");
        }
    }

    private void readObject(ObjectInputStream stream) throws InvalidObjectException{
        throw new InvalidObjectException("proxy required");
    }

    private Object writeReplace(){
        return new SerializationProxy(this);
    }

    private static class SerializationProxy implements Serializable{
        private static final long serialVersionUID = 1L;
        public SerializationProxy(SerializableSingletonClass ignored) { } //Here is the question

        private Object readResolve(){
            return SerializableSingletonHolder.INSTANCE;
        }
    }
}

Sử dụng trong thực tế

Quản lý kết nối cơ sở dữ liệu

Singleton Pattern rất thích hợp để tạo ra các lớp quản lý tài nguyên như quản lý kết nối cơ sở dữ liệu (Database Connection Pool).

Đối với một ứng dụng, việc tạo và đóng kết nối cơ sở dữ liệu là một tác vụ tốn kém về mặt thời gian và tài nguyên. Vì vậy, nó không hiệu quả nếu chúng ta tạo một kết nối mới mỗi khi cần truy vấn dữ liệu. Thay vào đó, chúng ta có thể sử dụng một pool kết nối sẵn có, và mỗi khi cần, chúng ta chỉ cần lấy một kết nối từ pool này.

Dưới đây là một cách triển khai cơ bản của Database Connection Pool sử dụng Singleton Pattern:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class ConnectionPool {
    private static ConnectionPool instance;
    private List<Connection> pool;
    private static final int MAX_CONNECTIONS = 10;

    private ConnectionPool() {
        pool = new ArrayList<>();
        for(int i = 0; i < MAX_CONNECTIONS; i++) {
            try {
                Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password");
                pool.add(conn);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public static synchronized ConnectionPool getInstance() {
        if(instance == null) {
            instance = new ConnectionPool();
        }
        return instance;
    }

    public synchronized Connection getConnection() {
        if(pool.isEmpty()) {
            return null; // All connections are busy
        }
        Connection conn = pool.remove(pool.size() - 1);
        return conn;
    }

    public synchronized void releaseConnection(Connection conn) {
        pool.add(conn);
    }
}

Trong ví dụ trên, ConnectionPool là một lớp Singleton. Khi được khởi tạo, nó sẽ tạo một pool với một số lượng kết nối tối đa. Mỗi khi một phần của ứng dụng cần truy cập cơ sở dữ liệu, nó sẽ gọi ConnectionPool.getInstance().getConnection() để lấy một kết nối từ pool. Khi hoàn thành, nó sẽ gọi ConnectionPool.getInstance().releaseConnection(conn) để trả kết nối về pool.

Lưu ý rằng đây chỉ là một ví dụ đơn giản và có thể cần phải tinh chỉnh để đáp ứng nhu cầu của ứng dụng thực tế. Ví dụ, chúng ta có thể muốn tạo thêm kết nối nếu tất cả các kết nối hiện có đều đang bận, hoặc chúng ta có thể muốn thêm cơ chế timeout để không giữ kết nối quá lâu nếu không sử dụng.

Ngoài ra, việc quản lý kết nối cơ sở dữ liệu cũng cần phải cẩn trọng về việc đảm bảo an toàn thread. Trong ví dụ trên, chúng ta đã sử dụng từ khóa synchronized để đảm bảo rằng chỉ có một thread có thể lấy hoặc trả kết nối tại một thời điểm. Tuy nhiên, trong một ứng dụng thực tế với nhiều thread, việc sử dụng synchronized có thể dẫn đến hiệu suất giảm sút. Chúng ta có thể cần tìm cách tối ưu hóa việc đồng bộ hóa này, ví dụ, bằng cách sử dụng java.util.concurrent.locks.ReentrantLock hoặc một cấu trúc dữ liệu thread-safe khác.

Cuối cùng, hãy lưu ý rằng nhiều thư viện và framework đã cung cấp sẵn các cơ chế quản lý kết nối cơ sở dữ liệu, nên trong hầu hết các trường hợp, bạn không cần phải tự triển khai từ đầu. Tuy nhiên, hiểu cách hoạt động của một connection pool và cách triển khai nó sử dụng Singleton Pattern vẫn là một kỹ năng hữu ích.

Ghi Log

Singleton Pattern cũng thường được sử dụng để triển khai các lớp Logger. Logger là một thành phần rất quan trọng trong hầu hết các ứng dụng, giúp ghi lại các thông tin về hoạt động của hệ thống như các thông báo lỗi, thông tin về quá trình thực thi, các sự kiện quan trọng, v.v.

Thành phần Logger thường được thiết kế theo mô hình Singleton, vì nó chỉ cần một instance duy nhất trong toàn bộ ứng dụng, đồng thời cũng đảm bảo hiệu suất và tài nguyên sử dụng tối ưu.

Dưới đây là một cách triển khai cơ bản của một Logger sử dụng Singleton Pattern:

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;

public class Logger {
    private static Logger instance;
    private PrintWriter writer;

    private Logger() {
        try {
            FileWriter fileWriter = new FileWriter("log.txt", true);
            writer = new PrintWriter(fileWriter, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static synchronized Logger getInstance() {
        if(instance == null) {
            instance = new Logger();
        }
        return instance;
    }

    public void log(String message) {
        writer.println(LocalDateTime.now() + ": " + message);
    }
}

Trong ví dụ trên, lớp Logger chỉ có một instance, được khởi tạo khi gọi Logger.getInstance(). Phương thức log(String message) sẽ ghi tin nhắn vào một tệp log, kèm theo thời gian hiện tại. Với thiết kế này, mọi phần của ứng dụng đều có thể ghi log mà không cần quan tâm đến việc mở và đóng tệp, cũng như quản lý các tài nguyên liên quan.

Lưu ý rằng trong một ứng dụng thực tế, Logger thường cần phải hỗ trợ nhiều tính năng phức tạp hơn, như ghi log ở nhiều mức độ (debug, info, error, v.v.), ghi log vào nhiều đích khác nhau (tệp, console, v.v.), định dạng log, và cả việc đồng bộ hóa khi ghi log từ nhiều thread. Tuy nhiên, Singleton Pattern vẫn là nền tảng cho thiết kế của Logger.

Quản lý cấu hình ứng dụng

Singleton còn có thể được sử dụng để quản lý cấu hình toàn cục của ứng dụng. Hãy tưởng tượng bạn có một tệp cấu hình chứa nhiều thông số khác nhau như chuỗi kết nối cơ sở dữ liệu, API keys, các thông số môi trường, v.v. Việc đọc tệp cấu hình này và giữ nó trong bộ nhớ có thể là một tác vụ tốn kém và bạn không muốn thực hiện điều đó nhiều lần trong quá trình chạy ứng dụng. Thay vào đó, bạn có thể đọc tệp cấu hình một lần, lưu trữ các giá trị trong một đối tượng Singleton và sử dụng đối tượng đó ở bất kỳ đâu trong ứng dụng.

Dưới đây là một ví dụ về cách bạn có thể triển khai một lớp AppConfig sử dụng Singleton Pattern:

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class AppConfig {
    private static AppConfig instance;
    private Properties properties;

    private AppConfig() {
        properties = new Properties();
        try {
            FileInputStream fileInputStream = new FileInputStream("app.config");
            properties.load(fileInputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static synchronized AppConfig getInstance() {
        if(instance == null) {
            instance = new AppConfig();
        }
        return instance;
    }

    public String getProperty(String key) {
        return properties.getProperty(key);
    }
}

Trong đoạn mã trên, AppConfig đọc tệp cấu hình app.config khi được khởi tạo và lưu trữ tất cả các giá trị trong một đối tượng Properties. Phương thức getProperty(String key) sau đó có thể được sử dụng để lấy các giá trị cấu hình từ bất kỳ nơi nào trong ứng dụng.

Như vậy, thông qua việc sử dụng Singleton Pattern, chúng ta đã tạo ra một cấu trúc giúp quản lý cấu hình ứng dụng một cách hiệu quả và tiện lợi.

Adapter Pattern

  • Adapter pattern chuyển đổi interface của một class thành interface mà client yêu cầu.

  • Adapter ở giữa gắn kết các lớp làm việc với nhau dù cho có những interface không tương thích với nhau.

Ví dụ: Tạo 2 class Volt là Socket

public class Volt {

	private int volts;

	public Volt(int v){
		this.volts=v;
	}

	public int getVolts() {
		return volts;
	}

	public void setVolts(int volts) {
		this.volts = volts;
	}

}
public class Socket {

	public Volt getVolt(){
		return new Volt(120);
	}
}

Tiếp theo tạo ra interface Adapter với mong muốn đầu ra được lazy hơn.

public interface SocketAdapter {

	public Volt get120Volt();

	public Volt get12Volt();

	public Volt get3Volt();
}

Chúng ta sẽ có 2 cách để triển khai Adapter pattern:

1. Class implement

 //Using inheritance for adapter pattern
public class SocketClassAdapterImpl extends Socket implements SocketAdapter{

	Override
	public Volt get120Volt() {
		return getVolt();
	}

	Override
	public Volt get12Volt() {
		Volt v= getVolt();
		return convertVolt(v,10);
	}

	Override
	public Volt get3Volt() {
		Volt v= getVolt();
		return convertVolt(v,40);
	}

	private Volt convertVolt(Volt v, int i) {
		return new Volt(v.getVolts()/i);
	}

}

Object implement

public class SocketObjectAdapterImpl implements SocketAdapter{

	//Using Composition for adapter pattern
	private Socket sock = new Socket();

	Override
	public Volt get120Volt() {
		return sock.getVolt();
	}

	Override
	public Volt get12Volt() {
		Volt v= sock.getVolt();
		return convertVolt(v,10);
	}

	Override
	public Volt get3Volt() {
		Volt v= sock.getVolt();
		return convertVolt(v,40);
	}

	private Volt convertVolt(Volt v, int i) {
		return new Volt(v.getVolts()/i);
	}
}

Chain of Responsibility Pattern

Chain of Responsibility cho phép nhiều đối tượng cơ hội xử lý yêu cầu. Yêu cầu được truyền dọc theo chuỗi các đối tượng tiếp nhận, và khi nào tìm được đối tượng phù hợp sẽ xử lý yêu cầu đó. Mỗi đối tượng tiếp nhận liên kết với nhau qua tham chiếu, và dùng message để truyền yêu cầu nếu không tự xử lý được.

Ví dụ các ATM sử dụng Chain of Responsibility để xử lý cho cơ chế rút tiền.

Khi người dùng rút 2 triệu từ ATM, máy trả về 3 tờ 500k, 4 tờ 100k và 2 tờ 50k. Có thể do cách phân bổ tờ tiền từ ATM - nó không trả về toàn bộ tờ 500k để đảm bảo sự phân bố hợp lý giữa các mệnh giá.

Nếu tất cả người rút tiền đầu tiên (với số tiền > 500k) đều nhận tờ 500k, ATM sẽ sớm hết tờ tiền này. Khi đó, rút 5 triệu mà chỉ nhận được tờ tiền 10k sẽ gây khó khăn cho việc kiểm tra tổng số tiền của người rút.

Việc phân bổ các mệnh giá khác nhau giúp ATM duy trì đủ các tờ tiền để phục vụ nhiều khách hàng khác nhau và hạn chế trường hợp hết tờ tiền cụ thể nào đó quá sớm.

Cấu trúc

Handler (bộ xử lý)

Nó xác định các interface để xử lý yêu cầu. Có nghĩa là phân công nhiệm vụ xử lý cụ thể cho từng đối tượng tiếp nhận yêu cầu.

ConcreteHandlerA · B (bộ xử lý cụ thể)

Thực hiện giao diện của "Handler". Xử lý yêu cầu hoặc nếu nó không xử lý được yêu cầu thì gửi yêu cầu đến đối tượng xử lý tiếp theo .

Client (người sử dụng)

Áp dụng "Chain Of Responsibility", tạo ra các yêu cầu và yêu cầu đó sẽ được gửi đến các đối tượng tiếp nhận.

Quay trở lại với ví dụ về ATM ta có thể tổ chức code như sau:

B1: Tạo model Currency là số tiền mỗi lần thanh toán được dùng bởi chuỗi implement interface DispenseChain

public class Currency {

	private int amount;

	public Currency(int amt){
		this.amount=amt;
	}

	public int getAmount(){
		return this.amount;
	}
}
public interface DispenseChain {

	void setNextChain(DispenseChain nextChain);

	void dispense(Currency cur);
}

B2: Tạo các processor cho từng loại tiền 50K, 20K, 10K.

public class Dollar50Dispenser implements DispenseChain {

	private DispenseChain chain;

	@Override
	public void setNextChain(DispenseChain nextChain) {
		this.chain=nextChain;
	}

	@Override
	public void dispense(Currency cur) {
		if(cur.getAmount() >= 50){
			int num = cur.getAmount()/50;
			int remainder = cur.getAmount() % 50;
			System.out.println("Dispensing "+num+" 50$ note");
			if(remainder !=0) this.chain.dispense(new Currency(remainder));
		}else{
			this.chain.dispense(cur);
		}
	}

}
public class Dollar20Dispenser implements DispenseChain{

	private DispenseChain chain;

	Override
	public void setNextChain(DispenseChain nextChain) {
		this.chain=nextChain;
	}

	Override
	public void dispense(Currency cur) {
		if(cur.getAmount() >= 20){
			int num = cur.getAmount()/20;
			int remainder = cur.getAmount() % 20;
			System.out.println("Dispensing "+num+" 20$ note");
			if(remainder !=0) this.chain.dispense(new Currency(remainder));
		}else{
			this.chain.dispense(cur);
		}
	}

}
public class Dollar10Dispenser implements DispenseChain {

	private DispenseChain chain;

	Override
	public void setNextChain(DispenseChain nextChain) {
		this.chain=nextChain;
	}

	Override
	public void dispense(Currency cur) {
		if(cur.getAmount() >= 10){
			int num = cur.getAmount()/10;
			int remainder = cur.getAmount() % 10;
			System.out.println("Dispensing "+num+" 10$ note");
			if(remainder !=0) this.chain.dispense(new Currency(remainder));
		}else{
			this.chain.dispense(cur);
		}
	}

}

B3: Vận hành máy ATM này chúng ta cần lưu ý: các processor xử lý request lần lượt là: Dispenser 50K >> Dispenser 20K >> Dispenser 10K

public class ATMDispenseChain {

	private DispenseChain c1;

	public ATMDispenseChain() {
		// initialize the chain
		this.c1 = new Dollar50Dispenser();
		DispenseChain c2 = new Dollar20Dispenser();
		DispenseChain c3 = new Dollar10Dispenser();

		// set the chain of responsibility
		c1.setNextChain(c2);
		c2.setNextChain(c3);
	}

	public static void main(String[] args) {
		ATMDispenseChain atmDispenser = new ATMDispenseChain();
		while (true) {
			int amount = 0;
			System.out.println("Enter amount to dispense");
			Scanner input = new Scanner(System.in);
			amount = input.nextInt();
			if (amount % 10 != 0) {
				System.out.println("Amount should be in multiple of 10s.");
				return;
			}
			// process the request
			atmDispenser.c1.dispense(new Currency(amount));
		}

	}

}

Last updated