Dean Martin

The Decorator Pattern in Go

When I was writing my RSS notification plugin for my chat bot, walter, I ran into a problem when displaying notifications to users. It was litered with HTML.

This was a good opportunity to use the Decorator pattern, considering I already had an interface defined too.

Overview

Let’s take a step back and give a quick summary of what the RSS module wants to achieve. The RSS plugin was created to notify users of new RSS feed items. Users may subscribe to a feed in a channel with specified keywords. For example,

pseudo client

#chat
<gh0st> !subscribe darknet_diaries security,privacy,crypto
<bot> Subscribed to darknet_diaries with keywords: security,privacy,crypto

darknet_diaries is a RSS feed that was added by a bot administrator. In this case, it links to Darknet Diaries’ RSS feed here: https://feeds.megaphone.fm/darknetdiaries

Notice that the user is subscribing in a specific channel, #chat. A subscription is unique to a user, channel, and RSS feed. This means that you can subscribe again to darknet_diaries in a different channel, but not again in #chat.

Every thirty minutes, the bot will parse all the feeds, then find all the subscriptions for each feed, and then start keyword matching. It will pluck out all the Items that match the user’s subscription, and create a pending notification. Once finished, it will send all notifications to the appropriate channels.

There’s some more nuance to it, but not important here. (limiting notifications per channel, ignoring already seen Items, etc)

Code!

With a rough understanding of the core concept of the plugin, let’s dive into some code.

At some point in the process I laid out earlier, we need to download a Feed’s URL and parse it into a more friendly API.

To do this, I created a Parser interface:

// Parser downloads a Feed.Url and translates it to a ParsedFeed to
// be checked by a Subscription.
type Parser interface {
	ParseURL(string) (*ParsedFeed, error)
}

A Parser returns a ParsedFeed struct, a Domain Entity created to separate the domain from external libraries.

By creating the internal ParsedFeed struct and Parser interface, we’re able to separate the core from third party packages, the outside world.

This enables testing to be isolated. By defining an interface, I can then implement a StubParser

as a dummy in my testing. Then have it return the expected ParsedFeed in the test case.

If the tests had to actually http download a feed url, too many things could go wrong.

Good architecture leads to good tests.

This Parser is used by the Processor, the struct that actually goes through and processes user subscriptions:

func NewProcessor(repo Repository, parser Parser) *Processor {
	return &Processor{
		repository:   repo,
		parser:       parser,
	}
}

In this case, I used the gofeed library as the main implementation. It worked great, but when trying to display a simple Notification as a string:

type MinimalFormatter struct {
}

func (m MinimalFormatter) Format(n Notification) string {
    i := n.Item
    return fmt.Sprintf(
        "%s - %s : %s",
        i.Title, i.Link, strings.Join(n.Users, ","),
    )
}

Long anchor tags, spans, etc would pollute the output. Not legible output.

The Decorator

Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors. 1

Let’s look at a simple example of a Decorator first:

type Formatter interface {
    Format(str) string
}

type MyDecorator struct {
    wrappee Formatter
}

func NewMyDecorator(formatter Formatter) *MyDecorator {
    return &MyDecorator{wrappee: formatter}
}

type (b *MyDecorator) Format(str string) string {
    // I can do whatever I want before,
    strings.Trim(str)
    str := b.wrappee.Format(str)
    // Or after I call the wrappee!
    // No yelling, only questions?
    str = strings.Replace(str, "!", "?")

    return str
  }

MyDecorator accepts an existing instance of a Formatter, and then applies its own logic before, and after the wrappee call. This allows you to extend and build layers of behavior while having organized, decoupled code. Pretty nifty.

After some digging around, I found the bluemonday package. Exactly what I needed. All I had to do was create a StripHTML Decorator, and this problem would be solved!

The StripHTML Parser Decorator:

strip_html.go

package decorators

import (
    "html"
    "reflect"

    "github.com/microcosm-cc/bluemonday"
    "github.com/dean-martin/walter/mods/rss"
)

type cleanHtml struct {
    // The "wrappee".
    BaseParser rss.Parser
}

// Create a new StripHtml Parser that wraps around another Parser.
func StripHtml(p rss.Parser) rss.Parser {
    return &cleanHtml{BaseParser: p}
}

func (s *cleanHtml) ParseURL(url string) (*rss.ParsedFeed, error) {
    // Call the BaseParser with the given URL and get its ParsedFeed.
    feed, err := s.BaseParser.ParseURL(url)
    // If it failed, just return that error, nothing for us to clean.
    if err != nil {
        return feed, err
    }

    // Strip the base struct fields.
    stripHtml(feed)
    // Then all its Items.
    for _, i := range feed.Items {
        stripHtml(i)
    }

    return feed, nil
}

// Use reflection to find all the string fields on the struct,
// and then HTML escape & sanitize them.
func stripHtml(any interface{}) {
    p := bluemonday.StripTagsPolicy()
    r := reflect.ValueOf(any).Elem()
    for i := 0; i < r.NumField(); i++ {
        f := r.Field(i)
        if f.Kind() == reflect.String {
            f.SetString(
                html.UnescapeString(p.Sanitize(f.String()))
            )
        }
    }
}

Bringing it all together

// ...
rss_plugin.New(rss.Context{
    // Create a new gofeed Parser as the base, and then
    // wrap a StripHtml Parser around it.
    Parser:     decorators.StripHtml(gofeed.New()),
    Formatter:  rss.MinimalFormatter{},
    Repository: sqlite.NewRssRepository(db),
}),
No more HTML in notifications.

Wrapping Up (Pun Intended)

The Decorator pattern, is a simple, but effective pattern to layering behaviors. It abides to the Open-Close principle, adding new functionality by extending exisiting software, instead of changing it. As well as organizing behavior.

Some pitfalls however, in the Pros and Cons section here are worth mentioning.

Most notably:

Thanks for reading.


  1. The above quote is excerpted from refactoring.guru ↩︎

Tags: