从Java Builders到Kotlin DSL

从Java Builders到Kotlin DSL

介绍

DSL(领域特定语言)是Kotlin圈子中不断发展的主题。它们使我们可以扩展一些最令人兴奋的语言功能,同时在代码中完成更具可读性和可维护性的解决方案。

今天,我想向您展示如何实现某种DSL-我们将在Kotlin中包装一个现有的Java Builder。毫无疑问,您遇到过 建造者模式 例如,如果您是Android开发人员,则必须使用Java AlertDialog.BuilderOkHttpClient.BuilderRetrofit.Builder 在某一点。在纯DSL设计中,包装这样的构建器是一个很好的练习。您只需要担心包装器提供的API,因为DSL为用户提供的所有实现都已经在构建器中完成了!

我们的例子

碰巧我是做这件事的库的创建者和维护者,而实现的一小部分就是我们将要用作示例的内容。原始的图书馆非常棒,非常受欢迎 材料抽屉 通过 迈克·彭兹 , which allows you to create complex, good looking, 和 customized navigation drawers in your application, all via various Builder objects in 爪哇 , with no XML writing involved on your part. This is what my library, 材料抽屉 Kt 提供了一个方便的Kotlin DSL包装器。

生成器API

让我们看一下我们将在示例中创建的抽屉。

  安卓 _drawer

这是代码,使用原始API中的构建器:

  DrawerBuilder  ()
        .withActivity(this)
        .withTranslucentStatusBar(false)
        .withDrawerLayout(R.layout.material_drawer_fits_not)
        .addDrawerItems(
                 PrimaryDrawerItem ()
                        .withName(" 家 ")
                        .withDescription("Get started  这里 !"),
                 PrimaryDrawerItem ()
                        .withName("Settings")
                        .withDescription("Tinker around")
        )
        .withOnDrawerItemClickListener { view, position, drawerItem ->
            when(position) {
                0 -> toast(" 家  clicked")
                1 -> toast("Settings clicked")
            }
            true
        }
        .withOnDrawerListener(object: Drawer.OnDrawerListener {
            override fun onDrawerSlide(drawerView: View?, slideOffset: Float) {
                // Empty
            }

            override fun onDrawerClosed(drawerView: View?) {
                toast("Drawer closed")
            }

            override fun onDrawerOpened(drawerView: View?) {
                toast("Drawer opened")
            }
        })
        .build()

在这段代码中,我们已经...
- set the Activity we want the drawer to appear in,
- made some layout adjustments so that the drawer is below the ActionBar,
-仅创建两个带有名称和描述的菜单项,
-设置一个侦听器,在该侦听器中我们可以使用SAM转换实现单个方法接口,从而按位置处理项目选择,
-添加了一个侦听器以使用对象表达式检测抽屉的移动,因为必要的接口具有多种方法。

我们看到这里使用了两种不同的构建器,即 DrawerBuilder PrimaryDrawerItem 类。尽管他们的构建器语法实际上看起来相当不错并且可读,但是我们将看到DSL可以做得更好。

请注意,您可以在GitHub上查看本文的整个工作演示项目。 这里 。 见 提交历史 在构建DSL时逐步了解本文。

创建实例

Let's start small. We'll want to create a drawer with a drawer {} call, let's implement just that.

fun Activity.drawer(dummy: () -> Unit) {
     DrawerBuilder ()
            .withActivity(this)
            .build()
}

We've defined our first function as an 延期 on Activity, so that it's available when we're in one, 和 it can access the Activity instance as this without us having to pass it in explicitly.

Now, we should add our PrimaryDrawerItem instances. This syntax would be nice for a start:

drawer {
    primaryItem()
    primaryItem()
}

To get this, we'll need a primaryItem function that's only available within the drawer block, 和 that somehow adds an item to the DrawerBuilder we've already created.

To be able to call methods on our DrawerBuilder instance before we build() it, we'll introduce a new wrapper class that holds it:

