Reading Time: 17 minutes

Việc sử dụng các design pattern (mẫu thiết kế) rất phổ biến trong giới lập trình viên. Mỗi design pattern là một giải pháp thiết kế được mô tả rõ ràng cho một vấn đề phần mềm thường gặp khi viết code.

Một số lợi ích của việc sử dụng design pattern bao gồm:

  1. Design pattern đã được định nghĩa sẵn. Nó cung cấp một phương pháp tiếp cận theo tiêu chuẩn để giải quyết một vấn đề lặp đi lặp lại, do đó giúp ta tiết kiệm thời gian nếu sử dụng nó một cách hợp lý. Có nhiều design pattern trong Java mà chúng ta có thể sử dụng trong các dự án dựa của mình.
  2. Việc sử dụng design pattern tăng tính khả năng tái sử dụng, dẫn đến code ổn định và dễ bảo trì hơn. Nó giúp giảm tổng chi phí sở hữu (TCO) của sản phẩm phần mềm.
  3. Vì các design pattern đã được định nghĩa sẵn, code chúng ta viết trở nên dễ hiểu và dễ debug hơn. Điều này dẫn đến việc phát triển phần mềm nhanh hơn và các thành viên mới trong nhóm có thể dễ dàng nắm bắt code dễ hơn.

Các design pattern trong Java được chia thành ba loại: creational (khởi tạo), structural (cấu trúc), và behavioral (hành vi). Bài viết này sẽ giới thiệu sơ qua tất cả design pattern thông dụng trong Java trong từng thể loại đó.

Các design pattern cho việc khởi tạo (Creational)

Design pattern loại creational cung cấp các giải pháp để khởi tạo một đối tượng theo cách tốt nhất có thể cho từng tình huống cụ thể.

1. Singleton

Singleton pattern hạn chế việc khởi tạo một class và đảm bảo rằng chỉ có một thực thể của class đó tồn tại trong máy ảo Java (JVM). Việc triển khai singleton pattern luôn là một chủ đề gây tranh cãi giữa các nhà phát triển.

2. Factory

Factory pattern được sử dụng khi chúng ta có một superclass với nhiều subclass và chúng ta cần trả về một trong các subclass đó dựa trên input đầu vào. Pattern này loại bỏ trách nhiệm khởi tạo một class khỏi chương trình client và chuyển giao cho factory class. Chúng ta có thể áp dụng singleton pattern cho factory class hoặc làm cho factory method trở thành static.

3. Abstract Factory

Nó tương tự như factory pattern và là một factory của các factory. Nếu bạn đã quen thuộc với factory design pattern trong Java, bạn sẽ nhận thấy rằng chúng ta có một factory class duy nhất trả về các subclass khác nhau dựa trên input được cung cấp, và factory class đó sử dụng các câu lệnh if/else hoặc switch để đạt được điều này.

Trong abstract factory pattern, chúng ta loại bỏ khối if/else và có một factory class cho mỗi subclass, sau đó một abstract factory class sẽ trả về subclass dựa trên factory class đầu vào.

4. Builder

Builder pattern được tạo ra để giải quyết một số vấn đề với factory và abstract factory khi đối tượng chứa rất nhiều thuộc tính. Pattern này giải quyết vấn đề với số lượng lớn các tham số tùy chọn và trạng thái không nhất quán bằng cách cung cấp một cách để xây dựng đối tượng từng bước và cung cấp một phương thức sẽ thực sự trả về đối tượng cuối cùng.

5. Prototype

Prototype pattern được sử dụng khi việc tạo đối tượng đòi hỏi nhiều thời gian cũng như tài nguyên, trong khi bạn đã có một đối tượng tương tự tồn tại. Nó cung cấp một cơ chế để sao chép đối tượng gốc sang một đối tượng mới và sau đó sửa đổi nó theo nhu cầu của chúng ta.

Pattern này sử dụng cơ chế cloning của Java để sao chép đối tượng. Nó yêu cầu class mà bạn đang sao chép phải cung cấp tính năng sao chép. Việc này không nên được thực hiện bởi bất kỳ class nào khác. Tuy nhiên, việc sử dụng shallow copy (copy nông) hay deep copy (copy sâu) các thuộc tính của đối tượng phụ thuộc vào yêu cầu cụ thể của từng trường hợp.

Các design pattern thông dụng trong Java cho cấu trúc (Structural)

Structural design pattern cung cấp các cách khác nhau để tạo ra cấu trúc của một class (ví dụ sử dụng kế thừa và composition để tạo ra một class lớn từ các class nhỏ hơn).

1. Adapter

Design pattern này được sử dụng để hai giúp interface không liên quan có thể làm việc cùng nhau. Đối tượng kết nối các interface không liên quan này được gọi là adapter.

2. Composite

Composite pattern được sử dụng khi chúng ta phải biểu diễn một cấu trúc phân cấp dạng một phần-toàn thể (part-whole hierarchy). Khi chúng ta cần tạo ra một cấu trúc mà ở đó các đối tượng phải được đối xử theo cùng một cách, chúng ta có thể áp dụng kiểu thiết kế này.

3. Proxy

Proxy pattern cung cấp một đối tượng giữ chỗ cho một đối tượng khác để kiểm soát quyền truy cập vào nó. Pattern này được sử dụng khi chúng ta muốn cung cấp quyền truy cập chức năng có kiểm soát.

4. Flyweight

Mẫu này được sử dụng khi chúng ta cần tạo ra nhiều đối tượng của một class. Mỗi đối tượng trong Java đều tiêu tốn không gian bộ nhớ và điều đó có thể gây ra vấn đề ở các thiết bị có ít bộ nhớ (như thiết bị di động hoặc hệ thống nhúng). Flyweight design pattern có thể được áp dụng để giảm tải cho bộ nhớ bằng cách chia sẻ đối tượng.

Cách triển khai string pool trong Java là một trong những ví dụ điển hình nhất về việc triển khai pattern này.

5. Facade

Pattern này được sử dụng để giúp các ứng dụng client dễ dàng tương tác với hệ thống.

6. Bridge

Khi chúng ta có hệ thống phân cấp interface ở cả interface lẫn phần triển khai, thì bridge design pattern có thể được sử dụng để tách rời interface khỏi phần triển khai và ẩn các chi tiết triển khai khỏi chương trình client. Việc triển khai nó tuân theo quan tắc ưu tiên việc kết hợp (composition) hơn kế thừa (inheritance).

7. Decorator

Ta có thể sử dụng design pattern này để sửa đổi chức năng của một đối tượng tại thời điểm runtime. Đồng thời, việc này sẽ không gây ảnh hưởng các instance khác của cùng một class, do đó đối tượng riêng lẻ sẽ bị sửa đổi hành vi.

Decorator pattern sử dụng các abstract class hoặc interface với composition để triển khai. Chúng ta sử dụng kế thừa hoặc composition để mở rộng hành vi của một đối tượng, nhưng điều này được thực hiện tại thời gian biên dịch và áp dụng cho tất cả các instance của class.

Chúng ta không thể thêm bất kỳ chức năng mới nào hoặc loại bỏ bất kỳ hành vi hiện có nào tại runtime. Đây là lúc decorator pattern trở nên có ích.

Các design pattern cho hành vi (Behavioral)

Các behavioral pattern giúp nâng cao tính tương tác giữa các đối tượng cũng như làm cho chúng linh hoạt và tách rời (loose-coupling) hơn để dễ dàng mở rộng.

1. Template Method

Template method pattern (mẫu phương thức khung) hay được sử dụng để tạo một method stub (khung sườn phương thức) và ủy thác một số bước triển khai cho các subclass. Template method định nghĩa các bước để thực thi một thuật toán, và nó có thể cung cấp một triển khai mặc định chung cho tất cả hoặc một số subclass.

2. Mediator

Design pattern này được sử dụng để cung cấp một phương tiện giao tiếp tập trung giữa các đối tượng khác nhau trong một hệ thống. Nếu các đối tượng tương tác trực tiếp với nhau, các thành phần hệ thống sẽ phụ thuộc quá chặt với nhau, làm cho chi phí bảo trì cao hơn và chúng không còn đủ linh hoạt để dễ dàng mở rộng.

Mediator pattern tập trung vào việc cung cấp một mediator (bộ điều phối trung tâm) giữa các đối tượng để giao tiếp và tạo phụ thuộc lỏng giữa các đối tượng. Mediator hoạt động như một router (điều hướng) giữa các đối tượng và nó có thể có logic riêng cho việc giao tiếp giữa các thành phần.

