0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Mẹo Biến Compiler Thành Trợ Lý Lập Trình An Toàn

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

• 6 phút đọc

Chủ đề:

KungFuTech

Giới thiệu

Trong bài viết trước, chúng ta đã thoát khỏi những cạm bẫy kiến trúc của đối tượng "Junk Drawer" bằng cách biến một lớp class lộn xộn, dễ lỗi thành một lớp sealed class gọn gàng, chính xác. Đây là công cụ tùy chỉnh của chúng ta, nơi mà mọi trạng thái đều có vị trí hoàn hảo của nó.

Đó là một bước tiến lớn, nhưng sức mạnh thực sự, sự thay đổi mô hình thực sự, đến khi chúng ta đặt câu hỏi cho công cụ này. Thực sự, đây là lý do tôi rất đam mê tính năng này. Đó là khoảnh khắc mà biên dịch viên Kotlin chuyển từ một trình dịch đơn giản thành một trợ lý lập trình viên đáng tin cậy, làm việc 24/7.

Lỗi Im Lặng Mà Tôi Đã Từng Gặp

Hãy nói về một loại lỗi im lặng, tinh vi. Loại lỗi không gây ra sự cố ngay lập tức, mà chỉ khiến ứng dụng của bạn hoạt động sai cách, một cách âm thầm, cho đến khi người dùng phàn nàn.

Trước khi có các lớp sealed, tôi đã xử lý các trạng thái khác nhau bằng cách sử dụng câu lệnh when và một nhánh "mạng lưới an toàn" else. Giả sử chúng ta có một enum cho trạng thái đăng ký của người dùng.

kotlin Copy
enum class Status { FREE, PREMIUM, PRO }

fun showFeature(status: Status) {
    when (status) {
        Status.PREMIUM -> showPremiumFeature()
        Status.PRO -> showProFeature()
        else -> showFreeUpsell() // Đoạn "catch-all"
    }
}

Điều này có vẻ ổn, đúng không? Nhánh else xử lý trường hợp FREE. Nhưng sau sáu tháng, một yêu cầu mới được đưa ra. Chúng ta thêm một hạng mục Status.ENTERPRISE.

Một lập trình viên thêm nó vào enum, nhưng họ quên cập nhật hàm cụ thể showFeature. Điều gì xảy ra? Không có gì. Mã vẫn biên dịch. Tại thời điểm chạy, ENTERPRISE một cách im lặng rơi vào nhánh else, và những khách hàng doanh nghiệp quý giá nhất của chúng ta được hiển thị một upsell cho phiên bản miễn phí. Điều này thật đáng xấu hổ, và đó là một lỗi mà QA có thể không phát hiện.

Nhánh else không phải là một mạng lưới an toàn; nó là một bãi rác lỗi. Đây là nơi mà các trạng thái không được xử lý rơi vào và bị lãng quên.

Phép Thuật: Tính Exhaustiveness và Smart Casting

Bây giờ, hãy xem cách mà DeliveryResult sealed mà chúng ta đã đề cập trước đó hoàn toàn tránh được cạm bẫy này.

kotlin Copy
sealed class DeliveryResult {
    object Preparing : DeliveryResult()
    data class Dispatched(val trackingId: String) : DeliveryResult()
    data class Delivered(val receiversName: String) : DeliveryResult()
}

Khi chúng ta sử dụng when trên một thể hiện của lớp sealed, một điều thần kỳ xảy ra.

kotlin Copy
fun getStatusMessage(result: DeliveryResult) {
    when (result) {
        is DeliveryResult.Preparing -> {
            println("Gói hàng của bạn đang được chuẩn bị.")
        }
        is DeliveryResult.Dispatched -> {
            // Phép thuật #1: `result` giờ là kiểu Dispatched, không cần ép kiểu!
            println("Đã gửi! Theo dõi: ${result.trackingId}") 
        }
        is DeliveryResult.Delivered -> {
            // Phép thuật #1 một lần nữa: `result` là kiểu Delivered ở đây!
            println("Được ký bởi ${result.receiversName}.")
        }
    }
    // Phép thuật #2: Ở đâu có nhánh `else`? Chúng ta không cần nó!
}