class  DrawerBuilder Kt(activity: Activity) {
    val  建造者  =  DrawerBuilder ().withActivity(activity)

    internal fun build() {
         建造者 .build()
    }
}

We'll update our original drawer function to create an instance of this wrapper class. We'll also modify its parameter - 通过 making the setup function an 延期 on our own class, the client of the DSL will be placed in a new scope inside the lambda passed to the drawer function, where the methods of DrawerBuilder Kt become available, as we'll see in a moment.

fun Activity.drawer(setup:  DrawerBuilder Kt.() -> Unit) {
    val  建造者  =  DrawerBuilder Kt(this)
     建造者 .setup()
     建造者 .build()
}

You might have spotted that we've marked our own build method internal - this is because the only call to it will be the one inside the drawer function. Hence, there's no need to expose it to the clients of our library.

The visibility of 建造者 is another story - we'll have to keep this public so that our library stays extensible. We 可以 make it internal for the purpose of us implementing wrappers around the built-in drawer items, but that would mean that nobody else 可以 add their own custom drawer items to the DSL - something you 可以 用原始库做。这是我们不想从客户身上剥离的功能。

Now we can finally add the primaryItem function we were planning earlier. To make this available inside the lambda passed to drawer, we 可以 make it a member of the DrawerBuilder Kt class. Modifying this class for every new drawer item type we add, however, seems like an odd thing to do. The various types of drawer items should in no way affect how the drawer itself works.

We can instead use the same mechanics as clients would use to create custom drawer items. We'll get a neat, decoupled design 通过 adding primaryItem as an 延期 function:

fun  DrawerBuilder Kt.primaryItem() {
     建造者 .addDrawerItems(PrimaryDrawerItem())
}

现在,我们可以将空白抽屉项目添加到抽屉中了!

设定属性

Before we get to setting the name 和 description of our drawer items with the DSL, we have the properties of DrawerBuilder to take care of, as we've seen in the very first code snippet. This is the syntax we'll create for these:

drawer {
    drawerLayout = R.layout.material_drawer_fits_not 
    translucentStatusBar = false
}

These, of course, will be properties on the DrawerBuilder Kt class so that they're available in the right scope.

由于原始构建器不允许我们访问设置的值,因此我们需要创建的本质上是只写属性。这些属性将没有后备字段来存储值,它们要做的就是将调用转发到适当的构建器方法。

Unfortunately, Kotlin only has properties that can be both read 和 written (var) 和 read-only ones (val). We'll solve this 通过 using a var , d throwing an exception when someone tries to read these properties. We'll also include Kotlin's powerful @Deprecated 允许我们使用getter标记错误的注释,以便客户端在编辑/编译时停止这样做,而不仅仅是获得运行时异常:

class  DrawerBuilder Kt(activity: Activity) {
    ...

    var drawerLayout: Int
        @Deprecated(message = "Non readable property.", level = DeprecationLevel.ERROR)
        get() = throw UnsupportedOperationException("")
        set(value) {
             建造者 .withDrawerLayout(value)
        }
}

设置属性的替代方法

现在,我们可以继续自定义抽屉项目了。显而易见的解决方案是继续使用与以前相同的语法样式:

drawer {
    primaryItem {
        name = " 家 "
        description = "Get started  这里 !"
    }
}

We know how to do this 通过 creating a wrapper class around PrimaryDrawerItem , d then adding a couple non-readable properties, just like we did before. But let's do something more interesting, 和 create this alternative syntax:

drawer {
    primaryItem(name = " 家 ", description = "Get started  这里 !")
}

This is also pretty straightforward, we're just calling a function that has two parameters, 和 using named parameters for readability. Let's add these parameters to primaryItem then. We'll also throw in default values so that they're each optional:

fun  DrawerBuilder Kt.primaryItem(name: String = "", description: String = "") {
    val item =  PrimaryDrawerItem ()
            .withName(name)
            .withDescription(description)
     建造者 .addDrawerItems(item)
}

