从Java Builders到Kotlin DSLS

从Java Builders到Kotlin DSLS

介绍

DSLS - 域特定语言 - 是Kotlin圈中的趋势主题。它们允许我们灵活一些最令人兴奋的语言功能,同时在我们的代码中完成更可读和可维护的解决方案。

今天我想向您展示如何实现某种DSL - 我们将在Kotlin的现有Java Builder包装。毫无疑问,你已经遇到了 建造者模式 在Java之前,例如,如果您是Android开发人员,则必须使用 AlertDialog.Builder, 一个 OkHttpClient.Builder,或者 Retrofit.Builder 在某一点。像这样包装的建筑师是纯粹的DSL设计中的一个很好的运动。所有你必须担心正在设计你提供的API,因为无论您的DSL提供所有DSL提供用户是否已在构建器内完成!

我们的例子

这就是这样,我就是我是一个图书馆的创造者和维护者,这是一件非常重要的,它的一小部分是我们将使用的是我们的榜样。原始图书馆是美妙而非常受欢迎的 MaterialDrawer. 经过 迈克佩兹, which allows you to create complex, good looking, and customized navigation drawers in your application, all via various Builder objects in Java, with no XML writing involved on your part. This is what my library, MaterialDrawer.kt. 提供方便的Kotlin DSL包装器。

Builder API.

让我们来看看我们将在我们的榜样中创建的抽屉。

和roid_drawer.

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

drawerBuilder.()
        .withActivity(this)
        .withTranslucentStatusBar(false)
        .withDrawerLayout(R.layout.material_drawer_fits_not)
        .addDrawerItems(
                PrimaryDrawerItem()
                        .withName("首页")
                        .withDescription("Get started here!"),
                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. 课程。虽然他们的Builder语法实际上看起来非常不错,但我们会看到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 extension on Activity, so that it's available when we're in one, and 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, and 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 DrawerBuilderKt(activity: Activity) {
    val builder = DrawerBuilder().withActivity(activity)

    internal fun build() {
        builder.build()
    }
}

We'll update our original drawer function to create an instance of this wrapper class. We'll also modify its parameter - by 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: DrawerBuilderKt.() -> Unit) {
    val builder = DrawerBuilderKt(this)
    builder.setup()
    builder.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 builder is another story - we'll have to keep this public so that our library stays extensible. We could make it internal for the purpose of us implementing wrappers around the built-in drawer items, but that would mean that nobody else could 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 by adding primaryItem as an extension function:

fun DrawerBuilderKt.primaryItem() {
    builder.addDrawerItems(PrimaryDrawerItem())
}

我们现在可以向我们的抽屉添加空白抽屉项!

设置属性

Before we get to setting the name and 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 and written (var) and read-only ones (val). We'll solve this by using a var, 一个d throwing an exception when someone tries to read these properties. We'll also include Kotlin's powerful @Deprecated 注释允许我们使用Getters的错误一个错误,以便在编辑/编译时停止客户端,而不是仅获取运行时异常:

class DrawerBuilderKt(activity: Activity) {
    ...

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

设置属性的替代方案

现在,我们可以继续自行定制抽屉项。这里的明显解决方案是继续与以前相同的语法样式:

drawer {
    primaryItem {
        name = "首页"
        description = "Get started here!"
    }
}

We know how to do this by 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, and create this alternative syntax:

drawer {
    primaryItem(name = "首页", description = "Get started here!")
}

This is also pretty straightforward, we're just calling a function that has two parameters, and 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 DrawerBuilderKt.primaryItem(name: String = "", description: String = "") {
    val item = PrimaryDrawerItem()
            .withName(name)
            .withDescription(description)
    builder.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 DrawerBuilderKt(activity: Activity) {
    ...

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

我们正在利用 山姆转换 with the call to the withOnDrawerItemClickListener method of the builder here. The usual SAM conversion 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, and we're passing in the handler parameter which has the appropriate function type for a conversion.

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

class DrawerBuilderKt(activity: Activity) {
    ...

    fun onItemClick(handler: (position: Int) -> Boolean) {
        builder.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 here. 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 DrawerBuilderKt(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 builder we're holding. We can't make a withOnDrawerListener call in each of them and 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, and delegates each of these calls to one of its properties.

class DrawerBuilderKt(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 DrawerBuilderKt(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 builder at some point - we'll do that after the setup lambda passed to drawer has already done its work, and it calls our DrawerBuidlerKt.build() method:

class DrawerBuilderKt(activity: Activity) {
    val builder = DrawerBuilder().withActivity(activity)

    internal fun build() {
        builder.withOnDrawerListener(onDrawerListener)
        builder.build()
    }

    ...
}

结果

作为提醒,这里是我们开始的构建商代码:

drawerBuilder.()
        .withActivity(this)
        .withTranslucentStatusBar(false)
        .withDrawerLayout(R.layout.material_drawer_fits_not)
        .addDrawerItems(
                PrimaryDrawerItem()
                        .withName("首页")
                        .withDescription("Get started here!"),
                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 here!")
    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的变化更容易,当您实际制造它们时,当然会变得明显(但担心在正确的地方放置逗号,例如,在过去)。

结论

谢谢您在整个旅程中粘在一起。我希望你有一个很好的感受如何设计自己的DSLS(始终首先写下所需的语法!),以及如何构建DSL实现。

如果您对更多感兴趣,我建议签出实际的来源 MaterialDrawer.kt. 图书馆,它处理了许多更高级的概念,我无法在这篇文章中融入。在其他事情之外,它使用泛型和继承巨大处理 MaterialDrawer.“内置抽屉项类型的复杂层次结构”,限制函数调用和属性访问适当的DSL层次结构等级,还有更多。我可能会在未来的文章中涵盖这些。

此外,您可以找到另一篇文章,涵盖了编写DSL的过程 这里以及其他关于更多普通DSL设计的文章 这里.

但这是现在的,而且,现在,现在是你的时间,开始创建自己的DSL。祝你好运!

2 thoughts on “从Java Builders到Kotlin DSLS

发表评论

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