Kotlin DSL Everywhere
This post hasn't been updated for 6 years
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 codearticleBuilder { (...) }
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ọiarticleBuilder { ... }
. -
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àmpublic
nào thuộc classArticleBuilder
mà không cần phải khởi tạo 1 đối tượng mới thuộc kiểuArticleBuilder
.
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à body
và imageUrl
:
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.
All Rights Reserved