3. Chain of Responsibility

Pattern này giúp đạt được tính loose-coupling (phụ thuộc lỏng lẻo) khi một request từ client được chuyển qua một chuỗi các đối tượng để xử lý. Sau đó, đối tượng trong chuỗi sẽ quyết định đối tượng nào sẽ xử lý request và liệu request có cần được gửi đến đối tượng tiếp theo trong chuỗi hay không.

Chúng ta biết rằng có thể có nhiều khối catch trong một khối lệnh try-catch. Ở đây, mỗi khối catch giống như một bộ xử lý để xử lý exception cụ thể nào đó. Vì vậy, khi một exception xảy ra trong khối try, nó được gửi đến khối catch đầu tiên để xử lý. Nếu khối catch không thể xử lý nó, nó sẽ chuyển tiếp request đến catch tiếp theo trong chuỗi (tức là khối catch tiếp theo). Nếu ngay cả khối catch cuối cùng cũng không thể xử lý nó, exception sẽ được ném ra ngoài chuỗi cho chương trình gọi.

4. Observer

Hãy cân nhắc dùng pattern này khi bạn quan tâm đến trạng thái của một đối tượng và muốn nhận thông báo bất cứ khi nào có bất kỳ thay đổi nào. Trong observer pattern, đối tượng theo dõi trạng thái của một đối tượng khác được gọi là observer (người quan sát), và đối tượng đang được theo dõi được gọi là subject (đối tượng).

Java cung cấp sẵn các tiện ích để triển khai observer pattern thông qua class java.util.Observable và interface java.util.Observer. Tuy nhiên, nó không được sử dụng rộng rãi vì các implementation này còn hạn chế. Hơn nữa, chúng ta ít khi muốn phải kế thừa một class chỉ để triển khai observer pattern vì Java không hỗ trợ đa kế thừa trong các class.

Java Message Service (JMS) sử dụng observer pattern cùng với mediator pattern để cho phép các ứng dụng đăng ký và xuất data cho các ứng dụng khác.

5. Strategy

Strategy pattern được sử dụng khi chúng ta có nhiều thuật toán cho một tác vụ cụ thể, và client sẽ quyết định triển khai nào sẽ được sử dụng tại runtime. Nó còn được gọi là policy pattern. Chúng ta cần định nghĩa nhiều thuật toán và để các ứng dụng client truyền thuật toán sẽ được sử dụng dưới dạng tham số.

Một trong những ví dụ điển hình nhất của pattern này là phương thức Collections.sort(). Nó nhận tham số Comparator, và dựa trên các cách triển khai khác nhau của interface Comparator, các đối tượng sẽ được sắp xếp theo những cách khác nhau.

6. Command

Command pattern được sử dụng để triển khai loose-coupling trong mô hình request-response. Trong pattern này, request được gửi đến invoker (bộ kích hoạt) và invoker chuyển nó cho đối tượng command (lệnh) được đóng gói. Đối tượng command chuyển request đến phương thức thích hợp của receiver (nơi nhận) để thực hiện hành động cụ thể.

7. State

Pattern này được dùng khi một đối tượng thay đổi hành vi dựa trên trạng thái nội tại của nó.

Nếu chúng ta phải thay đổi hành vi của một đối tượng dựa trên trạng thái của nó, chúng ta có thể có một biến trạng thái trong đối tượng và sử dụng khối điều kiện if-else để thực hiện các hành động khác nhau dựa trên trạng thái đó. State pattern triển khai context và state để đạt được điều này một cách có hệ thống và có tính loose-coupling.

8. Visitor

Visitor pattern được sử dụng khi chúng ta phải thực hiện một thao tác trên một nhóm các đối tượng tương tự nhau. Với sự trợ giúp của visitor pattern, chúng ta có thể di chuyển logic thao tác từ các đối tượng sang một class khác.

9. Interpreter

Interpreter pattern được sử dụng để định nghĩa một biểu diễn ngữ pháp của một ngôn ngữ và cung cấp một interpreter (trình diễn dịch) để xử lý ngữ pháp này.

10. Iterator

