Sunday, June 4, 2023

Visitor Design Patterns with Kotlin and comparative analysis with Java

 Visitor Design Patterns

This design pattern is usually a close friend of the Composite design pattern Structural Patterns. It can either extract data from a complex tree-like structure or add behavior to each node of the tree, much like the Decorator design pattern. So, my plan, being a lazy software architect, worked out quite well. Both my mail-sending system from Builder and my request-answering system from Chain of Responsibility worked quite well. But some developers still begin to suspect that I'm a bit of a fraud. To confuse them, I plan to produce weekly emails with links to all the latest buzzword articles. Of course, I don't plan to read them myself, just collect them from some popular technology sites.

Writing a crawler 

Let's look at the following structure, which is very similar to what we had when discussing the Iterator design pattern: 

Page(Container(Image(),Link(),Image()),Table(),Link(),

     Container(Table(),

               Link()),

     Container(Image(),

               Container(Image(),

                         Link()))) 

The Page is a container for other HtmlElements, but not HtmlElement by itself. Container holds other containers, tables, links, and images. Image holds its link in the src attribute. Link has the href attribute instead. We start by creating a function that will receive the root of our object tree, a Page in this case, and return a list of all available links: 

fun collectLinks(page: Page): List<String> {

    // No need for intermediate variable there

return LinksCrawler().run {

        page.accept(this)

        this.links

    }

Using run allows us to control what we return from the block body. In this case, we would return the links we've gathered. In Java, the suggested way to implement the Visitor design pattern is to add a method for each class that would accept our new functionality. We'll do the same, but not for all classes. Instead, we'll define this method only for container elements: 

private fun Container.accept(feature: LinksCrawler) {

    feature.visit(this)

}

 // Same as above but shorter

private fun Page.accept(feature: LinksCrawler) = feature.visit(this) Our feature will need to hold a collection internally, and expose it only for read purposes. In Java, we would specify only the getter and no setter for that member. In Kotlin, we can specify the value without a backing field: 

class LinksCrawler {

    private var _links = mutableListOf<String>()

val links

        get()= _links.toList()

    ...

We wish for our data structure to be immutable. That's the reason we're calling toList() on it. The functions that iterate over branches could be further simplified if we use the Iterator design pattern. For containers, we simply pass their elements further: class LinksCrawler {

    ...

    fun visit(page: Page) {

        visit(page.elements)

    }

 fun visit(container: Container) = visit(container.elements)

    ...

Specifying the parent class as sealed helps the compiler further: 


sealed class HtmlElement

class Container(...) : HtmlElement(){

    ...

}

 class Image(...) : HtmlElement() {

    ...

}

 class Link(...) : HtmlElement() {

    ...

}

 class Table : HtmlElement() The most interesting logic is in the leaves: class LinksCrawler {

    ...

    private fun visit(elements: List<HtmlElement>) {

        for (e in elements) {

            when (e) {

                is Container -> e.accept(this)

                is Link -> _links.add(e.href)

                is Image -> _links.add(e.src)

                else -> {}

            }

        }

    }

Note that in some cases, we don't want to do anything. That's specified by an empty block in our else: else -> {}.

That's the first time we've seen smart casts in Kotlin. Notice that after we checked that the element is a Link, we gained type-safe access to its href attribute. That's because the compiler is doing the casts for us. The same holds true for the Image element as well. Although we achieved our goals, the usability of this pattern can be argued. As you can see, it's one of the more verbose elements, and introduces tight coupling between classes receiving additional behavior and Visitor itself.

Code Example :

package designpattern.visitor

fun main(args: Array<String>) {

val page = Page(
Container(
Image(),
Link(),
Image()
),
Table(),
Link(),
Container(
Table(),
Link()
),
Container(
Image(),
Container(
Image(),
Link()
)
)
)

println(collectLinks(page))
}

fun collectLinks(page: Page): List<String> {
// No need for intermediate variable there
return LinksCrawler().run {
page.accept(this)
this.links
}
}


class LinksCrawler {
private var _links = mutableListOf<String>()

val links
get() = _links.toList()

fun visit(page: Page) {
visit(page.elements)
}

fun visit(container: Container) = visit(container.elements)

private fun visit(elements: List<HtmlElement>) {
for (e in elements) {
when (e) {
is Container -> e.accept(this)
is Link -> _links.add(e.href)
is Image -> _links.add(e.src)
else -> {}
}
}
}
}

private fun Container.accept(feature: LinksCrawler) {
feature.visit(this)
}

// Same as above but shorter
private fun Page.accept(feature: LinksCrawler) = feature.visit(this)


class Page(val elements: MutableList<HtmlElement> = mutableListOf()) {
constructor(vararg elements: HtmlElement) : this(mutableListOf()) {
for (s in elements) {
this.elements.add(s)
}
}
}


sealed class HtmlElement

class Container(val elements: MutableList<HtmlElement> = mutableListOf()) :
                                                       HtmlElement() {

constructor(vararg units: HtmlElement) : this(mutableListOf()) {
for (u in units) {
this.elements.add(u)
}
}
}

class Image : HtmlElement() {
val src: String
get() = "http://some.image"
}

class Link : HtmlElement() {
val href: String
get() = "http://some.link"
}

class Table : HtmlElement()

Output :

[http://some.image, http://some.link,

http://some.image, http://some.link,

http://some.link, http://some.image,

http://some.image,

http://some.link]





No comments: