Kotlin DSL Everywhere

DSL (domain-specific language) là một khái niệm khá đơn giản, nó cung cấp cho chúng ta ngữ cảnh của việc chúng ta đang làm, ví dụ như một đoạn script của Gradle dưới đây:

android {
  compileSdkVersion 26
  defaultConfig {
    applicationId "com.example.you"
    minSdkVersion 15
    targetSdkVersion 26
    versionCode 1
    versionName "1.0
  }
}

Trong mục android, chúng ta đưa vào những config riêng của android và trong mục defaultConfig chúng ta đưa vào những config chung. Nhờ có cấu trúc này mà chúng ta có thể hiểu được ngữ cảnh của từng đầu mục.

Chúng ta có thể ứng dụng cấu trúc này vào việc xây dựng một API với nhiệm vụ riêng biệt. Ví dụ như chúng ta viết một class builder để tạo ra một bài báo chẳng hạn, sẽ rất hay nếu chúng ta có thể làm thế này:

val articleBuilder = ArticleBuilder()
articleBuilder {
  title = "This is the title"
  addParagraph {
    body = "This is the first paragraph body"
  }
  addParagraph {
    body = "This is the first paragraph body"
    imageUrl = "https://path/to/url"
  }
}

Đây là một API rất rõ ràng và dễ sử dụng, nó gần như là việc chúng ta miêu tả lại quá trình tạo ra bài báo bằng ngôn ngữ tự nhiên vậy. Trong Kotlin, chúng ta có thể dễ dàng tạo ra những DSL nhỏ như vậy bởi vì ngôn ngữ này cung cấp cho chúng ta rất nhiều tiện ích. Việc đầu tiên chúng ta cần làm là tạo ra 1 invoke operator cho class này:

class ArticleBuilder {
 operator fun invoke(block: ArticleBuilder.() -> Unit) {
    block()
  }
}

Trong đoạn code trên chúng ta sử dụng 3 kỹ thuật từ Kotlin:

  • Trong Kotlin, chúng ta có thể override rất nhiều operator và một trong số chúng là operator invoke. Operator này cho phép chúng ta thực thi một đoạn code mà không cần thiết phải tự gọi đến hàm nào cả, vậy nên đoạn code articleBuilder { (...) } là hoàn toàn hợp lệ.

  • Kỹ thuật thứ 2 là việc chúng ta có thể tùy chọn loại bỏ dấu ngoặc đơn nếu hàm chỉ có một parameter là lambda (hoặc có nhiều parameter, nhưng parameter cuối là lambda) do đó chúng ta thay vì gọi articleBuilder { (...) } như trên thì có thể gọi articleBuilder { ... }.

  • Cuối cùng là lambda với receiver, nó cho phép chúng ta có một implicit receiver (trong trường hợp này chính là ArticleBuilder.this) trong ngoặc nhọn. Sử dụng implitcit receiver này đồng nghĩa với việc chúng ta có thể gọi bất cứ hàm public nào thuộc class ArticleBuilder mà không cần phải khởi tạo 1 đối tượng mới thuộc kiểu ArticleBuilder.

Tiếp theo, chúng ta sẽ thêm thuộc tính title vào class ArticleBuilder:

class ArticleBuilder {
  lateinit var title: String
}

Bởi vì đây là thuộc tính public nên như đã nói ở mục 3 bên trên, chúng ta có thể trực tiếp refer đến đối tượng này: articleBuilder{title = “title”}.

Giờ thì chúng ta sẽ cho phép việc thêm các đoạn văn cho bài báo, để làm được như vậy thì trước hết chúng ta sẽ thêm 1 class Paragraph với 2 thuộc tính là bodyimageUrl:

class Paragraph {
  lateinit var body: String
  var imageUrl: String? = null
}

Sau đó chúng ta sẽ thêm một đoạn mới bằng cách tạo ra một hàm với tên gọi là addParagraph với receiver là Paragraph tương tự như ở trên. Hàm này sẽ có nhiệm vụ khởi tạo và set các thuộc tính cho đoạn văn:

fun addParagraph(block: Paragraph.() -> Unit) {
  val paragraph = Paragraph()
  paragraph.block()
  paragraphs.add(paragraph)
}

Đây là đoạn code cuối cùng của chúng ta:

class Paragraph {
  lateinit var body: String
  var imageUrl: String? = null
}

class ArticleBuilder {
  lateinit var title: String
  val paragraphs = mutableListOf<Paragraph>()

  operator fun invoke(block: ArticleBuilder.() -> Unit) {
    block()
  }

  fun addParagraph(block: Paragraph.() -> Unit) {
    val paragraph = Paragraph()
    paragraph.block()
    paragraphs.add(paragraph)
  }
}

fun main(args: Array<String>) {
  val articleBuilder = ArticleBuilder()
  articleBuilder {
    title = "This is the title"
    addParagraph {
      body = "This is the first paragraph body"
    }
    addParagraph {
      body = "This is the first paragraph body"
      imageUrl = "https://path/to/url"
    }
  }
}

Đây là một kỹ thuật khá hay khi chúng ta tạo một API mới, nhưng với những API có sẵn thì sao? Ví dụ tôi muốn thay đổi builder pattern của API TransitionSet sang thành DSL, hãy thử tạo một API để xóa một view và thêm một view khác. Trong Java thì chúng ta sẽ làm thế này:

Transition transition = new TransitionSet().
    addTransition(Slide(Gravity.TOP).addTarget(text1)).
    addTransition(Slide(Gravity.BOTTOM).addTarget(text2)).
    setDuration(1000).
    setInterpolator(new FastOutSlowInInterpolator()).
    addListener(new Transition.TransitionListener() {
      @Override public void onTransitionStart(Transition transition) {

      }

      @Override public void onTransitionEnd(Transition transition) {
        text1.setVisibility(View.GONE);
      }

      @Override public void onTransitionCancel(Transition transition) {

      }

      @Override public void onTransitionPause(Transition transition) {

      }

      @Override public void onTransitionResume(Transition transition) {

      }
    });

Hoặc ngắn gọn hơn, bạn có thể sử dụng TransitionListenerAdapter.

Còn trong Kotlin, chúng ta có thể improve nó như sau:

val transition = TransitionSet()
transition {
  addTransition {
    Slide(Gravity.TOP).addTarget(text1)
  }

  addTransition {
    Slide(Gravity.TOP).addTarget(text2)
  }

  duration = 1000
  interpolator = FastOutSlowInInterpolator()
  addEndListener {
    text1.visibility = View.GONE
  }
}

Đối với tôi thì phiên bản DSL rõ ràng và dễ hiểu hơn rất nhiều. Bởi vì chúng ta cần phải gọi những API có sẵn, chúng ta có thể sử dụng extension function sau đó làm tương tự như với ví dụ về ArticleBuilder ở trên:

operator fun TransitionSet.invoke(block: TransitionSet.() -> Unit) {
  block()
}

fun TransitionSet.addTransition(block : TransitionSet.() -> Transition) {
  addTransition(block())
}

fun TransitionSet.addEndListener(block: TransitionSet.() -> Unit) {
  addListener(object : Transition.TransitionListener {
    override fun onTransitionEnd(transition: Transition?) {
      block()
    }

    override fun onTransitionResume(transition: Transition?) {

    }

    override fun onTransitionPause(transition: Transition?) {

    }

    override fun onTransitionCancel(transition: Transition?) {

    }

    override fun onTransitionStart(transition: Transition?) {

    }

  })
}

Qua bài viết chúng ta đã học được cách ứng dụng những kỹ thuật khá hay trong Kotlin để tạo ra một bộ API đẹp mắt và dễ sử dụng. Hi vọng trong tương lai DSL sẽ được mọi người đón nhận nhiều hơn nữa. Cảm ơn các bạn đã theo dõi.

Bài viết được dịch từ Kotlin DSL Everywhere của tác giả Ronen Sabag.