Iterator pattern cung cấp một cách tiêu chuẩn để duyệt qua một nhóm đối tượng. Nó được nhiều trong Java Collection Framework, nơi nó cung cấp các phương thức để duyệt qua một collection.

Pattern này cũng được sử dụng để cung cấp các loại iterator khác nhau dựa trên yêu cầu của người lập trình. Nó ẩn đi việc triển khai thực tế của việc duyệt qua collection và các chương trình client sử dụng các phương thức của iterator.

11. Memento

Tham khảo memento design pattern khi bạn muốn lưu trạng thái của một đối tượng để có thể khôi phục lại sau này. Pattern này thực thi việc đó bằng cách ngăn truy cập từ phía ngoài đối tượng vào dữ liệu trạng thái đã lưu của đối tượng, từ đó bảo vệ tính toàn vẹn của dữ liệu trạng thái đã lưu.

Memento pattern được triển khai với hai đối tượng: originator và caretaker. Originator là đối tượng có trạng thái cần được lưu và khôi phục, và nó sử dụng một inner class để lưu trạng thái của đối tượng. Inner class này được gọi là Memento, và nó phải là private để không thể bị truy cập từ các đối tượng khác.

Các design pattern khác

Có rất nhiều design pattern không thuộc 3 nhóm trên. Chúng ta hãy xem qua một số ví dụ phổ biến sau.

1. DAO

Data Access Object (DAO, đối tượng truy cập dữ liệu) được sử dụng để tách riêng logic lưu trữ dữ liệu lâu dài sang một tầng riêng biệt. DAO là một pattern rất phổ biến khi chúng ta thiết kế các hệ thống làm việc với cơ sở dữ liệu.

Ý tưởng chính của pattern này là giữ cho service layer (lớp dịch vụ) tách biệt khỏi data access layer (lớp truy cập). Bằng cách này, chúng ta thực hiện việc tách biệt logic trong ứng dụng của mình.

2. Dependency Injection Pattern

Mẫu dependency injection (chèn phụ thuộc) cho phép chúng ta loại bỏ các dependency được chèn cứng và làm cho ứng dụng của chúng ta có liên kết lỏng, dễ mở rộng và dễ bảo trì.

Chúng ta có thể triển khai kiểu thiết kế này trong Java để chuyển việc giải quyết phụ thuộc (dependency resolution) từ lúc biên dịch sang lúc chạy (runtime). Spring được xây dựng dựa trên nguyên lý này.

3. MVC

Model-View-Controller (MVC, Dữ liệu-Hiển thị-Xử lý) là một trong những pattern kiến trúc lâu đời và được sử dụng rộng rãi nhiều nhất để tạo các ứng dụng web. Nó chia logic ứng dụng thành ba thành phần liên kết với nhau, mỗi thành phần có trách nhiệm riêng.

Model đại diện cho dữ liệu và business logic, View chịu trách nhiệm hiển thị giao diện người dùng, và Controller xử lý đầu vào từ người dùng và cập nhật Model và View tương ứng. Việc tách biệt các mối quan tâm này là lý do MVC là phương pháp lý tưởng để xây dựng các ứng dụng web có khả năng mở rộng, dễ bảo trì và linh hoạt.

Pattern Anti-Pattern: Khi nào không nên sử dụng chúng

Pattern anti-pattern đề cập đến việc sử dụng sai hoặc lạm dụng design pattern và gây ra nhiều tác hại hơn là lợi ích. Chúng ta phải hiểu khi nào không nên áp dụng một design pattern để tránh sự phức tạp không cần thiết, các vấn đề về hiệu năng hoặc các vấn đề về khả năng bảo trì.

Một số trường hợp phổ biến mà design pattern có thể bị sử dụng sai bao gồm:

  • Phức tạp hóa (Over-engineering): Áp dụng một design pattern cho một vấn đề đơn giản, làm cho mọi thứ trở nên phức tạp hơn.
  • Tối ưu hóa sớm (Premature optimization): Triển khai một design pattern để tối ưu hóa hiệu năng trước khi việc này thực sự cần thiết.
  • Hiểu sai vấn đề: Áp dụng một design pattern mà không hiểu đầy đủ vấn đề mà nó giải quyết.

Đây là một ví dụ về việc phức tạp hóa vấn đề trong Java:

// Dùng Factory pattern cho một vấn đề đơn giản
public interface Animal {
    void sound();
}

public class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Woof");
    }
}

public class Cat implements Animal {
    @Override
    public void sound() {
        System.out.println("Meow");
    }
}

public class AnimalFactory {
    public static Animal createAnimal(String type) {
        if (type.equals("dog")) {
            return new Dog();
        } else if (type.equals("cat")) {
            return new Cat();
        } else {
            return null;
        }
    }
}

// Cách dùng
Animal animal = AnimalFactory.createAnimal("dog");
animal.sound(); // Output: Woof

// Giải pháp đơn giản không cần dùng Factory pattern
public class Animal {
    public static void sound(String type) {
        if (type.equals("dog")) {
            System.out.println("Woof");
        } else if (type.equals("cat")) {
            System.out.println("Meow");
        }
    }
}

// Cách dùng
Animal.sound("dog"); // Output: Woof

Tích hợp với Java hiện đại

Các tính năng Java hiện đại như biểu thức lambda, tham chiếu phương thức (method reference) và lập trình hàm, có thể được tận dụng để đơn giản hóa việc triển khai các design pattern. Ví dụ, Strategy pattern có thể được triển khai ngắn gọn hơn bằng cách sử dụng biểu thức lambda, giúp code dễ diễn đạt và dễ đọc hơn.

Dưới đây là ví dụ của việc sử dụng biểu thức lambda để triển khai Strategy pattern trong Java:

// Cách triển khai Strategy pattern truyền thống
public interface SortingStrategy {
    void sort(int[] array);
}

public class BubbleSortStrategy implements SortingStrategy {
    @Override
    public void sort(int[] array) {
        // Triển khai Bubble sort
    }
}

public class QuickSortStrategy implements SortingStrategy {
    @Override
    public void sort(int[] array) {
        // Triển khai Quick sort
    }
}

public class Sorter {
    private SortingStrategy strategy;

    public Sorter(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void sort(int[] array) {
        strategy.sort(array);
    }
}

// Cách dùng
Sorter sorter = new Sorter(new BubbleSortStrategy());
sorter.sort(new int[]{5, 2, 8, 3, 1, 6, 4});

// Dùng biểu thức lambda để triển khai Strategy pattern
public class SorterLambda {
    public void sort(int[] array, SortingStrategy strategy) {
        strategy.sort(array);
    }
}

// Cách dùng
SorterLambda sorterLambda = new SorterLambda();
sorterLambda.sort(new int[]{5, 2, 8, 3, 1, 6, 4}, (int[] arr) -> {
    // Triển khai Bubble sort
});

Mối quan hệ với các nguyên tắc SOLID

SOLID là một tập hợp các nguyên tắc thiết kế nhằm làm code sạch hơn và dễ bảo trì hơn. Chúng liên quan chặt chẽ đến design pattern, vì nhiều pattern được thiết kế để tuân thủ các nguyên lý này.

Các nguyên lý SOLID bao gồm:

  • Nguyên lí trách nhiệm đơn nhất (Single Responsibility Principle, SRP): Một class chỉ nên có một lý do để thay đổi.
  • Nguyên lí đóng/mở (Open-Closed Principle, OCP): Khả năng cho phép mở rộng chức năng mới với một class mà không cần thay đổi code hiện có.
  • Nguyên lí thay thế Liskov (Liskov Substitution Principle, LSP): Các kiểu con phải có thể thay thế cho các kiểu cơ sở của chúng.
  • Nguyên lí phân tách giao diện (Interface Segregation Principle, ISP): Client không nên bị buộc phải phụ thuộc vào các interface mà nó không sử dụng.
  • Nguyên lí đảo ngược phụ thuộc (Dependency Inversion Principle, DIP): Các module cấp cao không nên phụ thuộc vào các module cấp thấp, mà cả hai nên phụ thuộc vào các trừu tượng (abstraction).

Bằng cách tuân theo các nguyên lý SOLID, ta có thể tạo ra các hệ thống phần mềm dễ bảo trì, linh hoạt và có khả năng mở rộng và sửa đổi theo thời gian hơn.

Các lỗi thường gặp và giải pháp

Lạm dụng global state

Việc lạm dụng global state (trạng thái toàn cục) xảy ra khi một chương trình phụ thuộc quá nhiều vào các biến toàn cục (global), gây khó khăn cho việc hiểu và bảo trì code.

Để khắc phục vấn đề này, bạn cần:

