12/08/2018, 15:51

Kotlin và Algebraic Data Types

Khi Kotlin phát triển ngày càng rộng trong công chúng thì càng có nhiều developer Java tiếp xúc với những khái niệm "mới" vốn đã có trong các ngôn ngữ khác trong nhiều năm nay. Algebraic Data Types (gọi tắt là ADT) là một trong những khái niệm này. ADT là gì? Nói một cách đơn giản, một ADT là ...

Khi Kotlin phát triển ngày càng rộng trong công chúng thì càng có nhiều developer Java tiếp xúc với những khái niệm "mới" vốn đã có trong các ngôn ngữ khác trong nhiều năm nay. Algebraic Data Types (gọi tắt là ADT) là một trong những khái niệm này.

ADT là gì?

Nói một cách đơn giản, một ADT là một type được đại diện bởi một số subtype khác. Vẫn còn nghe quá phức tạp? Đuợc. Hãy xem ví dụ sau:

enum DeliveryStatus {
 
  PREPARING,
  DISPATCHED,
  WAITING_FOR_YOU_TO_LEAVE_HOME_SO_YOU_WILL_MISS_IT
  
}

Chờ đã, đây không phải là một enum Java sao? Vâng, là nó. Nó cũng là một ví dụ rất đơn giản của ADT. DeliveryStatus là một loại type được đại diện bởi một số loại khác (trong trường hợp enum entry). Nhưng chờ một chút, nó còn là nhiều hơn nữa!

Trên thực tế, hóa ra rằng khi một parcel được dispatched , chúng ta cũng muốn biết tracking number của nó.

Dưới đây là cách thực hiện có thể giống như trong Java.

@GenerateConstructorSomehow
class DeliveryStatus {
 
  public final Stage stage;
  public final String trackingNumber;
  
  enum Stage {
    PREPARING,
    DISPATCHED,
    WAITING_FOR_YOU_TO_LEAVE_HOME_SO_YOU_WILL_MISS_IT
  }
  
}

Nó nhìn xấu tệ, phải không? Tuy nhiên, đó là một thiếu sót quan trọng - trackingNumber chỉ được sử dụng khi Stage là DISPATCHED và là null trong mọi trường hợp khác. Điều này có thể chấp nhận được nếu số lượng các trường hợp "đặc biệt" như vậy là nhỏ, nhưng khi code ngày càng phát triển nhiều field như vậy.

Kotlin với cách cứu chữa

Kotlin giúp chúng ta giải quyết vấn đề này bằng cách cung cấp các lớp sealed. Đây là cách bạn sẽ định nghĩa một ADT trong Kotlin.

sealed class DeliveryStatus {
  
  object Preparing : DeliveryStatus()
  
  data class Dispatching(
    val trackingNumber: String
  ) : DeliveryStatus()
  
  object Delivered : DeliveryStatus()
  
}

Hãy xem những gì đang xảy ra ở đây.

  • Chúng ta định nghĩa một lớp sealed. Điều này có nghĩa là không có class nào ngoài class Kotlin này có thể mở rộng DeliveryStatus, có nghĩa là compiler biết về mọi subtype có thể có của DeliveryStatus. Chúng ta sẽ làm được điều đó vào một thời điểm sau.
  • Chúng ta xác định một số type mở rộng DeliveryStatus.
  • Chúng ta sử dụng một đối tượng nếu không có điểm trong việc có các trường hợp khác nhau của type này.

Lưu ý rằng bây giờ chỉ có DeliveryStatus.Dispatching chứa một tracking number. Không có type khác biết về nó.

Sử dụng ADT

Rất tuyệt vời nhưng làm thế nào để chúng ta sử dụng DeliveryStatus bây giờ? Một trong những cách sẽ được sử dụng khi điều hành sẽ khiến việc sử dụng các ADT đặc biệt thuận tiện.

fun showDeliveryStatus(status: DeliveryStatus) {
  return when (status) {
    is Preparing -> showPreparing()
    is Dispatched -> showDispatched(it.trackingNumber) // note that no cast needed!
  }
}

Điều quan trọng cần chú ý:

  • Không cần phải cast trạng thái Dispatched vì compiler đã biết điều đó.
  • Chúng ta đã return mặc dù không có type trả về được chỉ định. Đó là bởi vì mỗi hàm return Unit theo mặc định.
  • Nó không biên dịch.

Chờ đã? Bạn đọc nó đúng - nó thực sự không biên dịch, nhưng bạn có thể đoán tại sao? Lý do biên dịch lỗi cũng là một ưu điểm khác của các ADT. Nó không được biên dịch bởi vì chúng ta đã quên chỉ định trạng thái Delivered trong mệnh đề when. Ouch!

fun showDeliveryStatus(status: DeliveryStatus) {
  return when (status) {
    is Preparing -> showPreparing()
    is Dispatched -> showDispatched(it.trackingNumber) // note that no cast needed!
    is Delivered -> showDelivered()
  }
}

Bây giờ nó đã hoạt động. Các ADT đang giúp bạn đảm bảo rằng khi các type mới được thêm vào, bạn xử lý chúng đúng cách. Nhưng tất nhiên bạn không phải làm điều đó và nếu bạn sẽ loại bỏ return từ showDeliveryStatus nó sẽ biên dịch tốt ngay cả khi không có Delivered .

Cuối cùng

  • ADTs rất hữu ích trong trường hợp khi bạn cần một giá trị duy nhất đại diện cho một trong một số trạng thái có thể.
  • Nó rất dễ dàng để bắt đầu. Chỉ cần viết class tiếp theo của bạn bằng cách sử dụng ADT. Không cần thay đổi bất cứ điều gì trong code hiện có.
  • ADTs không phải là một sự thay thế cho tất cả các mô hình class, chỉ là những class có các phân lớp phụ.
  • Enum vẫn là một ADT.
0