DEV Community

loading...

Observer Design Pattern tutorial in JavaScript + Fun With a Language API

Erik W
Software engineer and researcher specializing in software architecture, database design and development, testing, and methodologies.
Updated on ・9 min read

April 2021 Update: The website who issued the API key I use in this tutorial no longer offers free API keys, so the examples here won't work. I'm very sorry :(

Hello everyone and thank you for clicking on this article. I'm really excited about our tutorial today because it combines my 2 favorite things: programming and languages (the spoken kind).

I'm going to show you how to implement the "observer" design pattern in JavaScript, and then I'm going to demonstrate its usefulness in a web project. You are 1000% encouraged to take this code, tweak it, make it your own, and demonstrate it in your portfolio, because our end product is going to be pretty cool if I do say so myself. (This is what we're going to make)

Here's today's agenda:

  • Quickly talk about what is is the observer pattern
  • Talk about how we're going to implement it in JavaScript
  • The code
  • Where do we go from here?

What is...

..a design pattern?

In programming, design patterns are tried and true solutions to problems we always have. According to the ol' Wikipedia, they are

...formalized best practices that the programmer can use to solve common problems when designing an application or system

..the observer pattern?

The observer pattern is when we have one object controlling a list of subscribers. Officially, we call the subscription controlling object the "Subject" and the subscribers the "observers."

For a very simple and watered down example, think of the news agency BBC. The BBC will get news from everywhere, and then expect their different networks to report on it. They have BBC World Service reporting in English, BBC Arabic reporting in Arabic, and BBC Brasil reporting in Portuguese. In this case, BBC as a whole would be the subject, and the observers would be the various networks (World Service, Arabic, Brasil).

What are we going to do?

Glad you asked! We're going to create 2 JavaScript classes, News our subject, and NewsOutlet our observer. We will instantiate one News object and three observers, add the observers to the subject's subscriber list, and transmit data to all observers via the subject, translate it into a different language, and display it.

Sound confusing? I promise you, it's not. Lets just start coding, you'll see.

JavaScript ❤️

The Subject

First, our subject, we shall name it "News":

// The news class is the Observable class or "subject"
class News {
  // A list of observers
  constructor() {
    this.observers = [];
  }

  // Method for subscribing to, or "observing" observable
  addSubscriber(subscriber) {
    this.observers.push(subscriber);
  }

  // Method for unsubscribing from observable
  unsubscribe(subscriber) {
    var index = this.observers.indexOf(subscriber);
    this.observers.splice(index, index);
  }

  // Method for sending data to subsribers
  transmit(data) {
    this.observers.forEach(subscriber => subscriber.receive(data));
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok, so lets talk about this method by method:

  • constructor - Nothing special here, we just want to make sure that when news is created, it has an array of observers. We'll add them later.

  • addSubscriber - This is the method which will officially make the observer subscribed to this subject. The syntax to add a subscriber will be like Subject.addSubscriber(observer)

  • unsubscribe - Should go without saying, but the observer we pass to this function will stop getting data from the subject.

  • transmit - This is how the data is going to get passed to the observers. This method loops through the subject's array of observers, and calls the observer's receive method. Obviously, this means we must give the observer classes a receive method.

So, in a nutshell, that is a very basic Subject for our observer pattern. Lets now define the class that will make up our observers.

The Observer

Here's the code for our observer "NewsOutlet, we'll go through method by method

// The News Outlets are subscribers to the news in different languages
class NewsOutlet {
    // We will set the language when we instantiate the news outlet
    constructor(language = "en") {
        this.language = language;
        this.data = "";
        this.news = "";
        // For this example we'll use my API key, but please go to 
        // https://yandex.com/ and sign up to get your own
        this.apiKey = "trnsl.1.1.20190807T020501Z.f95163fde699ac87.1f9b3df7b5d7c045104d21249dc322086ee38004";
        this.translateUrl = "https://translate.yandex.net/api/v1.5/tr.json/translate";
    }

    receive(data) {
        this.data = data;
        var urlParamList = "?";
        urlParamList += "key=" + this.apiKey;
        urlParamList += "&text=" + this.data;
        urlParamList += "&lang=" + this.language;
        var self = this;

        // Translate after receiving
        jQuery.ajax({
         url: this.translateUrl + urlParamList,
         contenttype: "application/json",
         datatype: "json",
         success: function(result) {
            self.news = result.text[0];
            self.reportTheNews();
         }
        });

    }

    reportTheNews() {
        // A shady workaround for our HTML efforts!
        let elemId = this.language + "1";
        document.getElementById(elemId).innerText = this.news;
    }

}
Enter fullscreen mode Exit fullscreen mode

Alright! There's a lot to unpack here so let's go slow.

First of all, we are going to be using the Yandex API for translation. Please go to https://yandex.com to get your own API key, and swap it out with mine there before you run this too many times. It's free!

  • constructor - For the observers, we are going to give them a language when we instantiate them. We have to pass the ISO language code to the API to make this work (here is a list of all the supported languages). "en" is English and our default. We're making instance variables called data (the data from our Subject) and news (translated data). Finally, for the sake of ease, we're putting the API key and translation API URL in instance variables.

  • receive - Remember in the News class when our transmit method was calling the receive method of all our observers? Well this is it. This method is first taking the transmitted data from the Subject and putting it into its own data variable. Then, it builds the complete API URL that we'll use to translate the transmitted data.

  • The jQuery.ajax part - This is a pretty basic way to use public APIs. We assign the URL, content type, and data type, then say what needs to happen after a successful call to the API. Notice before this function the var self = this;. We did this because this will not be available in the ajax function, so we will use self. Don't let this confuse you, in some languages self is a reserved word. Not in JavaScript, you could call this variable almost anything you want. Anyway, once the API call has returned successfully, it'll set the news instance variable as the first element of the result.text object that comes back (it'll look like ["this is the text returned in an array for some reason"]). Finally, it will call the reportTheNews method, which you can see by the comment, is not something I'm super proud of.

  • reportTheNews - When we show this off in our HTML later, we will have some div elements displaying the news. If you want to see the results now in a console, change the function like this:

reportTheNews() {
    console.log(this.news);
}
Enter fullscreen mode Exit fullscreen mode

Almost done, lets recap

At this point, we have actually already built our observer pattern. If you want to see the whole thing in action, make sure to switch the code in reportTheNews like shown above, and write this into your console:

let news = new News;
let enOutlet = new NewsOutlet("en");
let ptOutlet = new NewsOutlet("pt");
let arOutlet = new NewsOutlet("ar");

Enter fullscreen mode Exit fullscreen mode

These lines create our subject news and observers enOutlet, ptOutlet, and arOutlet. Lets have our outlets subscribe:

news.addSubscriber(enOutlet);
news.addSubscriber(ptOutlet);
news.addSubscriber(arOutlet);
Enter fullscreen mode Exit fullscreen mode

And we're pretty much ready to go. Decide what you want the first headline to be. Mine is going to be "Erik is the best"

> news.transmit("Erik is the best")
< undefined
  Erik is the best
  إريك هو أفضل
  Erik é o melhor
Enter fullscreen mode Exit fullscreen mode

Just call me Mr. Worldwide 😎

That is it! That is the observer pattern. We transmit our data via the subject (news in this case) to our subscribers (the 3 **Outlet variables).

Now, I'm going to put this into something somewhat worth looking at, and you are more than welcome to follow along. But for the sake of "learning the observer pattern," you are done! Congrats, skip to the "Where do We Go From Here?" section

Live Demo!

Ready to put what we learned into practice? Here's what we're going to build:

Implementing the Observer Pattern like the demo above

Lets get to it. First of all, if you switched the reportTheNews function, switch it back to

    reportTheNews() {
        // A shady workaround for our HTML efforts!
        let elemId = this.language + "1";
        document.getElementById(elemId).innerText = this.news;
    }
Enter fullscreen mode Exit fullscreen mode

We will be using that after we make the HTML page. The reason this has been configured in a roundabout way is because of the way ajax can behave sometimes. I won't get into that yet because this project is not a great example of good ajax work, so lets press on.

The HTML part

Lets make an input where we can type in our news, a button to send it, and a few things to show us what the different outlets are saying:

<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" type="text/css" href="style.css">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
        <script src="News.js"></script>
        <script src="NewsOutlet.js"></script>
        <script src="implement.js"></script>
    </head>
    <body onload="defaultBreaking()">
        <h2 id="pageTitle">The World News</h2>
        <h3>JavaScript implementation of the Observer Pattern</h3>

        <div class="worldNews">
            <div>
                What's the news??
            </div>
            <div>
                <input type="text" id="sourceNews">
            </div>
                <button onclick="sendNews()" id="transmitter">Transmit!</button>
            <div>
            </div>
        </div>

        <div class="row">
            <div class="column" id="enOutlet">
                <div class="newsTitle">The US Gazette</div>
                <div id="en1" class="breaking"></div>
            </div>
            <div class="column" id="ptOutlet">
                <div class="newsTitle">The Portugal Post</div>
                <div id="pt1" class="breaking"></div>
            </div>
            <div class="column" id="arOutlet">
                <div class="newsTitle">The Halab Herald</div>
                <div id="ar1" class="breaking"></div>
            </div>
        </div>

        <div class="footer" id="Yandex">
            All translation Powered by <a href="https://translate.yandex.com/">Yandex</a>
        </div>
        <div class="footer">
            <p>by Erik Whiting</p>
        </div>

    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Note, if you're copy/pasting, you must name the other files we're creating the same as I have:

  • style.css
  • News.js
  • NewsOutlet.js
  • implement.js (we haven't made this one yet)

The CSS

Side note I really hate CSS but I'm trying to get better. I'm not designer by any means, so don't laugh at me:

* {
  box-sizing: border-box;
}

body, h2, p {
  font-family: Courier, sans-serif;
}

#pageTitle {
  font-size: 50px;
}

.worldNews {
  background-color: #262626;
  color: white;
  padding: 30px;
  text-align: center;
  font-size: 35px;
  border: 1px solid black;
  border-radius: 8px;
}

button {
  background-color: orange;
  font-family: Courier;
  font-color: black;
  font-size: 20px;
  width: 400px;
}

input {
  width: 400px;
  height: 40px;
  font-size: 30px;
}

.column {
  float: left;
  width: 33.33%;
  padding: 10px;
  height: 300px;
  border-radius: 8px;
  border: 1px solid black;
}

.newsTitle {
  width: 100%;
  text-align: center;
}

.breaking {
  text-align: left;
  font-size: 20px;
  border: 1px solid;
  border-radius: 8px;
  padding: 5px;
  margin: 2px;
}

#enOutlet {
  color: white;
  background-color: #820400;
}

#enOutlet > .breaking {
  background-color: #fc6f6a;
}

#ptOutlet {
  color: black;
  background-color: #008c00;
}

#ptOutlet > .breaking {
  background-color: #78f580;
}

#arOutlet {
  color: white;
  background-color: #000485;
}

#arOutlet > .breaking {
  background-color: #515cfc;
}

.newsTitle {
  font-size: 20px;
}

.row:after {
  content: "";
  display: table;
  clear: both;
}

.footer {
  background-color: #f1f1f1;
  padding: 10px;
  text-align: left;
}

@media (max-width: 600px) {
  .column {
    width: 100%;
  }
}
Enter fullscreen mode Exit fullscreen mode

If you're going to make this into your own project, play around with the format and colors, make it your own, show people your artistic side!

The script that ties it together

Ok, last thing's last, we need a little script to kinda put everything together. I called it implement.js for no other reason than I couldn't think of a better name:

// Create all the objects we'll use
let news = new News;
let enOutlet = new NewsOutlet("en");
let ptOutlet = new NewsOutlet("pt");
let arOutlet = new NewsOutlet("ar");

// Subscribe
news.addSubscriber(enOutlet);
news.addSubscriber(ptOutlet);
news.addSubscriber(arOutlet);

// A function for setting the news elements to prompt user to type
// you don't need this if you don't want, just remove the onload
// from the HTML file if you remove this
var defaultBreaking = function() {
  var breaking = document.getElementsByClassName("breaking");
  for (let item of breaking) { item.innerText = "Type some news..."; }
}

// The function that will call transmit on news
// setting off the chain event of subscribers receiving
// then translating, then showing the news
var sendNews = function() {
  let theNews = document.getElementById("sourceNews").value;
  news.transmit(theNews);
  news.observers.forEach(function(o){
    o.reportTheNews();
  });
}
Enter fullscreen mode Exit fullscreen mode

