Mình muốn chia sẻ đến mọi người những thói quen tốt trong ngành phát triển phần mềm mà những bạn Engineering cả mới, cả cũ nên biết để có thể nâng cao kĩ năng của bản thân, định vị được giá trị, sự khác biệt của mình trên thị trường.

Cố gắng commit có ý nghĩa rõ ràng

Cố gắng ghi những nội dung rõ ràng, dễ hiểu, kiểu dạng như bên dưới là một trong những thói quen của mình, nếu các bạn làm việc trong dự án có sử dụng Jira thì hãy cố gắng điền cả ticket jira vào để dễ tìm kiếm lại sau này.

Mình còn nhớ hồi còn làm việc ở One Mount, tiêu chuẩn trong việc đặt tên branch, nội dung commit message có ticket jira là bắt buộc.

Dần dần việc này trở thành thói quen với mình, về chi tiết các bạn có thể xem lại bài viết Git flow ở công ty nghìn người sẽ như thế nào mình viết hồi trước về quy trình làm việc với git.

Code xong thì nhớ self test trước khi bàn giao

Điều quan trọng thì nên nhắc lại nhiều lần, để tránh việc bị QC/QA complain nhiều thì hãy cố gắng đảm bảo code được Unit Test, Integration Test, hoặc ít nhất là phải self test đủ các trường hợp cần thiết.

Tuyệt đối không nên làm kiểu code xong mà không test gì, sau đó đẩy lên DEV và UAT rồi bỏ đi chơi, quay lại thì mới nhận ra là CI/CD bị lỗi, code không deploy lên được, hoặc code bị dính nhiều bug ảnh hưởng đến người khác đang phát triển trên cùng nhánh.

Mình từng gặp nhiều trường hợp rất buồn cười, đồng nghiệp code xong không hề test lại, làm ảnh hưởng đến luồng chính do mình phát triển, thành ra QC kêu API mình viết bị lỗi, trong khi root cause không phải do mình gây ra.

thẳng thắn với đồng nghiệp về việc họ không self test nhưng cứ đẩy code lên

Các bạn kĩ sư phần mềm kể cả ít kinh nghiệm, hoặc nhiều kinh nghiệm cũng nên rút kinh nghiệm cho bản thân mình nhé, cứ cố gắng đảm bảo code của mình không làm ảnh hưởng đến code của người khác.

Nếu có thay đổi hãy bổ sung thêm test để luôn đảm bảo coverage được những đoạn logic khó, những đoạn logic “magic”, lúc đó thì chẳng ai nói được gì các bạn.

490 test/service

Hồi trước, có những service mình và các thành viên trong team cùng viết hơn 490 cái test/1 service để coverage code, mỗi người nắm khoảng 5, 6 cái service trong hệ thống.

Mục tiêu để coverage cả những phần code cũ và những phần code mới, kể cả những code không phải do mình viết ra cũng phải viết test đủ, năm đó tỉ lệ code coverage của toàn bộ hệ thống đạt hơn 80%.

Sau khi mình sang công ty mới thì ở đó không hề có 1 cái Integration Test hay Unit Test nào trong hệ thống, anh em dev chủ yếu là chỉ tự self test dưới local, xong rồi selt test trên hai môi trường phát triển (DEV, UAT), có người đẩy code lên còn chả thèm test gì nhưng vẫn báo QC check, khi xuất hiện nhiều bug thì lại bị QC claim rồi log bug tùm lum.

Mặc dù hệ thống cũng khá to, số lượng user lên đến 3 triệu khách hàng, phần lớn là khách hàng dùng tài khoản đi xe qua trạm.

Lúc đó mình đã bắt đầu tự xây dựng base những đoạn code Unit Test, Integration Test đầu tiên cho hệ thống core, đem lại thêm chất lượng cho những dòng code của bản thân.

code struct base for integration test

Luôn giữ mindset về tính tái sử dụng (Reusability Mindset)

Khi phát triển các tính năng hoặc module, hãy nghĩ đến khả năng tái sử dụng trong các dự án khác hoặc trong các phần khác của hệ thống.

Tạo các thư viện nội bộ, viết code có tính module hóa, và áp dụng nguyên tắc “write once, use anywhere” giúp giảm thiểu sự lặp lại và dễ dàng mở rộng khi cần thiết.

