Nguyên tắc thiết kế hướng đối tượng

Nguyên tắc thiết kế hướng đối tượng là nền tảng để xây dựng phần mềm có khả năng bảo trì, mở rộng và tái sử dụng cao. Dưới đây là các nguyên tắc thiết kế hướng đối tượng chính:

  1. Nguyên tắc SOLID

1.1 Nguyên tắc trách nhiệm đơn lẻ (SRP)

Một lớp chỉ nên có một lý do để thay đổi

// Vi phạm SRP
class NguoiDung {
    private String hoTen;
    private String diaChiEmail;
    
    public void luuVaoCSDL() {
        // Thao tác CSDL
    }
    
    public void guiThu() {
        // Gửi email
    }
    
    public void kiemTra() {
        // Logic kiểm tra
    }
}

// Tuân thủ SRP
class NguoiDung {
    private String hoTen;
    private String diaChiEmail;
    
    // Chỉ chứa các thuộc tính cốt lõi của người dùng
}

class KhoNguoiDung {
    public void luu(NguoiDung nguoiDung) {
        // Thao tác CSDL
    }
}

class DichVuEmail {
    public void guiThu(NguoiDung nguoiDung) {
        // Gửi email
    }
}

class BoXacThucNguoiDung {
    public boolean kiemTra(NguoiDung nguoiDung) {
        // Logic kiểm tra
    }
}

1.2 Nguyên tắc đóng mở (OCP)

Mở cho việc mở rộng, đóng cho việc sửa đổi

// Vi phạm OCP
class VeHinh {
    public void ve(String loaiHinh) {
        if (loaiHinh.equals("hinhtron")) {
            veHinhTron();
        } else if (loaiHinh.equals("hinhtugiac")) {
            veHinhTuGiac();
        }
        // Thêm hình mới cần sửa lớp này
    }
}

// Tuân thủ OCP
interface Hinh {
    void ve();
}

class HinhTron implements Hinh {
    @Override
    public void ve() {
        // Vẽ hình tròn
    }
}

class HinhTuGiac implements Hinh {
    @Override
    public void ve() {
        // Vẽ hình tứ giác
    }
}

class BoVeHinh {
    public void ve(Hinh hinh) {
        hinh.ve(); // Không cần sửa để hỗ trợ hình mới
    }
}

// Mở rộng hình mới
class HinhTamGiac implements Hinh {
    @Override
    public void ve() {
        // Vẽ hình tam giác
    }
}

1.3 Nguyên tắc thay thế Liskov (LSP)

Lớp con phải có thể thay thế được lớp cha

// Vi phạm LSP
class HinhChuNhat {
    protected int chieuRong;
    protected int chieuCao;
    
    public void datChieuRong(int chieuRong) {
        this.chieuRong = chieuRong;
    }
    
    public void datChieuCao(int chieuCao) {
        this.chieuCao = chieuCao;
    }
    
    public int tinhDienTich() {
        return chieuRong * chieuCao;
    }
}

class HinhVuong extends HinhChuNhat {
    @Override
    public void datChieuRong(int chieuRong) {
        super.datChieuRong(chieuRong);
        super.datChieuCao(chieuRong); // Phá vỡ hành vi hình chữ nhật
    }
    
    @Override
    public void datChieuCao(int chieuCao) {
        super.datChieuCao(chieuCao);
        super.datChieuRong(chieuCao); // Phá vỡ hành vi hình chữ nhật
    }
}

// Mã kiểm tra
void kiemTraDienTich(HinhChuNhat hinh) {
    hinh.datChieuRong(5);
    hinh.datChieuCao(4);
    assert hinh.tinhDienTich() == 20; // Thất bại với HinhVuong
}

// Tuân thủ LSP
abstract class Hinh {
    public abstract int tinhDienTich();
}

class HinhChuNhat extends Hinh {
    private int chieuRong;
    private int chieuCao;
    
    // getters and setters
    
    @Override
    public int tinhDienTich() {
        return chieuRong * chieuCao;
    }
}

class HinhVuong extends Hinh {
    private int canh;
    
    public void datCanh(int canh) {
        this.canh = canh;
    }
    
    @Override
    public int tinhDienTich() {
        return canh * canh;
    }
}

1.4 Nguyên tắc phân tách giao diện (ISP)

Client không nên bị ép phụ thuộc vào giao diện mà chúng không sử dụng

// Vi phạm ISP
interface NguoiLam {
    void lamViec();
    void an();
    void ngu();
}

class NguoiLamThongThai implements NguoiLam {
    @Override
    public void lamViec() { /* Làm việc */ }
    
    @Override
    public void an() { /* Ăn */ }
    
    @Override
    public void ngu() { /* Ngủ */ }
}

class NguoiMay implements NguoiLam {
    @Override
    public void lamViec() { /* Làm việc */ }
    
    @Override
    public void an() { 
        throw new UnsupportedOperationException("Người máy không ăn");
    }
    
    @Override
    public void ngu() {
        throw new UnsupportedOperationException("Người máy không ngủ");
    }
}

// Tuân thủ ISP
interface LamDuoc {
    void lamViec();
}

interface AnDuoc {
    void an();
}

interface NguDuoc {
    void ngu();
}

class NguoiLamThongThai implements LamDuoc, AnDuoc, NguDuoc {
    @Override
    public void lamViec() { /* Làm việc */ }
    
    @Override
    public void an() { /* Ăn */ }
    
    @Override
    public void ngu() { /* Ngủ */ }
}

class NguoiMay implements LamDuoc {
    @Override
    public void lamViec() { /* Làm việc */ }
}

1.5 Nguyên tắc đảo ngược phụ thuộc (DIP)

Phụ thuộc vào abstraction thay vì implementation cụ thể

// Vi phạm DIP
class DenCay {
    public void bat() {
        // Bật đèn
    }
    
    public void tat() {
        // Tắt đèn
    }
}

class CongTac {
    private DenCay den;
    
    public CongTac() {
        this.den = new DenCay(); // Phụ thuộc class cụ thể
    }
    
    public void thaoTac() {
        // Thao tác đèn
        den.bat();
    }
}

// Tuân thủ DIP
interface CoTheBatTat {
    void bat();
    void tat();
}

class DenCay implements CoTheBatTat {
    @Override
    public void bat() {
        // Bật đèn
    }
    
    @Override
    public void tat() {
        // Tắt đèn
    }
}

class Quat implements CoTheBatTat {
    @Override
    public void bat() {
        // Bật quạt
    }
    
    @Override
    public void tat() {
        // Tắt quạt
    }
}

class CongTac {
    private CoTheBatTat thietBi;
    
    public CongTac(CoTheBatTat thietBi) { // Phụ thuộc abstraction
        this.thietBi = thietBi;
    }
    
    public void thaoTac() {
        thietBi.bat();
    }
}

  1. Các nguyên tắc quan trọng khác

2.1 Nguyên tắc ưu tiên Composition hơn Inheritance

Ưu tiên sử dụng composition thay vì inheritance để tái sử dụng code

// Sử dụng inheritance (không khuyến khích)
class Chim {
    public void bay() {
        // Logic bay
    }
}

class CuG extends Chim {
    // CuG không biết bay, nhưng thừa hưởng method bay
    @Override
    public void bay() {
        throw new UnsupportedOperationException("CuG không biết bay");
    }
}

// Sử dụng composition (khuyến khích)
interface BayDuoc {
    void bay();
}

class KhaNangBay implements BayDuoc {
    @Override
    public void bay() {
        // Logic bay
    }
}

class Chim {
    protected BayDuoc khaNangBay;
    
    public Chim(BayDuoc khaNangBay) {
        this.khaNangBay = khaNangBay;
    }
    
    public void thucHienBay() {
        if (khaNangBay != null) {
            khaNangBay.bay();
        }
    }
}

class ChimSe extends Chim {
    public ChimSe() {
        super(new KhaNangBay());
    }
}

class CuG extends Chim {
    public CuG() {
        super(null); // Không có khả năng bay
    }
}

2.2 Định luật Demeter (Nguyên tắc ít nhất kiến thức)

Một đối tượng nên biết ít nhất về các đối tượng khác

// Vi phạm định luật Demeter
class KhachHang {
    private ViTien tien;
    
    public ViTien getViTien() {
        return tien;
    }
}

class NguoiBanBao {
    public void thuTien(KhachHang khachHang, int soTien) {
        ViTien tien = khachHang.getViTien(); // Biết KhachHang có ViTien
        if (tien.getTien() >= soTien) {
            tien.truTien(soTien);
        }
    }
}

// Tuân thủ định luật Demeter
class KhachHang {
    private ViTien tien;
    
    public boolean thanhToan(int soTien) {
        return tien.truTienNeuDu(soTien);
    }
}

class NguoiBanBao {
    public void thuTien(KhachHang khachHang, int soTien) {
        khachHang.thanhToan(soTien); // Chỉ tương tác với KhachHang
    }
}

2.3 Nguyên tắc DRY (Don't Repeat Yourself)

Tránh trùng lặp code

// Vi phạm DRY
class DichVuNguoiDung {
    public void taoNguoiDung(String tenDangNhap, String email) {
        if (tenDangNhap == null || tenDangNhap.trim().isEmpty()) {
            throw new IllegalArgumentException("Tên đăng nhập không được để trống");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Định dạng email không đúng");
        }
        // Logic tạo người dùng
    }
    
    public void capNhatNguoiDung(String tenDangNhap, String email) {
        if (tenDangNhap == null || tenDangNhap.trim().isEmpty()) {
            throw new IllegalArgumentException("Tên đăng nhập không được để trống");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Định dạng email không đúng");
        }
        // Logic cập nhật người dùng
    }
}

// Tuân thủ DRY
class BoXacThuc {
    public static void xacThucTenDangNhap(String tenDangNhap) {
        if (tenDangNhap == null || tenDangNhap.trim().isEmpty()) {
            throw new IllegalArgumentException("Tên đăng nhập không được để trống");
        }
    }
    
    public static void xacThucEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Định dạng email không đúng");
        }
    }
}

class DichVuNguoiDung {
    public void taoNguoiDung(String tenDangNhap, String email) {
        BoXacThuc.xacThucTenDangNhap(tenDangNhap);
        BoXacThuc.xacThucEmail(email);
        // Logic tạo người dùng
    }
    
    public void capNhatNguoiDung(String tenDangNhap, String email) {
        BoXacThuc.xacThucTenDangNhap(tenDangNhap);
        BoXacThuc.xacThucEmail(email);
        // Logic cập nhật người dùng
    }
}

  1. Ví dụ ứng dụng thực tế

3.1 Thiết kế hệ thống thương mại điện tử

// Hệ thống thương mại điện tử tuân thủ SOLID
interface PhuongThucThanhToan {
    void thanhToan(double soTien);
}

class ThanhToanTheTinDung implements PhuongThucThanhToan {
    @Override
    public void thanhToan(double soTien) {
        // Logic thanh toán thẻ tín dụng
    }
}

class ThanhToanPayPal implements PhuongThucThanhToan {
    @Override
    public void thanhToan(double soTien) {
        // Logic thanh toán PayPal
    }
}

interface DichVuThongBao {
    void guiThongBao(String noiDung);
}

class ThongBaoEmail implements DichVuThongBao {
    @Override
    public void guiThongBao(String noiDung) {
        // Logic gửi email thông báo
    }
}

class ThongBaoSMS implements DichVuThongBao {
    @Override
    public void guiThongBao(String noiDung) {
        // Logic gửi SMS thông báo
    }
}

class XuLyDonHang {
    private PhuongThucThanhToan phuongThucThanhToan;
    private DichVuThongBao dichVuThongBao;
    
    public XuLyDonHang(PhuongThucThanhToan phuongThucThanhToan, 
                       DichVuThongBao dichVuThongBao) {
        this.phuongThucThanhToan = phuongThucThanhToan;
        this.dichVuThongBao = dichVuThongBao;
    }
    
    public void xuLyDon(DonHang donHang) {
        phuongThucThanhToan.thanhToan(donHang.tinhTongTien());
        dichVuThongBao.guiThongBao("Đơn hàng đã được xử lý");
    }
}

3.2 Checklist kiểm tra nguyên tắc thiết kế

Nguyên tắc Câu hỏi kiểm tra
SRP Lớp này có chỉ một trách nhiệm không?
OCP Thêm tính năng mới có cần sửa code hiện có không?
LSP Lớp con có thể thay thế hoàn toàn lớp cha không?
ISP Giao diện có đủ nhỏ và tập trung không?
DIP Có phụ thuộc abstraction thay vì implementation cụ thể không?
Composition Có sử dụng inheritance quá mức không?
Demeter Đối tượng có biết quá nhiều về đối tượng khác không?
DRY Có logic code trùng lặp không?
  1. Cân bằng giữa các nguyên tắc thiết kế

Trong các dự án thực tế, cần cân nhắc các nguyên tắc này dựa trên tình huống cụ thể:

  1. Thiết kế quá mức vs Thiết kế vừa đủ: Không nên áp dụng nguyên tắc vì nguyên tắc
  2. Xem xét giai đoạn dự án: Giai đoạn prototype có thể linh hoạt hơn, hệ thống cốt lõi cần tuân thủ nghiêm ngặt
  3. Kỹ năng đội ngũ: Chọn độ phức tạp thiết kế phù hợp với kinh nghiệm đội ngũ
  4. Yêu cầu hiệu năng: Một số nguyên tắc có thể gây tổn hao hiệu năng
// Ví dụ thiết kế vừa đủ
// Với dự án nhỏ, có thể gộp các trách nhiệm
class DichVuNguoiDungDonGian {
    // Gộp nhiều trách nhiệm liên quan đến người dùng, nhưng code đơn giản hơn
    public void dangKy(User nguoiDung) { }
    public void dangNhap(String tenDangNhap, String matKhau) { }
    public void datLaiMatKhau(String email) { }
}

// Với hệ thống doanh nghiệp lớn, nên tách biệt nghiêm ngặt
interface DangKyNguoiDung { }
interface XacThucNguoiDung { }
interface QuanLyMatKhau { }

Việc nắm vững các nguyên tắc thiết kế này giúp bạn tạo ra các hệ thống hướng đối tượng mạnh mẽ và dễ bảo trì hơn. Điều quan trọng là hiểu được tư tưởng đằng sau các nguyên tắc, thay vì áp dụng máy móc.

Thẻ: Java design-patterns solid object-oriented clean-code

Đăng vào ngày 6 tháng 6 lúc 18:52