Trang chủ » Blog » SOLID Trong Java: Áp Dụng Thực Tế Qua Refactoring

SOLID Trong Java: Áp Dụng Thực Tế Qua Refactoring

| Blog

Trong kỹ nghệ phần mềm, SOLID là tập hợp 5 nguyên tắc thiết kế hướng đối tượng (OOP) do Robert C. Martin (Uncle Bob) giới thiệu. Tuy nhiên, việc hiểu lý thuyết đơn thuần rất khác với việc nhận biết mùi mã nguồn vi phạm và tiến hành refactoring trong dự án thực tế.

Bài viết này đi sâu vào phân tích từng nguyên tắc SOLID thông qua góc nhìn refactoring: Từ việc phát hiện vi phạm (Detect Violation) trong một codebase Java legacy, cho đến việc tái cấu trúc (Refactoring) để đạt được thiết kế chuẩn mực, dễ bảo trì và dễ mở rộng. Cuối bài sẽ có một hướng dẫn thực tế về khi nào nên – và không nên áp dụng cứng nhắc các nguyên tắc này.

1. Single Responsibility Principle (SRP) – Nguyên Tắc Đơn Trách Nhiệm

Định nghĩa: A class should have one, and only one, reason to change. (Một lớp chỉ nên có một và chỉ một lý do để thay đổi).

Lớp vi phạm SRP thường là các “God Object” hoặc các Service chứa hàng loạt logic không liên quan:

  • Vừa xử lý Business Logic, vừa thực hiện I/O Operations (Database, Network, File).
  • Vừa thực hiện Validation, vừa tính toán Data Transformation, vừa format UI/Response.
  • Dấu hiệu: OrderService có độ dài hơn 1,000 dòng code, import từ java.sql.*, javax.mail.* cho đến com.fasterxml.jackson.*.

Chúng ta cùng xem lớp OrderProcessor ôm đồm quá nhiều trách nhiệm:

public class OrderProcessor {

 

    public void processOrder(Order order) {

        // 1. Business Logic: Validate order

        if (order.getItems().isEmpty()) {

            throw new IllegalArgumentException(“Order must contain at least one item”);

        }

 

        // 2. Business Logic: Calculate total price

        double total = 0;

        for (OrderItem item : order.getItems()) {

            total += item.getPrice() * item.getQuantity();

        }

        order.setTotalAmount(total);

 

        // 3. Infrastructure: Save to Database

        try (Connection conn = DriverManager.getConnection(“jdbc:mysql://localhost:3306/shop”, “root”, “secret”)) {

            String sql = “INSERT INTO orders (id, total_amount) VALUES (?, ?)”;

            PreparedStatement pstmt = conn.prepareStatement(sql);

            pstmt.setString(1, order.getId());

            pstmt.setDouble(2, order.getTotalAmount());

            pstmt.executeUpdate();

        } catch (SQLException e) {

            e.printStackTrace();

        }

 

        // 4. Infrastructure: Send Email Notification

        String emailContent = “Your order “ + order.getId() + ” has been processed. Total: “ + total;

        // Logic gửi email qua SMTP server…

        System.out.println(“Sending email to “ + order.getCustomerEmail() + “: “ + emailContent);

    }

}

Nhìn vào lớp OrderProcessor: Khi muốn thay đổi công thức tính giá,hay  thay đổi Database/ORM, hoặc thay đổi nhà cung cấp dịch vụ Email (SendGrid/Mailgun) đều bắt buộc phải sửa lớp này.

Bây giờ chúng ta sẽ refactored code bằng cách phân tách trách nhiệm (Separation of Concerns).

Tách biệt các trách nhiệm thành các thành phần chuyên biệt:

// 1. Validator

public class OrderValidator {

    public void validate(Order order) {

        if (order.getItems() == null || order.getItems().isEmpty()) {

            throw new IllegalArgumentException(“Order must contain at least one item”);

        }

    }

}

 

// 2. Domain Service (Calculator)

public class OrderCalculator {

    public double calculateTotal(Order order) {

        return order.getItems().stream()

                .mapToDouble(item -> item.getPrice() * item.getQuantity())

                .sum();

    }

}

 

// 3. Repository (Persistence)

public interface OrderRepository {

    void save(Order order);

}

 

public class MySqlOrderRepository implements OrderRepository {

    @Override

    public void save(Order order) {

        // JDBC / JPA / Hibernate code ở đây

    }

}

 

// 4. Notification Service

public interface NotificationService {

    void sendOrderConfirmation(Order order);

}

 

public class EmailNotificationService implements NotificationService {

    @Override

    public void sendOrderConfirmation(Order order) {

        // Logic gửi Email ở đây

    }

}

 

// 5. High-level Orchestrator (Single Responsibility: Điều phối luồng)

public class OrderService {

    private final OrderValidator validator;

    private final OrderCalculator calculator;

    private final OrderRepository repository;

    private final NotificationService notificationService;

 

    public OrderService(OrderValidator validator, OrderCalculator calculator, 

                        OrderRepository repository, NotificationService notificationService) {

        this.validator = validator;

        this.calculator = calculator;

        this.repository = repository;

        this.notificationService = notificationService;

    }

 

    public void processOrder(Order order) {

        validator.validate(order);

        double total = calculator.calculateTotal(order);

        order.setTotalAmount(total);

        

        repository.save(order);

        notificationService.sendOrderConfirmation(order);

    }

}

Sau khi refactoring, code được tổ chức theo các tầng điển hình trong DDD (Domain-Driven Design):

  • OrderValidator & OrderCalculator → Domain Layer (Business Logic thuần túy).
  • OrderRepository → Infrastructure Layer (Persistence).
  • EmailNotificationService → Application Layer (Giao tiếp với bên ngoài).
  • OrderService → Application Layer (Application Service / Use Case).

Sự phân tách này không chỉ đảm bảo SRP mà còn tuân theo các nguyên tắc của Clean Architecture / Hexagonal Architecture, giúp việc thay thế cơ sở dữ liệu hay nhà cung cấp email trở nên dễ dàng mà không ảnh hưởng đến logic nghiệp vụ cốt lõi.

2. Open/Closed Principle (OCP) – Nguyên Tắc Đóng/Mở

Định nghĩa: Software entities should be open for extension, but closed for modification. (Các thực thể phần mềm nên mở rộng cho việc phát triển mới, nhưng đóng lại đối với việc chỉnh sửa mã nguồn sẵn có).

Việc nhận biết mã nguồn vi phạm nguyên tắc OCP thường dễ hơn như:

  • Sử dụng quá nhiều câu lệnh if-else hoặc switch-case phụ thuộc vào type/enum để quyết định hành vi.
  • Mỗi khi thêm một yêu cầu nghiệp vụ mới (ví dụ: thêm loại thanh toán mới, thêm loại discount mới), lập trình viên buộc phải sửa trực tiếp vào các hàm cũ.
  • Rủi ro: Việc sửa code cũ dễ làm gãy (break) các tính năng đang chạy ổn định.

Chúng ta cùng xem ví dụ sau: Sử dụng switch kiểm tra loại chiết khấu

public enum CustomerType {

    REGULAR, VIP, PREMIUM

}

 

public class DiscountCalculator {

    public double calculateDiscount(CustomerType type, double amount) {

        switch (type) {

            case REGULAR:

                return amount * 0.05; // Giảm 5%

            case VIP:

                return amount * 0.15; // Giảm 15%

            case PREMIUM:

                return amount * 0.25; // Giảm 25%

            default:

                throw new IllegalArgumentException(“Unknown type”);

        }

    }

}

Nhìn vào đoạn code trên thoạt nhìn không thấy có vấn đề gì tuy nhiên khi có thêm loại khách hàng mới như PARTNER hay EMPLOYEE, ta phải sửa enum CustomerType và mở lớp DiscountCalculator ra để thêm nhánh case mới, điều này sẽ ảnh hưởng đến tính logic của hệ thống khi sử dụng phương thức này. Để giải quyết việc này ta có thể triển khai một số cách sau

2.1 Cách 1 – Áp dụng Strategy Pattern

Chuyển đổi các nhánh switch thành các triển khai của một Interface chung.

// Interface cho chiến lược tính chiết khấu

public interface DiscountStrategy {

    double calculate(double amount);

}

 

// Triển khai cụ thể cho từng loại

public class RegularDiscountStrategy implements DiscountStrategy {

    @Override

    public double calculate(double amount) {

        return amount * 0.05;

    }

}

 

public class VipDiscountStrategy implements DiscountStrategy {

    @Override

    public double calculate(double amount) {

        return amount * 0.15;

    }

}

 

public class PremiumDiscountStrategy implements DiscountStrategy {

    @Override

    public double calculate(double amount) {

        return amount * 0.25;

    }

}

 

// Khi cần thêm loại chiết khấu mới, chỉ cần tạo Class mới triển khai Interface

public class PartnerDiscountStrategy implements DiscountStrategy {

    @Override

    public double calculate(double amount) {

        return amount * 0.30; // Giảm 30% cho đối tác

    }

}

 

// Context Class: Đóng hoàn toàn đối với sự thay đổi

public class DiscountContext {

    private final DiscountStrategy strategy;

 

    public DiscountContext(DiscountStrategy strategy) {

        this.strategy = strategy;

    }

 

    public double executeDiscount(double amount) {

        return strategy.calculate(amount);

    }

}

2.2 Cách 2 – Sử dụng Enum với abstract method

Nếu số lượng loại chiết khấu ít và ít thay đổi, ta có thể tận dụng sức mạnh của enum trong Java để đạt OCP một cách đơn giản hơn:

public enum CustomerType {

    REGULAR {

        @Override

        public double applyDiscount(double amount) {

            return amount * 0.05;

        }

    },

    VIP {

        @Override

        public double applyDiscount(double amount) {

            return amount * 0.15;

        }

    },

    PREMIUM {

        @Override

        public double applyDiscount(double amount) {

            return amount * 0.25;

        }

    };

 

    // Abstract method bắt buộc mỗi hằng số phải implement

    public abstract double applyDiscount(double amount);

}

 

// Sử dụng: vẫn đảm bảo OCP – khi thêm loại mới, không sửa code cũ

double discountedAmount = customerType.applyDiscount(originalAmount);

Lựa chọn giữa hai cách: Enum với abstract method cực kỳ ngắn gọn và phù hợp khi logic chiết khấu đơn giản, không cần dependency bên ngoài. Strategy Pattern linh hoạt hơn, cho phép inject các service phức tạp (ví dụ: gọi API tính thuế, lấy tỷ giá hối đoái) và dễ dàng mock trong unit test. Tùy vào độ phức tạp của nghiệp vụ mà chọn cách phù hợp.

3. Liskov Substitution Principle (LSP) – Nguyên Tắc Thay Thế Liskov

Định nghĩa: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. (Các đối tượng của lớp con phải có thể thay thế hoàn toàn cho các đối tượng của lớp cha mà không làm thay đổi tính đúng đắn của chương trình).

Mã nguồn vi phạm nguyên tắc LSP có một số đặc điểm sau:

  • Lớp con ghi đè (override) phương thức của lớp cha nhưng lại ném ra UnsupportedOperationException.
  • Phương thức ở lớp con để trống (empty implementation) vì không sử dụng đến.
  • Lập trình viên phải dùng instanceof để kiểm tra kiểu của lớp con trước khi gọi phương thức – đây là một red flag cực lớn, cho thấy thiết kế kế thừa không phản ánh đúng quan hệ “is-a”.

Hãy xem và phân tích mã nguồn sau:

public class File {

    public void read() {

        System.out.println(“Reading file data…”);

    }

 

    public void write(byte[] data) {

        System.out.println(“Writing data to file…”);

    }

}

 

public class ReadOnlyFile extends File {

    @Override

    public void write(byte[] data) {

        // Vi phạm LSP: Ném ngoại lệ vì file chỉ đọc không thể ghi!

        throw new UnsupportedOperationException(“Cannot write to a read-only file!”);

    }

}

 

// Client Code bị gãy khi thay thế File bằng ReadOnlyFile

public class FileManager {

    public void updateFiles(List<File> files, byte[] newData) {

        for (File file : files) {

            file.write(newData); // Crash với UnsupportedOperationException nếu có ReadOnlyFile

        }

    }

}

Vấn đề:

  • Lớp ReadOnlyFile kế thừa File và ghi đè phương thức write để ném ngoại lệ.
  • Trong FileManager, phương thức updateFiles nhận một danh sách các File và gọi write lên từng đối tượng.
  • Nếu danh sách chứa một thể hiện của ReadOnlyFile, chương trình sẽ phát sinh UnsupportedOperationException và bị dừng (crash).

Để tuân thủ LSP, chúng ta cần thiết kế lại hệ thống sao cho các kiểu dữ liệu phản ánh đúng khả năng thực tế của từng loại file.

Tái cấu trúc lại phân cấp kế thừa (Class Hierarchy) dựa trên đúng khả năng của đối tượng:

public interface Readable {

    void read();

}

 

public interface Writable {

    void write(byte[] data);

}

 

// File chỉ đọc triển khai Readable

public class ReadOnlyFile implements Readable {

    @Override

    public void read() {

        System.out.println(“Reading read-only file data…”);

    }

}

 

// File có thể ghi triển khai cả Readable và Writable

public class WritableFile implements Readable, Writable {

    @Override

    public void read() {

        System.out.println(“Reading file data…”);

    }

 

    @Override

    public void write(byte[] data) {

        System.out.println(“Writing data to file…”);

    }

}

 

// Client Code sử dụng Interface chính xác, đảm bảo không bị gãy

public class FileManager {

    public void updateWritableFiles(List<Writable> files, byte[] newData) {

        for (Writable file : files) {

            file.write(newData); // Đảm bảo mọi đối tượng Writable đều thực thi được write()

        }

    }

}

Bài học quan trọng: Khi bạn thấy mình dùng instanceof hoặc ép kiểu (cast) để xử lý riêng biệt cho từng lớp con, đó là dấu hiệu chắc chắn LSP đang bị vi phạm. Giải pháp triệt để là sử dụng Interface nhỏ gọn hơn thay vì cố gắng “nhồi nhét” mọi thứ vào một lớp cha chung.

4. Interface Segregation Principle (ISP) – Nguyên Tắc Phân Tách Interface

Định nghĩa: Clients should not be forced to depend upon interfaces that they do not use. (Không nên ép buộc một client phụ thuộc vào các interface mà nó không sử dụng).

Đặc điểm nhận dạng mã nguồn vi phạm nguyên tắc này:

  • Xuất hiện các “Fat Interfaces” (Interface béo) chứa hàng chục phương thức không cùng nhóm chức năng.
  • Các class triển khai (implement) interface phải để trống phương thức hoặc trả về null / ném ngoại lệ vì tính năng đó không áp dụng cho chúng.

Cùng quan sát mã nguồn vi phạm ISP sau

public interface SmartDevice {

    void print();

    void scan();

    void fax();

}

 

public class EconomicPrinter implements SmartDevice {

    @Override

    public void print() {

        System.out.println(“Printing document…”);

    }

 

    @Override

    public void scan() {

        // Máy in giá rẻ không có chức năng Scan!

        throw new UnsupportedOperationException(“Scan not supported”);

    }

 

    @Override

    public void fax() {

        // Máy in giá rẻ không có chức năng Fax!

        throw new UnsupportedOperationException(“Fax not supported”);

    }

}

  1. Interface quá lớn (Fat Interface): SmartDevice gộp 3 chức năng hoàn toàn khác biệt (in, scan, fax) vào cùng một hợp đồng.
  2. Lớp triển khai bị ép buộc: EconomicPrinter (máy in giá rẻ) chỉ hỗ trợ chức năng in. Nhưng vì phải implement SmartDevice, nó buộc phải cung cấp thân cho scan()fax() – dù không thể thực hiện được.
  3. Hậu quả nghiêm trọng:
    • Xuất hiện ngoại lệ UnsupportedOperationException khi client gọi phương thức không được hỗ trợ → ứng dụng dễ bị crash ở runtime.
    • Code trở nên không an toàn về mặt kiểu (type safety) – không thể biết được đối tượng cụ thể có hỗ trợ chức năng gì nếu chỉ nhìn vào interface.
    • Việc kiểm tra phải dùng instanceof hoặc try-catch, gây phức tạp và dễ sai.

Chúng ta sẽ xử lý bằng cách tách thành các Interface nhỏ

Tách SmartDevice thành các Interface đơn trách nhiệm dựa trên vai trò chức năng:

public interface Printer {

    void print();

}

 

public interface Scanner {

    void scan();

}

 

public interface Fax {

    void fax();

}

 

// Máy in đơn năng chỉ implement Printer

public class EconomicPrinter implements Printer {

    @Override

    public void print() {

        System.out.println(“Printing document…”);

    }

}

 

// Máy in đa năng implement tất cả các Interface cần thiết

public class AllInOnePrinter implements Printer, Scanner, Fax {

    @Override

    public void print() {

        System.out.println(“All-in-one printing…”);

    }

 

    @Override

    public void scan() {

        System.out.println(“All-in-one scanning…”);

    }

 

    @Override

    public void fax() {

        System.out.println(“All-in-one faxing…”);

    }

}

Liên hệ với thư viện Java thực tế: Nguyên tắc này đã được áp dụng rộng rãi trong các thư viện chuẩn của Java:

  • java.sql tách thành ResultSet, PreparedStatement, CallableStatement thay vì một Statement đa năng.
  • Spring Framework tách thành InitializingBean, DisposableBean, BeanNameAware… thay vì một BeanLifecycle khổng lồ.
  • java.utilList, Set, Queue, Map riêng biệt thay vì một Collection duy nhất chứa tất cả.

5. Dependency Inversion Principle (DIP) – Nguyên Tắc Đảo Ngược Phụ Thuộc

5.1 Định nghĩa:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions. (Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai nên phụ thuộc vào sự trừu tượng).
  2. Abstractions should not depend on details. Details should depend on abstractions. (Sự trừu tượng không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào sự trừu tượng).

Dấu hiệu nhận biết mã nguồn vi phạm:

  • Sử dụng toán tử new để khởi tạo trực tiếp các dependency cấp thấp bên trong module cấp cao (Tight Coupling).
  • Khó viết Unit Test (Mocking/Stubbing) vì module cấp cao gắn chặt với triển khai thực tế (như MySQL Database, AWS S3,…).

Ví dụ lớp Car phụ thuộc trực tiếp vào GasolineEngine

// Low-level Module

public class GasolineEngine {

    public void start() {

        System.out.println(“Gasoline engine starting…”);

    }

}

 

// High-level Module

public class Car {

    private GasolineEngine engine; // Khởi tạo cứng (Hard dependency)

 

    public Car() {

        this.engine = new GasolineEngine(); // Khóa chặt Car với GasolineEngine!

    }

 

    public void startCar() {

        engine.start();

    }

}

Vấn đề: Nếu muốn tạo ElectricCar hoặc thay thế bằng DieselEngine, ta phải sửa mã nguồn của lớp Car. Ngoài ra, không thể mock GasolineEngine khi viết Unit Test cho Car.

Chúng ta có thể xử lý bằng cách đảo ngược phụ thuộc qua Abstraction & Dependency Injection

Giới thiệu một Abstraction (Engine interface) nằm giữa Car và các triển khai Động cơ cụ thể:

// Abstraction (Interface)

public interface Engine {

    void start();

}

 

// Low-level Module 1

public class GasolineEngine implements Engine {

    @Override

    public void start() {

        System.out.println(“Gasoline engine starting…”);

    }

}

 

// Low-level Module 2

public class ElectricEngine implements Engine {

    @Override

    public void start() {

        System.out.println(“Electric engine starting quietly…”);

    }

}

 

// High-level Module chỉ phụ thuộc vào Abstraction

public class Car {

    private final Engine engine;

 

    // Constructor Injection (Dependency Injection qua constructor)

    public Car(Engine engine) {

        this.engine = engine;

    }

 

    public void startCar() {

        engine.start();

    }

}

 

// Demo khởi tạo và inject phụ thuộc

public class Main {

    public static void main(String[] args) {

        Car gasCar = new Car(new GasolineEngine());

        gasCar.startCar();

 

        Car electricCar = new Car(new ElectricEngine());

        electricCar.startCar();

    }

}

5. 2 Các hình thức Dependency Injection:

  • Constructor Injection (như ví dụ trên) là phương pháp được khuyến khích nhất vì:
    • Tạo immutable object (dùng final).
    • Dễ dàng test (không cần framework, chỉ cần truyền mock vào constructor).
    • Tránh NullPointerException vì dependency được inject ngay khi khởi tạo.
  • Setter Injection (qua setter method) hữu ích khi dependency là tùy chọn (optional).
  • Field Injection (dùng @Autowired trong Spring) tiện lợi nhưng khiến class khó test nếu không có framework, và khó nhìn thấy các dependency. Trong các dự án lớn, Constructor Injection vẫn được ưu tiên hơn cả.

6. Khi nào KHÔNG NÊN áp dụng SOLID?

SOLID là kim chỉ nam, không phải luật bất di bất dịch. Áp dụng cứng nhắc SOLID trong mọi ngữ cảnh có thể dẫn đến over-engineering và làm chậm tiến độ dự án. Dưới đây là các tình huống bạn nên cân nhắc:

Tình huống Lời khuyên
Script / Utility nhỏ (< 200 dòng code) Đừng tạo ra 10 class, factory, strategy cho một công cụ nhỏ. Code đơn giản, dễ đọc quan trọng hơn.
Ứng dụng CLI / Batch Job đơn giản Một batch job chạy 3 phút mỗi ngày không cần kiến trúc hexagon. Một class với 3 method là đủ.
Prototype / MVP (Minimum Viable Product) Giai đoạn này, tốc độ ra thị trường là ưu tiên số một. Hãy code “dirty” để nhanh chóng validate ý tưởng, và chỉ refactor sau khi sản phẩm được chấp nhận.
Codebase có ít người bảo trì (< 3 người) Với team nhỏ, việc tạo quá nhiều abstraction có thể gây khó hiểu. Cân bằng giữa đơn giản và mở rộng là chìa khóa.

Nguyên tắc vàng: Áp dụng SOLID một cách có chọn lọc dựa trên:

  • Tuổi thọ dự án (Project Lifespan) – càng dài thì càng cần thiết.
  • Số lượng người tham gia (Team Size) – càng đông càng cần rõ ràng, tách biệt.
  • Tần suất thay đổi yêu cầu (Requirement Volatility) – càng thay đổi nhiều, càng cần OCP và DIP.

Kết hợp tất cả nguyên tắc: Use Case “Hệ thống thanh toán đa kênh”

Để thấy rõ sức mạnh của SOLID khi phối hợp, hãy xem một hệ thống xử lý thanh toán (Payment Gateway) được thiết kế đúng nguyên tắc:

PaymentController (Adapter)  →  chỉ nhận request HTTP, gọi use case

        │

        ▼

PaymentService (Application Service – SRP: chỉ điều phối)

        │

        ├── PaymentValidator (SRP: validate dữ liệu)

        ├── PaymentCalculator (SRP: tính phí, thuế)

        ├── PaymentProcessor (OCP: Strategy Pattern – VNPay, Momo, PayPal)

        ├── PaymentRepository (DIP: interface cho DB)

        └── NotificationDispatcher (ISP: chỉ gửi thông báo, có interface riêng cho Email, SMS, Push)

Khi cần thêm cổng thanh toán mới (ví dụ ZaloPay):

  • Ta không sửa PaymentService, PaymentValidator, PaymentRepository, NotificationDispatcher.
  • Ta chỉ thêm một class mới implement PaymentProcessor – đúng với OCP.
  • Mọi dependency đều được inject qua constructor (DIP).
  • Unit test dễ dàng mock từng thành phần, đảm bảo độ phủ cao.

7. Tổng kết

Áp dụng thành thạo SOLID không chỉ giúp codebase Java sạch sẽ, dễ bảo trì hơn mà còn nâng cao đáng kể độ phủ unit test và khả năng mở rộng của hệ thống trước những thay đổi liên tục của nghiệp vụ. Tuy nhiên, cần nhớ rằng SOLID là công cụ hỗ trợ, không phải mục đích cuối cùng – việc áp dụng cứng nhắc tất cả năm nguyên tắc trong mọi ngữ cảnh có thể dẫn đến over-engineering, đặc biệt với các dự án nhỏ, prototype hay script đơn giản.

Thay vào đó, hãy linh hoạt dựa trên tuổi thọ dự án, quy mô nhóm và tần suất thay đổi yêu cầu. Hãy bắt đầu refactoring từ những dấu hiệu nhỏ nhất như class quá dài, switch-case rải rác hay interface “béo”, và từng bước xây dựng một hệ thống bền vững. Quan trọng nhất, hãy nhớ rằng SOLID là hành trình, không phải đích đến – mỗi lần cải thiện một chút đều đưa bạn đến gần hơn với một phần mềm chất lượng, dễ bảo trì và thích ứng nhanh với thực tế kinh doanh.

Tags:

0 Lời bình

Gửi Lời bình

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

BÀI VIẾT LIÊN QUAN

BẠN MUỐN HỌC LẬP TRÌNH?

GỌI NGAY

098 953 44 58

Đăng ký tư vấn lộ trình học lập trình

Đăng ký tư vấn, định hướng lộ trình học và giải đáp các thắc mắc về ngành nghề – Miễn phí – Online.

12 + 11 =