Xác định các đoạn code lặp lại và loại bỏ chúng

Trong quá trình phát triển, hãy thường xuyên rà soát và phát hiện những đoạn code trùng lặp, đây có thể là các logic kiểm tra điều kiện, xử lý chuỗi, hoặc thậm chí là giao tiếp với cơ sở dữ liệu.

Hãy trích xuất chúng thành các hàm hoặc class chung để sử dụng được ở nhiều nơi.

Ví dụ trước khi áp dụng nguyên tắc DRY (Code lặp lại trong xử lý cơ sở dữ liệu):

public class ProductService {

    public Product getProductById(int id) throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
        PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE id = ?");
        statement.setInt(1, id);
        ResultSet resultSet = statement.executeQuery();

        Product product = null;
        if (resultSet.next()) {
            product = new Product(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getDouble("price"));
        }

        resultSet.close();
        statement.close();
        connection.close();
        return product;
    }

    public List<Product> getAllProducts() throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
        PreparedStatement statement = connection.prepareStatement("SELECT * FROM products");
        ResultSet resultSet = statement.executeQuery();

        List<Product> products = new ArrayList<>();
        while (resultSet.next()) {
            products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getDouble("price")));
        }

        resultSet.close();
        statement.close();
        connection.close();
        return products;
    }
}

Sau khi áp dụng nguyên tắc DRY:

public class DatabaseUtil {

    private static final String DB_URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String DB_USER = "user";
    private static final String DB_PASSWORD = "password";

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
    }

    public static void closeResources(ResultSet resultSet, PreparedStatement statement, Connection connection) {
        try {
            if (resultSet != null) resultSet.close();
            if (statement != null) statement.close();
            if (connection != null) connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

public class ProductService {

    public Product getProductById(int id) throws SQLException {
        String query = "SELECT * FROM products WHERE id = ?";
        try (
            Connection connection = DatabaseUtil.getConnection();
            PreparedStatement statement = connection.prepareStatement(query)
        ) {
            statement.setInt(1, id);
            try (ResultSet resultSet = statement.executeQuery()) {
                if (resultSet.next()) {
                    return new Product(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getDouble("price"));
                }
            }
        }
        return null;
    }

    public List<Product> getAllProducts() throws SQLException {
        String query = "SELECT * FROM products";
        try (
            Connection connection = DatabaseUtil.getConnection();
            PreparedStatement statement = connection.prepareStatement(query);
            ResultSet resultSet = statement.executeQuery()
        ) {
            List<Product> products = new ArrayList<>();
            while (resultSet.next()) {
                products.add(new Product(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getDouble("price")));
            }
            return products;
        }
    }
}

Lợi ích:

  1. Tái sử dụng hàm getConnection: Logic kết nối với cơ sở dữ liệu được viết một lần duy nhất.
  2. Quản lý tài nguyên tốt hơn: Hàm closeResources đảm bảo đóng đúng thứ tự và giảm nguy cơ rò rỉ tài nguyên.
  3. Mã gọn hơn: Các đoạn code xử lý kết nối không còn xuất hiện lặp lại trong từng phương thức.

Trích xuất logic dùng chung

Với các đoạn logic phức tạp xuất hiện trong nhiều module, bạn có thể trích xuất thành một service hoặc utility class. Ví dụ, nếu bạn thường xuyên cần mã hóa dữ liệu, hãy tạo một class EncryptionUtil thay vì lặp lại logic mã hóa ở mỗi nơi.

Trước khi áp dụng nguyên tắc DRY (Code mã hóa bị lặp lại trong nhiều service khác nhau):

public class UserService {

    public String encryptPassword(String password) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            hexString.append(String.format("%02x", b));
        }
        return hexString.toString();
    }

    public void saveUser(String username, String password) throws Exception {
        String encryptedPassword = encryptPassword(password);
        // Save username and encryptedPassword to database
        System.out.println("User saved: " + username + " with encrypted password: " + encryptedPassword);
    }
}

public class PaymentService {

    public String encryptTransactionId(String transactionId) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(transactionId.getBytes(StandardCharsets.UTF_8));
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            hexString.append(String.format("%02x", b));
        }
        return hexString.toString();
    }

    public void processPayment(String transactionId) throws Exception {
        String encryptedTransactionId = encryptTransactionId(transactionId);
        // Process payment with encryptedTransactionId
        System.out.println("Payment processed with encrypted transaction ID: " + encryptedTransactionId);
    }
}

Sau khi áp dụng nguyên tắc DRY (Tách logic mã hóa vào EncryptionUtil):

public class EncryptionUtil {

    public static String encrypt(String input) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            hexString.append(String.format("%02x", b));
        }
        return hexString.toString();
    }
}

public class UserService {

    public void saveUser(String username, String password) throws Exception {
        String encryptedPassword = EncryptionUtil.encrypt(password);
        // Save username and encryptedPassword to database
        System.out.println("User saved: " + username + " with encrypted password: " + encryptedPassword);
    }
}

public class PaymentService {

    public void processPayment(String transactionId) throws Exception {
        String encryptedTransactionId = EncryptionUtil.encrypt(transactionId);
        // Process payment with encryptedTransactionId
        System.out.println("Payment processed with encrypted transaction ID: " + encryptedTransactionId);
    }
}

Lợi ích của việc trích xuất:

  1. Loại bỏ sự trùng lặp: Logic mã hóa SHA-256 chỉ cần viết một lần trong EncryptionUtil.
  2. Tăng tính tái sử dụng: Các module khác có thể dùng chung phương thức encrypt mà không cần tự triển khai lại.
  3. Dễ bảo trì: Nếu cần thay đổi thuật toán mã hóa, chỉ cần sửa trong EncryptionUtil.
  4. Code gọn gàng hơn: Các service (UserService, PaymentService) tập trung vào logic nghiệp vụ chính

Tổ chức lại cấu trúc của dự án

Chia dự án thành các module có tính năng rõ ràng và không bị phụ thuộc chéo. Một module có thể được tái sử dụng bởi các module khác mà không cần sửa đổi.

1 trong số những dự án theo hướng Domain Driven Design mình dựng

Cấu trúc source code này thì tuỳ dự án, tuỳ công ty, có công ty chỉ làm theo MVC thuần, có công ty lại làm theo DDD, nhưng dù là theo kiến trúc nào thì cấu trúc source code cũng cần phải phân chia rành mạch và rõ ràng.

Viết tài liệu cẩn thận, rõ ràng, dễ hiểu

Việc ghi chú và viết tài liệu kỹ càng giúp các thành viên khác trong nhóm dễ dàng nắm bắt, cũng như giúp chính bạn khi cần xem lại mã sau này. Tài liệu có thể bao gồm README chi tiết, chú thích trong code, hoặc các tài liệu API đầy đủ.

Ngoài việc bản thân phải viết tài liệu rõ ràng dễ hiểu, thì chính bản thân các bạn cũng phải học cách đọc docs của người khác một cách cẩn thận, nếu có sai sót thì cần báo cáo hoặc phản hồi ngay.

Ví dụ có hai team cùng làm việc với nhau, team A gửi trước docs api để team B tích hợp trước, nhưng engineer của team B lại không đọc docs kĩ mà cứ đi hỏi tùm lum tứ tung những thứ đã được định nghĩa trong docs, còn cái ông viết docs api team A thì mỗi lúc lại sửa một kiểu, thành ra công việc cứ bị rối mà nó không trôi tí nào.

Có Docs API rồi, cần phải test thử nghiệm trước khi code

Thường khi làm tích hợp với một số bên, mình hay có thói quen trước khi code sẽ test liên tục để tìm lỗi trên API của đối tác, mục đích là để kiểm thử chức năng xem có đáp ứng được nhu cầu trước hay không.

Cứ truyền đủ loại request, param, value các thứ random vào để test, nếu có lỗi hoặc case lạ thì phản hồi lại sớm cho bên kia để họ sửa hoặc cập nhật, sau đó lưu lại một bộ request/response ở trong Postman cho những trường hợp cơ bản: thành công, thất bại, timeout, rate limit, bad request,…

Rồi cuối cùng mới tiến hành quá trình code, test, deploy, sit sau đó.

Nếu các bạn làm việc với những team lớn và chuẩn thì cả hai team chỉ cần thống nhất được tài liệu, ngày SIT, UAT với nhau, sau đó cả hai team cứ tự chủ động phát triển mà không cần họp mỗi ngày với nhau, có vấn đề thì raise lên để hai team cùng nắm và trao đổi.

Áp dụng “Backwards Compatibility” trong thay đổi API

Điều này gần như là cơ bản trong quá trình phát triển API, bởi vì business thì cứ thay đổi mỗi ngày, API sẽ cần phải đáp ứng được nghiệp vụ.

Khi cập nhật các API hoặc thay đổi cấu trúc dữ liệu, chúng ta cần cân nhắc giữ cho chúng tương thích ngược để không gây gián đoạn cho người dùng hoặc các hệ thống tích hợp khác.

Không thể nào mà lại đi sửa API cũ và làm ảnh hưởng tới toàn bộ client đã được tích hợp trước đó được, tuyệt đối nên hạn chế điều này.

Có thể cân nhắc cung cấp phiên bản mới song song với phiên bản cũ hoặc duy trì hỗ trợ cho định dạng cũ trong một khoảng thời gian nhất định trước khi bắt buộc client phải nâng cấp lên phiên bản mới.

Mình nhớ đâu đó trước tải một cái app ngân hàng số, xin phép không nhắc tên, mới tải được 1 ngày, xem release note của họ thì thấy là 2 ngày trước, đến hôm sau mở app ra dùng thì lại thấy bắt force app để tải phiên bản mới, mình viết đánh giá và từ đó không dùng cái app đó nữa.

Khi phát hiện lỗi thì nên báo ngay để sửa chữa kịp thời

Khi phát hiện được lỗi trong code, kể cả là code đó của bạn hay không phải do bạn viết ra, cũng cần báo cáo lại để có phương án xử lý sửa chữa kịp thời luôn.

1. Phát hiện thiếu code khi rebase từ master về

Trong lúc rebase code từ master về nhánh feature, mình phát hiện ra bị thiếu mất một đoạn code xử lý, sau đó thực hiện bổ sung và re-produce cùng QC trước khi sửa.

2. Phát hiện lỗi gửi auto gửi OTP SMS

Ảnh 2 là từ một lần khác, code logic của người cũ không check phương thức xác thực là OTP nhưng vẫn thực hiện gửi OTP SMS tới khách hàng, như kiểu phương thức xác thực là PASSWORD hoặc BIOMETRIC nhưng vẫn gửi OTP SMS, gây ra rủi ro thất thoát tiền rất lớn.

Phát hiện này của mình giúp công ty xử lý được việc gửi thừa OTP SMS cho khách hàng khi xác thực giao dịch, đỡ tốn bao nhiêu tiền tin nhắn.

3. Phát hiện QC miss test case trong lúc phát triển tính năng mới

Ảnh 3 là khi mình phát hiện ra QC bị miss test case của một luồng thanh toán, do
QC không test qua đường API, mà chỉ check thẳng từ luồng trên mobile app gọi xuống, thành ra mobile app gọi khác api nhưng kết quả vẫn đúng, dẫn tới bị miss test case. Sau khi phát hiện ra thì mình tái hiện tại cùng QC và bắt bug QC luôn.

Nói chung là không quan trọng lỗi là do bạn hay do người khác gây ra, bất cứ khi nào phát hiện thì cần phải có phương án xử lý kịp thời để phòng tránh, ngăn ngừa những hậu quả nặng nề về sau.

Không nên thiết kế API kiểu ngoài vỏ 200, trong ruột 400, 500,…

Theo ý kiến cá nhân mình, thì việc nếu có lỗi gì đó trong quá trình phát triển API thì nên trả ra http status tương ứng với trạng thái lỗi luôn, ví dụ 401, 404, 400,… chứ không nên trả ra kiểu http 200 nhưng bên trong thì có khi lại mã code 400, 500 các kiểu, như thế thì phía client hoặc bên tích hợp sẽ rất khó handle lỗi.

Hiện tại mình có đẩy các bài viết lên trên Medium và Substack, các bạn quan tâm có thể chia sẻ cho bạn bè, nhấn follow, subscriber hoặc đơn giản chỉ cần bấm thả tim, vỗ tay thôi là mình có động lực viết những bài viết mới rồi, mình xin cảm ơn.

medium profile

Xem thêm các bài viết nổi bật ở bên dưới: