0
0
Lập trình
Admin Team
Admin Teamtechmely

Giải Pháp An Toàn Với Sealed Classes Trong Kotlin

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

• 7 phút đọc

Giới thiệu

Đã 2 giờ sáng, và những thông báo lỗi hệ thống sản xuất không ngừng vang lên. Thủ phạm? Một NullPointerException xuất hiện từ một câu lệnh when đơn giản—mã mà tôi nghĩ là rất an toàn. Ứng dụng đang nhận một trạng thái mà nó chưa bao giờ được thiết kế để xử lý. Một khách không mời đã vượt qua rào cản bảo mật, đi thẳng vào bữa tiệc và khiến toàn bộ hệ thống sụp đổ.

Đây là nỗi sợ hãi cơ bản của một lập trình viên: điều chưa biết. Chúng ta xây dựng hệ thống để xử lý một thế giới có thể dự đoán, nhưng thế giới thực thì hỗn độn. Chúng ta vá những khoảng trống bằng các câu lệnh else phòng ngừa, vô số kiểm tra null và các mẫu thiết kế mong manh mà cảm giác như không phải kỹ thuật mà giống như cầu may.

Tôi đã xây dựng các ứng dụng dựa trên hy vọng đó. Sau đó, tôi phát hiện ra sealed class trong Kotlin, và đó không chỉ là một tính năng mới—đó là một cách suy nghĩ mới. Đó là cách mà tôi ngừng xây dựng những mìn và bắt đầu xây dựng những pháo đài.

Cát Quicksand Kiến Trúc: Đối Tượng "Junk Drawer"

Trước khi tôi thấy ánh sáng, tôi đã phạm phải một mẫu thiết kế xấu mà tôi chắc bạn đã thấy, hoặc thậm chí đã viết. Tôi gọi nó là đối tượng "Junk Drawer". Thuật ngữ chính thức là "Tagged Class".

Hãy tưởng tượng việc mô hình hóa trạng thái giao hàng. Cách tiếp cận cũ của tôi là nhồi nhét tất cả vào một lớp quá cồng kềnh.

kotlin Copy
class DeliveryStatus(
    val type: Type, // "nhãn" cho biết trạng thái hiện tại
    val trackingId: String? = null, // Chỉ sử dụng đôi khi...
    val receiversName: String? = null, // Cũng chỉ sử dụng đôi khi...
    val delayReason: String? = null // Và một thuộc tính nullable khác...
) {
    enum class Type {
        PREPARING,
        DISPATCHED,
        DELAYED,
        DELIVERED
    }
}

Hãy để tôi nói thẳng: mã này rất nguy hiểm. Nó là cát quicksand kiến trúc.

  1. Nó Tạo Ra Các Trạng Thái Không Thể: Điều gì ngăn cản bạn tạo DeliveryStatus(Type.PREPARING, receiversName = "John Doe")? Hoàn toàn không có gì. Đối tượng cho phép bạn đại diện cho các trạng thái mà về mặt logic là không thể. Cấu trúc dữ liệu của bạn đang nói dối về những gì hợp lệ.
  2. Đó Là Lễ Hội Của Nulls: Vì không phải thuộc tính nào cũng áp dụng cho mọi trạng thái, tất cả chúng đều phải là nullable. Mã của bạn trở thành một mảnh đất mìn của các toán tử ?. và các kiểm tra phòng ngừa, chỉ cần một kiểm tra bị quên là bạn sẽ gặp NullPointerException vào lúc 2 giờ sáng.
  3. Nó Vi Phạm Nguyên Tắc Cốt Lõi: Lớp này đang cố gắng trở thành bốn thứ khác nhau cùng một lúc, hoàn toàn vi phạm Nguyên Tắc Trách Nhiệm Đơn. Khi các trạng thái mới được thêm vào, lớp trở nên phình ra, trở thành một mớ hỗn độn không thể duy trì.

Đây không phải là an toàn kiểu; đây là một ảo tưởng về kiểm soát mà sẽ sụp đổ dưới áp lực thực tế.

Sự Chuyển Mình: Từ Junk Drawer Đến Hộp Dụng Cụ Tùy Chỉnh

Bây giờ, điều gì sẽ xảy ra nếu thay vì một chiếc hộp nhặt nhạnh mọi thứ vào nhau, bạn có một chiếc hộp dụng cụ được tổ chức hoàn hảo, được xây dựng riêng? Một chiếc hộp có một khe cụ thể, được định hình cho mọi công cụ. Bạn không thể đặt búa vào khe của tua vít.

Đó là sealed class. Đó là một hợp đồng với trình biên dịch nói rằng, "Đây là danh sách hoàn chỉnh, hữu hạn của tất cả các subtype có thể. Không có gì khác tồn tại. Thế giới đã khép kín và có thể dự đoán."

Hãy tái cấu trúc mớ hỗn độn DeliveryStatus thành hộp dụng cụ DeliveryResult:

kotlin Copy
sealed class DeliveryResult {
    // Một đối tượng đơn giản, không trạng thái. Khe của nó chỉ là tên của nó.
    object Preparing : DeliveryResult()

    // Một lớp dữ liệu với một khe CHỈ cho trackingId.
    data class Dispatched(val trackingId: String) : DeliveryResult()

    // Một hình dạng khác, với các khe dữ liệu độc đáo của riêng nó.
    data class Delivered(val trackingId: String, val receiversName: String) : DeliveryResult()

    // Một hình dạng thứ ba cho một trạng thái khác biệt.
    data class Delayed(val reason: String): DeliveryResult()
}

Sự khác biệt là rõ ràng. Các trạng thái không thể giờ đây không thể đại diện ở mức trình biên dịch. Bạn không thể tạo trạng thái Preparing với một trackingId. Bạn không thể có trạng thái Dispatched với một delayReason. Mỗi đối tượng đều gọn gàng, có mục đích và trung thực. Căn bệnh null đã biến mất.

Ma Thuật Thực Sự: Trình Biên Dịch Trở Thành Lưới An Toàn Của Bạn

Có các mô hình dữ liệu sạch là tuyệt vời, nhưng tính năng nổi bật của sealed class là những gì nó mở khóa trong các biểu thức when. Trình biên dịch, biết danh sách đầy đủ các subtype, trở thành một lập trình viên đồng hành cảnh giác của bạn.

kotlin Copy
fun handleStatus(result: DeliveryResult) {
    when (result) {
        is DeliveryResult.Preparing -> {
            println("Gói hàng của bạn đang được chuẩn bị.")
        }
        is DeliveryResult.Dispatched -> {
            println("Đã gửi! Theo dõi: ${result.trackingId}")
        }
        is DeliveryResult.Delivered -> {
            println("Đã ký nhận bởi ${result.receiversName}.")
        }
        is DeliveryResult.Delayed -> {
            println("Có sự chậm trễ: ${result.reason}")
        }
    }
}

Hai điều tuyệt vời đang xảy ra ở đây:

  • Thông Minh Casting: Bên trong mỗi nhánh, trình biên dịch tự động và an toàn chuyển đổi biến result thành subtype cụ thể mà bạn đang kiểm tra. Không cần phải chuyển đổi thủ công nữa!
  • Đầy Đủ: Bởi vì trình biên dịch biết rằng nó đã thấy mọi loại có thể từ phân cấp sealed, nó không yêu cầu nhánh else. Sự vắng mặt của else là một tuyên bố mạnh mẽ: "Logic này là hoàn chỉnh."

Nhưng đây là khoảnh khắc thực sự của "lập trình viên 10x". Điều gì xảy ra khi quản lý sản phẩm của bạn thêm một yêu cầu mới? "Chúng ta cần một trạng thái Returned."

Bạn chỉ cần thêm một dòng mã:

kotlin Copy
sealed class DeliveryResult {
    object Returned : DeliveryResult() // Trạng thái mới được thêm vào!
}

Ngay khi bạn làm điều này, hàm handleStatus của bạn sẽ không còn biên dịch được nữa. Trình biên dịch sẽ ném ra một lỗi cứng: 'when' expression must be exhaustive, add necessary 'is Returned' branch.

Lỗi biên dịch này là tính năng lớn nhất của sealed class. Đó không phải là lỗi; đó là lưới an toàn của bạn. Trình biên dịch vừa quét toàn bộ mã nguồn của bạn và tạo ra một danh sách hoàn hảo của mọi nơi cần được cập nhật để xử lý logic kinh doanh mới này. Bạn đã biến một lỗi runtime thành một nhiệm vụ compile-time không thể xảy ra.

Những Điều Chính Cần Nhớ

Điều này không chỉ là một mẹo ngôn ngữ thú vị; đó là một công cụ cơ bản để viết phần mềm mạnh mẽ, có thể duy trì.

  • Ngừng đại diện trạng thái bằng các thuộc tính nullable và enum. Mẫu "Tagged Class" này là một nhà máy sản xuất lỗi.
  • Hãy chấp nhận sealed classes để mô hình hóa các trạng thái hữu hạn, khác biệt. Làm cho các trạng thái không thể trở thành không thể đại diện trong mã của bạn.
  • Tin tưởng vào kiểm tra đầy đủ của trình biên dịch. Để nó trở thành lưới an toàn của bạn. Một lỗi biên dịch rẻ hơn rất nhiều so với một sự cố sản xuất.

Tính năng này đã cứu tôi khỏi vô số lỗi và làm cho mã quản lý trạng thái của tôi trong Android trở nên đơn giản và an toàn hơn rất nhiều, đặc biệt là với các kiến trúc hiện đại như MVI và Jetpack Compose.

Khoảnh khắc "aha!" nào đã thay đổi cách bạn viết mã? Hãy chia sẻ trong phần bình luận bên dưới!

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