This, of course, isn't a feasible method for adding dozens of properties to an item we're constructing with our DSL, adding a wrapper around the PrimaryItemClass with separate write-only properties that can be set in a setup lambda is still the way to go for most things.

但是,这是将一些非常基本的或通常设置的属性提升到代码中更重要位置的好方法。这是实现了更多属性的DSL的样子:

primaryItem(name = "Games", description = "Ready, player one?") {
    iicon = FontAwesome.Icon.faw_gamepad
    identifier = 3
    selectable = false
}

听众

We know how to add properties to our DSL, now let's see how we can go about listeners. We'll start with the easy one, setting the OnDrawerItemClickListener to handle item clicks for a given position in the drawer. Here's our goal:

drawer {
    onItemClick { position ->
        when (position) {
            0 -> toast(" 家  clicked")
            1 -> toast("Settings clicked")
        }
        true
    }
}

onItemClick will be a method in DrawerBuilder Kt , d it will take a lambda parameter that can be called when the original listener fires:

class  DrawerBuilder Kt(activity: Activity) {
    ...

    fun onItemClick(handler: (view: View?, position: Int, drawerItem: IDrawerItem<*, *>) -> Boolean) {
         建造者 .withOnDrawerItemClickListener(handler)
    }
}

我们正在利用 SAM转换 with the call to the withOnDrawerItemClickListener method of the 建造者 这里 . The usual SAM转换 syntax would have us passing in a lambda that gets transformed to the OnDrawerItemClickListener interface, but instead, we're going just a small step further, 和 we're passing in the handler parameter which has the appropriate function type for a conversion.

We'll simplify the above method a bit, 通过 taking a lambda which only gets the position passed to it, as clients will usually only care about that parameter. We're using SAM转换 again, this time with a regular lambda, because we want to ignore some parameters when calling our simpler handler parameter.

class  DrawerBuilder Kt(activity: Activity) {
    ...

    fun onItemClick(handler: (position: Int) -> Boolean) {
         建造者 .withOnDrawerItemClickListener { _, position, _ -> handler(position) }
    }
}

当然,只有此版本的方法会隐藏原始库的功能,因此在真正的包装器库中,我包括了 这两个 .

复杂的听众

Last but not least, let's see what we can do about the OnDrawerListener in our example. This interface has three methods, so the previous, simple solution won't work 这里 . As always, let's start with the syntax we want to achieve. Not that we're only setting two of the three methods that the interface defines.

drawer {
    onClosed {
        toast("Drawer closed")
    }
    onOpened {
        toast("Drawer opened")
    }  
}

As you can see, it would be nice to be able to specify either one or multiple of the methods of the interface independently of each other. We know that we'll want to define three methods to take the appropriate handler lambdas, very similarly to what we did before:

class  DrawerBuilder Kt(activity: Activity) {
    ...

    fun onOpened(handler: (drawerView: View) -> Unit) {
        // TODO implement
    }

    fun onClosed(handler: (drawerView: View) -> Unit) {
        // TODO implement
    }

    fun onSlide(handler: (drawerView: View, slideOffset: Float) -> Unit) {
        // TODO implement
    }
}

The question is how to pass these handlers to the 建造者 we're holding. We can't make a withOnDrawerListener call in each of them 和 create an object that wraps just that one handler, as the object created there would always implement just one of the three methods.

What I came up with for this is an object property in our DrawerBuilder Kt wrapper class that implements the OnDrawerListener interface, 和 delegates each of these calls to one of its properties.

class  DrawerBuilder Kt(activity: Activity) {
    ...

    private val onDrawerListener = object : Drawer.OnDrawerListener {
        var onSlide: ((View, Float) -> Unit)? = null

        override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
            onSlide?.invoke(drawerView, slideOffset)
        }

        var onClosed: ((View) -> Unit)? = null

        override fun onDrawerClosed(drawerView: View) {
            onClosed?.invoke(drawerView)
        }

        var onOpened: ((View) -> Unit)? = null

        override fun onDrawerOpened(drawerView: View) {
            onOpened?.invoke(drawerView)
        }
    }
}

然后,我们所有以前的方法要做的就是在调用它们时设置这些属性:

class  DrawerBuilder Kt(activity: Activity) {
    ...

    fun onOpened(handler: (drawerView: View) -> Unit) {
        onDrawerListener.onOpened = handler
    }

    fun onClosed(handler: (drawerView: View) -> Unit) {
        onDrawerListener.onClosed = handler
    }

    fun onSlide(handler: (drawerView: View, slideOffset: Float) -> Unit) {
        onDrawerListener.onSlide = handler
    }
}

Of course, we'll still have to pass this object to the 建造者 at some point - we'll do that after the setup lambda passed to drawer has already done its work, 和 it calls our DrawerBuidlerKt.build() method:

class  DrawerBuilder Kt(activity: Activity) {
    val  建造者  =  DrawerBuilder ().withActivity(activity)

    internal fun build() {
         建造者 .withOnDrawerListener(onDrawerListener)
         建造者 .build()
    }

    ...
}

结果

提醒一下,这是本文开头的构建器代码:

  DrawerBuilder  ()
        .withActivity(this)
        .withTranslucentStatusBar(false)
        .withDrawerLayout(R.layout.material_drawer_fits_not)
        .addDrawerItems(
                 PrimaryDrawerItem ()
                        .withName(" 家 ")
                        .withDescription("Get started  这里 !"),
                 PrimaryDrawerItem ()
                        .withName("Settings")
                        .withDescription("Tinker around")
        )
        .withOnDrawerItemClickListener { view, position, drawerItem ->
            when(position) {
                0 -> toast(" 家  clicked")
                1 -> toast("Settings clicked")
            }
            true
        }
        .withOnDrawerListener(object: Drawer.OnDrawerListener {
            override fun onDrawerSlide(drawerView: View?, slideOffset: Float) {
                // Empty
            }

            override fun onDrawerClosed(drawerView: View?) {
                toast("Drawer closed")
            }

            override fun onDrawerOpened(drawerView: View?) {
                toast("Drawer opened")
            }
        })
        .build()

这是我们整合在一起的最终DSL:

drawer {
    drawerLayout = R.layout.material_drawer_fits_not
    translucentStatusBar = false

    primaryItem(name = " 家 ", description = "Get started  这里 !")
    primaryItem(name = "Settings", description = "Tinker around")

    onItemClick { position ->
        when (position) {
            0 -> toast(" 家  clicked")
            1 -> toast("Settings clicked")
        }
        true
    }
    onClosed {
        toast("Drawer closed")
    }
    onOpened {
        toast("Drawer opened")
    }
}

我希望这可以作为一个很好的例子,说明DSL的外观和构建者风格的解决方案比DSL更好。另一个好处是在DSL中进行更改变得容易得多,这在您实际进行更改时会变得很明显(例如,过去担心将逗号放在正确的位置)。

结论

感谢您在整个旅程中坚持与我合作。我希望您对如何设计自己的DSL(总是先写下所需的语法!)以及如何构造DSL实现有良好的感觉。

如果您对更多内容感兴趣,建议您查看实际来源 材料抽屉 Kt 库,因为它处理了我在本文中无法涵盖的许多更高级的概念。除其他外,它大量使用泛型和继承来处理 材料抽屉 内置抽屉项目类型的复杂层次结构,将函数调用和属性访问限制在DSL层次结构的适当级别,等等。我可能会在以后的文章中介绍这些内容。

此外,您可以找到另一篇有关编写DSL过程的文章。 这里 ,另一篇关于更一般的DSL设计的文章 这里 .

但这就是现在,因此,现在您该开始创建自己的DSL。祝好运!

关于2个想法 “从Java Builders到Kotlin DSL

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *