Các loại Design Pattern - Giới thiệu và Ứng dụng trong Java
Dưới đây là một bài viết bằng tiếng Việt giới thiệu về các loại Design Pattern, kèm ví dụ chi tiết sử dụng Java và minh họa với biểu đồ mermaid cho trực quan:
Các loại Design Pattern: Giới thiệu và Ứng dụng trong Java
Trong quá trình phát triển phần mềm, Design Patterns (mẫu thiết kế) giúp chúng ta giải quyết các vấn đề thiết kế chung, tạo ra các hệ thống dễ bảo trì, mở rộng và tái sử dụng. Đây là những giải pháp được đúc kết từ kinh nghiệm lâu năm và có thể áp dụng trong nhiều trường hợp khác nhau.
Bài viết này sẽ giới thiệu một số Design Pattern phổ biến, được phân thành 3 nhóm chính: Creational, Structural và Behavioral. Mỗi nhóm có những đặc điểm và mục đích riêng, cùng với đó là các ví dụ minh họa bằng Java.
1. Phân loại Design Patterns
-
Creational Patterns: Liên quan đến việc tạo ra đối tượng (instance). Mục đích chính là ẩn quá trình khởi tạo, giúp code linh hoạt và giảm sự phụ thuộc giữa các thành phần.
-
Structural Patterns: Tập trung vào việc kết hợp các đối tượng lại với nhau để tạo thành một cấu trúc lớn hơn, đồng thời đảm bảo sự tương tác hợp lý giữa các đối tượng.
-
Behavioral Patterns: Xác định cách thức các đối tượng giao tiếp, phân phối trách nhiệm và xử lý các tác vụ phức tạp thông qua sự hợp tác.
2. Creational Patterns
2.1. Singleton Pattern
Mục đích: Đảm bảo một lớp chỉ có duy nhất một instance và cung cấp một điểm truy cập toàn cục đến instance đó.
Ví dụ Java:
public class Singleton {
private static Singleton instance;
// Constructor private để ngăn tạo instance từ bên ngoài
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
public void showMessage() {
System.out.println("Hello from Singleton!");
}
}
Sử dụng:
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.showMessage();
}
}
Biểu đồ mermaid minh họa:
2.2. Factory Pattern
Mục đích: Cho phép tạo ra đối tượng mà không cần chỉ định trực tiếp lớp của đối tượng đó. Thay vào đó, một Factory sẽ quyết định lớp cụ thể nào sẽ được khởi tạo dựa trên thông số đầu vào.
Ví dụ Java:
// Interface sản phẩm
public interface Shape {
void draw();
}
// Các lớp thực thi interface Shape
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Vẽ hình tròn.");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Vẽ hình vuông.");
}
}
// Factory class
public class ShapeFactory {
public Shape getShape(String shapeType) {
if(shapeType == null) {
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if(shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}
Sử dụng:
public class Main {
public static void main(String[] args) {
ShapeFactory factory = new ShapeFactory();
Shape shape1 = factory.getShape("CIRCLE");
shape1.draw();
Shape shape2 = factory.getShape("SQUARE");
shape2.draw();
}
}
2.3. Builder Pattern
Mục đích: Tách biệt quá trình xây dựng (build) một đối tượng phức tạp ra khỏi biểu diễn của nó, cho phép sử dụng cùng một quy trình để tạo ra nhiều biểu diễn khác nhau.
Ví dụ Java:
public class Computer {
// Các thành phần của máy tính
private String CPU;
private String RAM;
private String storage;
private Computer(Builder builder) {
this.CPU = builder.CPU;
this.RAM = builder.RAM;
this.storage = builder.storage;
}
public static class Builder {
private String CPU;
private String RAM;
private String storage;
public Builder setCPU(String CPU) {
this.CPU = CPU;
return this;
}
public Builder setRAM(String RAM) {
this.RAM = RAM;
return this;
}
public Builder setStorage(String storage) {
this.storage = storage;
return this;
}
public Computer build() {
return new Computer(this);
}
}
@Override
public String toString() {
return "Computer [CPU=" + CPU + ", RAM=" + RAM + ", storage=" + storage + "]";
}
}
Sử dụng:
public class Main {
public static void main(String[] args) {
Computer computer = new Computer.Builder()
.setCPU("Intel i7")
.setRAM("16GB")
.setStorage("512GB SSD")
.build();
System.out.println(computer);
}
}
3. Structural Patterns
3.1. Adapter Pattern
Mục đích: Cho phép các lớp với giao diện không tương thích có thể làm việc với nhau thông qua việc chuyển đổi giao diện của một lớp thành giao diện mà client mong đợi.
Ví dụ Java:
// Giao diện mục tiêu
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// Lớp thực thi giao diện MediaPlayer
public class AudioPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("mp3")) {
System.out.println("Đang phát file mp3: " + fileName);
} else {
System.out.println("Định dạng không được hỗ trợ: " + audioType);
}
}
}
// Giao diện khác cần chuyển đổi
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
public class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Đang phát file vlc: " + fileName);
}
@Override
public void playMp4(String fileName) {
// Không hỗ trợ
}
}
public class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// Không hỗ trợ
}
@Override
public void playMp4(String fileName) {
System.out.println("Đang phát file mp4: " + fileName);
}
}
// Adapter chuyển đổi AdvancedMediaPlayer thành MediaPlayer
public class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if(audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
} else if(audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if(audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}
Sử dụng:
public class Main {
public static void main(String[] args) {
MediaPlayer player = new AudioPlayer();
player.play("mp3", "baihat.mp3");
// Sử dụng adapter cho các định dạng khác
MediaPlayer adapter1 = new MediaAdapter("vlc");
adapter1.play("vlc", "video.vlc");
MediaPlayer adapter2 = new MediaAdapter("mp4");
adapter2.play("mp4", "video.mp4");
}
}
3.2. Decorator Pattern
Mục đích: Cho phép mở rộng chức năng của một đối tượng mà không cần thay đổi cấu trúc của đối tượng đó, bằng cách "gói" đối tượng gốc bên trong một lớp decorator.
Ví dụ Java:
// Interface Component
public interface Coffee {
double cost();
String description();
}
// Component cụ thể
public class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 5;
}
@Override
public String description() {
return "Coffee cơ bản";
}
}
// Decorator abstract
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
@Override
public String description() {
return decoratedCoffee.description();
}
}
// Decorator cụ thể
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 1.5;
}
@Override
public String description() {
return super.description() + ", thêm sữa";
}
}
Sử dụng:
public class Main {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.description() + " - Giá: " + coffee.cost());
Coffee milkCoffee = new MilkDecorator(new SimpleCoffee());
System.out.println(milkCoffee.description() + " - Giá: " + milkCoffee.cost());
}
}
4. Behavioral Patterns
4.1. Observer Pattern
Mục đích: Thiết lập mối quan hệ 1-n giữa các đối tượng sao cho khi trạng thái của một đối tượng thay đổi, tất cả các đối tượng liên quan sẽ được thông báo và cập nhật.
Ví dụ Java:
import java.util.ArrayList;
import java.util.List;
// Subject (Chủ đề)
public class Subject {
private List<Observer> observers = new ArrayList<>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public void attach(Observer observer){
observers.add(observer);
}
public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}
// Observer (Người quan sát)
public abstract class Observer {
protected Subject subject;
public abstract void update();
}
// Concrete Observer
public class ConcreteObserver extends Observer {
private String name;
public ConcreteObserver(Subject subject, String name) {
this.subject = subject;
this.name = name;
subject.attach(this);
}
@Override
public void update() {
System.out.println("Observer " + name + " nhận thông báo: state thay đổi thành " + subject.getState());
}
}
Sử dụng:
public class Main {
public static void main(String[] args) {
Subject subject = new Subject();
new ConcreteObserver(subject, "A");
new ConcreteObserver(subject, "B");
System.out.println("Thiết lập state thành 10");
subject.setState(10);
System.out.println("Thiết lập state thành 20");
subject.setState(20);
}
}
4.2. Strategy Pattern
Mục đích: Cho phép định nghĩa một họ các thuật toán, đóng gói từng thuật toán lại và làm cho chúng có thể thay thế cho nhau, giúp client có thể thay đổi thuật toán mà không cần thay đổi cấu trúc bên ngoài.
Ví dụ Java:
// Strategy interface
public interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategies
public class CreditCardStrategy implements PaymentStrategy {
private String cardNumber;
public CreditCardStrategy(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Thanh toán " + amount + " bằng thẻ tín dụng: " + cardNumber);
}
}
public class PayPalStrategy implements PaymentStrategy {
private String email;
public PayPalStrategy(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Thanh toán " + amount + " qua PayPal: " + email);
}
}
// Context sử dụng Strategy
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
Sử dụng:
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Sử dụng chiến lược thanh toán bằng thẻ tín dụng
cart.setPaymentStrategy(new CreditCardStrategy("1234-5678-9012-3456"));
cart.checkout(100);
// Sử dụng chiến lược thanh toán qua PayPal
cart.setPaymentStrategy(new PayPalStrategy("user@example.com"));
cart.checkout(200);
}
}
5. Kết luận
Design Patterns là những công cụ mạnh mẽ giúp chúng ta xây dựng các hệ thống phần mềm với cấu trúc rõ ràng, dễ bảo trì và mở rộng. Tùy vào yêu cầu cụ thể của dự án, bạn có thể lựa chọn áp dụng các pattern như Singleton, Factory, Builder, Adapter, Decorator, Observer, Strategy... để giải quyết các vấn đề thiết kế một cách hiệu quả.
Hy vọng bài viết trên đã cung cấp cho bạn cái nhìn tổng quan cũng như các ví dụ thực tế về cách áp dụng Design Patterns trong Java. Các ví dụ này chỉ là minh họa cơ bản, trong thực tế bạn có thể tinh chỉnh để phù hợp với yêu cầu cụ thể của dự án.
Chúc bạn áp dụng thành công các mẫu thiết kế này vào công việc phát triển phần mềm của mình!