Đây là cốt lõi của mẹo phép thuật.

  1. Smart Casting: Bên trong mỗi nhánh, biên dịch viên thông minh đủ để biết chính xác loại phụ mà bạn đang làm việc. Nó tự động và an toàn ép kiểu result cho bạn, vì vậy bạn có thể truy cập các thuộc tính như trackingId mà không cần làm thêm bất kỳ công việc nào.
  2. Exhaustiveness: Biên dịch viên thấy danh sách VIP của lớp sealed và xác minh rằng bạn đã xử lý mọi khả năng. Bởi vì bạn đã, nó không yêu cầu một nhánh else. Đây không chỉ là một sự tiện lợi; nó là một tuyên bố mạnh mẽ rằng logic của bạn là hoàn chỉnh.

Khoảnh Khắc Vĩ Đại: Ngắt Lặp Có Chủ Đích

Được rồi, mã nguồn đã sạch sẽ. Nhưng đây là khoảnh khắc vĩ đại. Đây là khoảnh khắc tách biệt mã tốt với mã thực sự kiên cường.

Quản lý sản phẩm của chúng ta nói rằng chúng ta cần một trạng thái mới: InTransit. Dễ thôi. Chúng ta chỉ cần thêm một dòng vào lớp sealed của mình.

kotlin Copy
sealed class DeliveryResult {
    object Preparing : DeliveryResult()
    data class Dispatched(val trackingId: String) : DeliveryResult()
    data class InTransit(val location: String) : DeliveryResult() // Trạng thái mới
    data class Delivered(val receiversName: String) : DeliveryResult()
}

Ngay khi bạn thêm dòng này, điều kỳ diệu xảy ra. Hàm getStatusMessage của bạn, mà vừa biên dịch hoàn hảo chỉ vài giây trước, giờ đây đã bị hỏng. IDE của bạn sẽ kêu lên với một gạch đỏ.

Lỗi sẽ nói: 'when' expression must be exhaustive, add necessary 'is InTransit' branch.

Hãy để điều đó ngấm vào. Biên dịch viên vừa ngăn chặn một lỗi sản xuất. Thay vì trạng thái mới InTransit của bạn im lặng rơi vào một nhánh else bị lãng quên, biên dịch viên đã dừng quá trình biên dịch và đưa cho bạn một danh sách việc cần làm hoàn hảo ở thời điểm biên dịch. Nó buộc bạn, lập trình viên, phải đưa ra quyết định có ý thức về cách trạng thái mới này nên được xử lý.

Đây không phải là một lỗi; đây là một món quà. Đây là cách rẻ nhất, an toàn nhất và sớm nhất để phát hiện một lỗi logic.

Một Điều Nữa: Sử Dụng when Như Một Biểu Thức

Đây là mẹo chuyên nghiệp để làm cho mã của bạn thậm chí còn mạnh mẽ và ngắn gọn hơn. Bạn có thể sử dụng when không chỉ như một câu lệnh, mà còn như một biểu thức trả về một giá trị.

kotlin Copy
fun getStatusMessage(result: DeliveryResult): String {
    // Khối `when` giờ đây trả về một String
    return when (result) {
        is DeliveryResult.Preparing -> "Gói hàng của bạn đang được chuẩn bị."
        is DeliveryResult.Dispatched -> "Đã gửi! Theo dõi: ${result.trackingId}"
        is DeliveryResult.Delivered -> "Được ký bởi ${result.receiversName}."
        is DeliveryResult.InTransit -> "Đang trên đường! Lần cuối thấy ở ${result.location}."
    }
}

Khi bạn làm điều này, kiểm tra tính exhaustiveness trở nên quan trọng hơn nữa. Nếu bạn quên một nhánh, biên dịch viên sẽ làm hỏng quá trình biên dịch vì nó không thể đảm bảo rằng biểu thức sẽ trả về một giá trị. Điều này làm cho mã của bạn trở nên chức năng hơn, loại bỏ sự cần thiết của các biến tạm thời có thể thay đổi, và thêm một lớp nữa cho mạng lưới an toàn của bạn.

Mẹo "phép thuật" về tính exhaustiveness này là lý do tại sao các lớp sealed trở thành một nền tảng của phát triển Kotlin và Android hiện đại. Nó thay đổi cơ bản vòng đời phát triển bằng cách chuyển trách nhiệm về tính chính xác từ một con người có thể sai sót tại thời điểm chạy sang một biên dịch viên không thể sai sót tại thời điểm biên dịch.

Có bao giờ bạn gặp phải một trường hợp "thất bại im lặng" hoặc một nhánh else bị lãng quên đã khiến bạn gặp khó khăn? Tôi rất muốn nghe những câu chuyện của bạn trong phần bình luận!

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