0
0
Lập trình
Flame Kris
Flame Krisbacodekiller

Phân Tích Nguyên Tắc SOLID Trong Epsilon-Greedy Recommender (Java)

Đăng vào 1 tháng trước

• 8 phút đọc

Phân Tích Nguyên Tắc SOLID Trong Epsilon-Greedy Recommender (Java)

Trong bài viết này, chúng ta sẽ xem xét một cách triển khai đơn giản của Epsilon-Greedy Recommender trong Java và kiểm tra xem nó có tuân thủ các nguyên tắc SOLID hay không. Sau đó, chúng ta sẽ tìm hiểu cách tái cấu trúc mã nguồn để cải thiện tính khả thi, khả năng mở rộng và khả năng kiểm thử.

Mục Lục

Giới thiệu

Trong lĩnh vực học máy, việc phát triển các hệ thống gợi ý hiệu quả là rất quan trọng. Epsilon-Greedy Recommender là một trong những phương pháp đơn giản nhưng hiệu quả để giải quyết vấn đề này. Tuy nhiên, việc đảm bảo mã nguồn của bạn tuân thủ các nguyên tắc thiết kế tốt như SOLID là rất cần thiết để giữ cho mã của bạn dễ bảo trì và mở rộng.

Mã Ví Dụ

Dưới đây là mã nguồn của lớp EpsilonGreedyRecommender:

java Copy
public class EpsilonGreedyRecommender {
    private int nItems;
    private double epsilon;
    private int[] counts;
    private double[] values;
    private Random random;

    public EpsilonGreedyRecommender(int nItems, double epsilon) {
        this.nItems = nItems;
        this.epsilon = epsilon;
        this.counts = new int[nItems];
        this.values = new double[nItems];
        this.random = new Random();
    }

    public int recommend() {
        if (random.nextDouble() < epsilon) {
            return random.nextInt(nItems);
        }
        int bestIndex = 0;
        for (int i = 1; i < nItems; i++) {
            if (values[i] > values[bestIndex]) {
                bestIndex = i;
            }
        }
        return bestIndex;
    }

    public void update(int item, double reward) {
        counts[item]++;
        values[item] += (reward - values[item]) / counts[item];
    }

    public double[] getValues() {
        return values;
    }

    public int[] getCounts() {
        return counts;
    }
}

Phân Tích Nguyên Tắc SOLID

SRP – Nguyên Tắc Trách Nhiệm Đơn

📖 Định Nghĩa: Một lớp chỉ nên có một lý do duy nhất để thay đổi – nó nên có một trách nhiệm duy nhất.

🔍 Phân Tích: Lớp này đang thực hiện nhiều chức năng:

  • Lưu trữ trạng thái bandit (counts, values)
  • Thực hiện chính sách lựa chọn (recommend())
  • Cập nhật thống kê (update())

Điều này có nghĩa là bất kỳ thay đổi nào trong logic chính sách, hoặc cách lưu trữ trạng thái đều yêu cầu sửa đổi trong cùng một lớp.

Kết luận: SRP bị vi phạm một phần – chúng ta có nhiều trách nhiệm trong một nơi.

OCP – Nguyên Tắc Mở/Đóng

📖 Định Nghĩa: Các lớp nên mở cho sự mở rộng nhưng đóng cho sự sửa đổi.

🔍 Phân Tích: Nếu chúng ta muốn chuyển sang một chính sách khác (ví dụ: Softmax, UCB), chúng ta sẽ phải sửa đổi phương thức recommend() trực tiếp.
Thiết kế tốt hơn: định nghĩa một giao diện SelectionPolicy và cắm vào các triển khai khác nhau.

Kết luận: OCP bị vi phạm – việc thêm chính sách mới yêu cầu sửa đổi lớp.

LSP – Nguyên Tắc Thay Thế Liskov

📖 Định Nghĩa: Các kiểu con phải có thể thay thế cho kiểu cơ sở của chúng mà không làm thay đổi tính đúng đắn của chương trình.

🔍 Phân Tích: Chúng ta không có kế thừa ở đây, vì vậy không có gì để vi phạm.

Kết luận: LSP được tôn trọng.

ISP – Nguyên Tắc Phân Tách Giao Diện

📖 Định Nghĩa: Các client không nên bị buộc phải phụ thuộc vào các giao diện mà họ không sử dụng.

🔍 Phân Tích: Vì chúng ta không có giao diện nào cả, nên không có vấn đề ở đây.

Kết luận: ISP được tôn trọng.

DIP – Nguyên Tắc Đảo Ngược Phụ Thuộc

📖 Định Nghĩa: Phụ thuộc vào các trừu tượng, không phụ thuộc vào các triển khai cụ thể.

🔍 Phân Tích: Lớp này tạo ra chính một thể hiện Random của nó. Đây là một phụ thuộc trực tiếp vào một lớp cụ thể, điều này làm cho việc kiểm thử trở nên khó khăn hơn (không có cách nào để tiêm một RNG có thể dự đoán).

Thiết kế tốt hơn: tiêm Random như một phụ thuộc thông qua constructor (hoặc sử dụng một giao diện).

Kết luận: DIP bị vi phạm – chúng ta phụ thuộc vào một triển khai Random cụ thể.

Bảng Tóm Tắt

Nguyên Tắc Trạng Thái Ghi Chú
SRP Nhiều trách nhiệm (trạng thái + chính sách + logic cập nhật)
OCP Không thể thêm chính sách mới mà không sửa đổi mã
LSP Không có kế thừa, không vi phạm
ISP Không có giao diện lớn, không vi phạm
DIP Phụ thuộc trực tiếp vào Random, khó kiểm thử

Thiết Kế Tái Cấu Trúc

Hãy tái cấu trúc mã để tuân thủ SOLID:

  • Giới thiệu một giao diện SelectionPolicy (Mẫu Chiến Lược)
  • Tiêm Random từ bên ngoài để cải thiện khả năng kiểm thử

Bước 1: Định Nghĩa Giao Diện Chính Sách

java Copy
public interface SelectionPolicy {
    int select(double[] values);
}

Bước 2: Triển Khai Chính Sách Epsilon-Greedy

java Copy
import java.util.Random;

public class EpsilonGreedyPolicy implements SelectionPolicy {
    private final double epsilon;
    private final Random random;

    public EpsilonGreedyPolicy(double epsilon, Random random) {
        this.epsilon = epsilon;
        this.random = random;
    }

    @Override
    public int select(double[] values) {
        int nItems = values.length;
        if (random.nextDouble() < epsilon) {
            return random.nextInt(nItems);
        }
        int bestIndex = 0;
        for (int i = 1; i < nItems; i++) {
            if (values[i] > values[bestIndex]) {
                bestIndex = i;
            }
        }
        return bestIndex;
    }
}

Bước 3: Tạo Lớp Bandit Tập Trung Vào Trạng Thái

java Copy
public class Bandit {
    private final int[] counts;
    private final double[] values;
    private final SelectionPolicy policy;

    public Bandit(int nItems, SelectionPolicy policy) {
        this.counts = new int[nItems];
        this.values = new double[nItems];
        this.policy = policy;
    }

    public int recommend() {
        return policy.select(values);
    }

    public void update(int item, double reward) {
        counts[item]++;
        values[item] += (reward - values[item]) / counts[item];
    }
}

✅ Bây giờ:

  • SRP được tôn trọng → Bandit chỉ quản lý trạng thái, EpsilonGreedyPolicy chỉ xử lý việc lựa chọn.
  • OCP được tôn trọng → Chúng ta có thể thêm các chính sách mới mà không cần chạm vào Bandit.
  • DIP được tôn trọng → Random được tiêm vào, vì vậy chúng ta có thể truyền một RNG mô phỏng trong các bài kiểm tra.

Kết Luận và Hướng Dẫn Thực Hành

Việc áp dụng SOLID giúp mã của bạn dễ mở rộng và bảo trì hơn. Sử dụng giao diện và tiêm phụ thuộc giúp mã của bạn có thể kiểm thử và mạnh mẽ hơn. Ngay cả các lớp nhỏ cũng có thể hưởng lợi từ SOLID - đặc biệt nếu bạn dự đoán rằng thuật toán sẽ phát triển theo thời gian.

💡 Bạn nghĩ sao? Bạn có giữ trạng thái và chính sách cùng nhau cho các dự án nhỏ hay luôn tách chúng như thế này?

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào