Register to get access to free programming courses with interactive exercises

Configuration JS: Object oriented design

Markdown is a simplified markup language that's easier to use with text (as opposed to HTML). Browsers cannot display Markdown directly, so it's translated into HTML and then rendered. The translation of Markdown to HTML is described by a pure function. It's independent of the external environment, deterministic, and produces no side effects.

html = markdownToHtml(markdown);

The input is text (in Markdown format), and the output is also text (in HTML format). If you want to change the behavior of the translation, you just need to pass an array of options as the second parameter.

// the second parameter is to pass the translation options
// `sanitize` — the flag responsible for making sure the rendering is done safely
// If you turn it off, the tags `<script>` inserted in Markdown will be displayed as is.
const html = markdownToHtml(markdown, { sanitize: false });

Now let's imagine an object-oriented version of this code. Before moving on, try to take a break from the reading and think about the following questions:

  • What exactly do we want from OOP that a pure function doesn't give us?
  • What will the resulting interface look like?

As you remember, classes allow you to implement abstraction. Can we say that there's an abstraction in the Markdown to HTML conversion process? No. Abstraction implies the existence of a concept (type) whose values have a lifetime. This means that it's created and then used repeatedly and in different ways. For example, it's impossible to present the work with the user as a single function. When it comes to Markdown, we're not interested in any particular text in that format per se, we don't define a set of operations on it, and we don't intend to actively use it. All we want, right here and now (in that code) is to get HTML and forget about Markdown.

If we wanted to build an abstraction around the text, the code would look like this:

// The md object describes the passed markdown text and allows you to manipulate it
const md = new Markdown(markdown);
const html = md.render();

In the example above, the Markdown type represents an abstraction over text in the Markdown format. The code doesn't make much sense and causes problems as a result. These two lines will inevitably start to occur anywhere where you want to get HTML. The md object immediately becomes unnecessary as soon as the HTML is received, it doesn't have a lifetime. This antipattern is especially common among beginners. The tricky part here is figuring out exactly where we have data abstraction and where we don't.

// Typical redundant code in a place where abstraction has been done but is not needed
const md1 = new Markdown(markdown1);
const html1 = md1.render();

// Once again, to reinforce
const md2 = new Markdown(markdown2);
const html2 = md2.render();

There's a formal rule to determine this. If the object creation and method call can be replaced by a standard function, then abstraction is out of the question, and the correct approach, in this situation, is to transfer data from the constructor to the method itself.

const md = new Markdown();
// it's very important that render remains a pure function and doesn't keep the markdown inside the object
const html1 = md.render(markdown1);
const html2 = md.render(markdown2);

In this code, the Markdown class is a type that refers to the translator, not the text. Such an object has a wider lifecycle than waiting for the render() function to be called once (as in the previous case). It can (and should) be reused as many times as needed. To do this, it's important to keep render() pure and not to change the state of the object between calls.

That would make it unclear as to why the object is here at all. And there are two reasons for that.

  1. Polymorphism of subtypes. We'll deal with that in the courses that follow.
  2. The second and main reason (for this case) is Configuration.

Let's look at the last point in more detail. Imagine that Markdown is used everywhere in the project (it's used on Hexlet very often) and the HTML generation code looks like this:

// In one place
const html1 = markdownToHtml(markdown1, { sanitize: true });

// Elsewhere
const html2 = markdownToHtml(markdown2, { sanitize: true });

The more such places arise, the more times the passing of options is duplicated. Changing the behavior would require all the places where this function is called to be rewritten. The logical step would be to set the options in one place and then reuse them.

// In one place
const html1 = markdownToHtml(markdown1, options);

// Elsewhere
const html2 = markdownToHtml(markdown2, options);

Using an object allows you to remove explicit passing (which is easy to forget about). The essence of this pattern is configuration. I.e., the object in this case acts as a container that contains options for Markdown that are applied during rendering, so you don't have to pass them every time.

const md = new Markdown({ sanitize: true });
const html1 = md.render(markdown1);
const html2 = md.render(markdown2);

Configuration always refers to passing options (various settings needed by a given library) to the constructor at the point the object is created. This configuration is especially useful when the object is created in one part of the program (during the initialization phase of the application) and used in other places. The ability to use configuration doesn't mean you have to use it. As a rule, such objects can be created without specifying anything, the default behavior remains, but the meaning doesn't change.

const md = new Markdown();
const html = md.render(markdown);

Axios is a popular library for HTTP requests, and is built on the same principle. It allows you to create an object that saves a basic configuration.

Try it for yourself. Is executing an HTTP request a data abstraction or not?

import axios from 'axios';

const client = new axios.Axios({ timeout: 1000 });
const response = await client.get('https://ru.hexlet.io/lessons.rss');
console.log(response.status);

This technique is not the prerogative of classes and objects. In functional languages (and in JS) it's extremely easy to implement using closure.

import axios from 'axios';

const getConfiguredRequest = (config) => (url) => axios.get(url, config);

const request = getConfiguredRequest({ timeout: 1000 });
const response = await request('https://ru.hexlet.io/lessons.rss');
console.log(response.status);

Recommended materials

  1. MarkdownIt library

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

Bookmate
Health Samurai
Dualboot
ABBYY
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.