And that's it. If you've been copy/pasting, then your site should look like mine. If you're getting a weird error about cross site scripting or something, and you're on windows, open run.exe and type this:

chrome.exe --user-data-dir="C:/Chrome dev session" --disable-web-security
Enter fullscreen mode Exit fullscreen mode

but be careful with that and don't do that often.

Where do We Go From Here?

Design patterns are awesome and knowing them will make you a better programmer, I don't care what anyone says. Just like with data structures and algorithms, when you at least know of design patterns and their general use, you will have a list of solutions in your head to many common problems.

One example I always see when people are talking about the observer pattern is trading. Say a ticker symbol has gone from 1.02 to 1.14, and there's a thousand different interfaces that need to be alerted. Ideally, in whatever server this kind of thing is running in, each one of those interfaces would be subscribed to the object broadcasting the ticker symbol's price.

The example we used is very simple, and there's lots of things you can do with it. For example, I'm pretty sure the KnockoutJS framework (hi C# developers!) was built entirely on this concept. This is just one example of a pretty cool design pattern, there are several more to learn.

If you liked this tutorial, please tell me what you liked about it and what you'd like me to talk about next. A new pattern? A different language? A new pattern in a different language? Something else entirely? Let me know and please don't hesitate to ask me any questions.

Discussion (0)

Forem Open with the Forem app