  • Giảm thiểu việc sử dụng biến toàn cục bằng cách truyền dữ liệu cần thiết dưới dạng tham số cho các hàm hoặc phương thức.
  • Xem xét sử dụng singleton pattern hoặc một framework chèn phụ thuộc để quản lý global state một cách có kiểm soát hơn.

Ví dụ giảm việc sử dụng biến toàn cục tối đa:

// Trước
public class MyClass {
    private static int globalVariable = 0;
    public void doSomething() {
        globalVariable++;
    }
}

// Sau
public class MyClass {
    private int localVariable = 0;
    public void doSomething() {
        localVariable++;
    }
}

Sử dụng pattern khi không cần thiết

Sử dụng design pattern ở những nơi không cần thiết có thể dẫn đến over-engineering và tăng độ phức tạp. Để tránh điều này:

  • Chỉ áp dụng design pattern khi chúng giải quyết một vấn đề cụ thể hoặc cải thiện khả năng đọc và bảo trì code.
  • Hãy xem xét nguyên lý tối giản hóa KISS (Keep It Simple, Stupid) và chọn giải pháp đơn giản nhất mà vẫn đáp ứng yêu cầu.

Ví dụ áp dụng nguyên lý KISS:

// Trước (phức tạp hóa)
public class ComplexCalculator {
    private CalculatorStrategy strategy;
    public ComplexCalculator(CalculatorStrategy strategy) {
        this.strategy = strategy;
    }
    public int calculate(int a, int b) {
        return strategy.calculate(a, b);
    }
}

// Sau (đơn giản hóa)
public class SimpleCalculator {
    public int calculate(int a, int b) {
        return a + b;
    }
}

Áp dụng sai pattern cấu trúc

Áp dụng sai structural pattern có thể dẫn đến liên kết quá chặt và cấu trúc code cứng nhắc. Để khắc phục điều này:

  • Đảm bảo rằng structural pattern được chọn phù hợp với phạm vi vấn đề và các yêu cầu của hệ thống.
  • Xem xét các pattern thay thế hoặc kết hợp các pattern để đạt được thiết kế linh hoạt và dễ bảo trì hơn.

Ví dụ việc chọn dùng pattern đúng theo phạm vi vấn đề:

// Trước (dùng nhầm pattern)
public class User {
    private List<Order> orders;
    public List<Order> getOrders() {
        return new ArrayList<>(orders);
    }
}

// Sau (dùng đúng pattern)
public class User {
    private OrderService orderService;
    public List<Order> getOrders() {
        return orderService.getOrdersForUser(this);
    }
}

Không xử lý concurrency

Không xử lý concurrency (việc thực thi song song) có thể dẫn đến race condition (điều kiện tranh chấp), deadlock (tắc nghẽn) và các vấn đề đồng bộ hóa khác. Để giải quyết vấn đề này:

  • Xác định các khu vực của code yêu cầu truy cập đồng thời và triển khai các cơ chế đồng bộ hóa thích hợp như lock (khóa), semaphore hoặc các tiến trình atomic (nguyên tử).
  • Xem xét sử dụng các abstraction concurrency (cấu trúc trừu tượng cho thực thi song song) cấp cao hơn như parallel stream (dòng song song) hoặc các collection hỗ trợ concurrency để đơn giản hóa việc lập trình thực thisong song.

Ví dụ thực thi song song với lock:

public class ConcurrentCounter {
    private int count = 0;
    private final Object lock = new Object();
    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Không hủy đăng ký observer

Lỗi này có thể dẫn đến rò rỉ bộ nhớ và các hành vi không mong muốn. Để khắc phục điều này:

  • Đảm bảo rằng các observer luôn được hủy đăng ký đúng cách khi ta không còn cần chúng hoặc khi đối tượng được quan sát đang bị hủy.
  • Xem xét sử dụng tham chiếu yếu (weak reference) cho các observer để ngăn các tham chiếu mạnh giữ chúng tồn tại một cách không cần thiết.

Các câu hỏi thường gặp

1. Design pattern trong Java là gì?

Design pattern là các giải pháp thiết kế code có thể tái sử dụng cho các vấn đề phổ biến phát sinh trong quá trình thiết kế phần mềm. Chúng cung cấp một phương pháp tiếp cận đã được tiêu chuẩn hóa và chứng minh trong thực tế để giải quyết một vấn đề thiết kế cụ thể. Sử dụng các pattern đó có thể giúp code trở nên linh hoạt hơn và dễ bảo trì cũng như mở rộng hơn.

Ví dụ ở dưới triển khai Singleton pattern để đảm bảo rằng chỉ có một instance của một class được tạo ra. Việc này tạo điều kiện cho việc quản lý tài nguyên hoặc cung cấp một điểm truy cập toàn cục dễ dàng hơn.

public class Observable {
    private List<Observer> observers = new ArrayList<>();
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}

2. Có bao nhiêu design pattern trong Java?

Có 23 design pattern cổ điển được mô tả trong cuốn sách của Gang of Four. Chúng được phân thành ba nhóm: Creational, Structural, và Behavioral. Tuy nhiên, cũng có nhiều pattern khác đã được tạo ra và ghi nhận theo thời gian.

3. Design pattern nào được sử dụng nhiều nhất trong Java?

Câu trả lời có lẽ là Singleton pattern. Lý do là nó đơn giản để triển khai và cung cấp một cách trực tiếp để quản lý quyền truy cập toàn cục vào một tài nguyên.

Ví dụ sau dùng Singleton pattern cho cơ chế ghi log, nơi cần một instance duy nhất của logger trong toàn bộ ứng dụng.

public class Logger {
    private static Logger instance;
    private Logger() {}
    public static synchronized Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
    public void log(String message) {
        System.out.println(message);
    }
}

4. Ta có được phép kết hợp nhiều design pattern trong một dự án không?

Việc kết hợp nhiều hơn một design pattern thông dụng trong Java trong cùng một dự án là việc phổ biến và chấp nhận được. Thực tế, việc kết hợp các pattern có thể dẫn đến các hệ thống phần mềm mạnh mẽ và dễ bảo trì hơn. Ví dụ bạn có thể sử dụng Factory pattern để tạo đối tượng và Singleton pattern để quản lý quyền truy cập vào một tài nguyên.

Ví dụ ở dưới kết hợp Factory và Singleton để tạo một hệ thống ghi log sử dụng một instance duy nhất của logger, nhưng cho phép việc linh hoạt tạo các loại logger khác nhau.

public class LoggerFactory {
    private static LoggerFactory instance;
    private LoggerFactory() {}
    public static synchronized LoggerFactory getInstance() {
        if (instance == null) {
            instance = new LoggerFactory();
        }
        return instance;
    }
    public Logger createLogger(String type) {
        if (type.equals("console")) {
            return new ConsoleLogger();
        } else if (type.equals("file")) {
            return new FileLogger();
        } else {
            return null;
        }
    }
}

5. Các design pattern có còn phù hợp trong Java hiện đại (Java 8+) không?

Các design pattern vẫn còn phù hợp để sử dụng trong Java hiện đại. Mặc dù Java 8 đã giới thiệu các tính năng lập trình hàm và biểu thức lambda (vốn có thể đơn giản hóa một số design pattern), các nguyên tắc cốt lõi của design pattern vẫn không thay đổi.

Các mẫu thiết kế này tiếp tục cung cấp một cách để viết code dễ bảo trì, có khả năng mở rộng và hiệu quả, nên chúng vẫn là kiến thức căn bản đối với bất kỳ lập trình viên Java nào.

Tổng kết

Chúng tôi hy vọng rằng thông qua bài viết này, bạn đã có thể nắm bắt và hiểu rõ hơn về tầm quan trọng, các ví dụ điển hình cũng như những nguyên tắc tốt nhất khi áp dụng các design pattern trong Java. Bạn có thể khám phá thêm các code ví dụ khác trong repo GitHub này.

Để tiếp tục hành trình học hỏi, phát triển kỹ năng chuyên môn một cách liên tục và không bị tụt hậu so với xu hướng công nghệ đang thay đổi nhanh chóng, hãy tìm đọc các hướng dẫn Java chi tiết khác mà chúng tôi đã biên soạn và đưa lên trang này.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.