Really Good JavaScript: Part 2/3 - Cogent

Really Good JavaScript: Part 2/3

Craig Ambrose

Craig Ambrose

Crafter of code, and idealistic change-maker.

When and How to use Object-Oriented Programming in JavaScript

In Part 1, we looked at how JavaScript is fundamentally a procedural language, and how writing really good JavaScript starts with an understanding of how to write clean procedural code on a line-by-line basis. Of course, if you’ve programmed at any point since 1980, you’re probably familiar with Object-Oriented Programming (OOP). Is JavaScript an OO language? Should we use OOP techniques in JavaScript, and if we do, how do they differ from other languages?

Object-oriented features in JavaScript

While the first OOP language was Simula in 1962, my introduction to these techniques was mainly C++, which was first released in 1980. C++ popularised Object-Oriented Programming, and greatly influenced Java which came along fifteen years later, in time to be an incredible popular language for the internet age.

It’s hard not to compare Java to JavaScript, but the naming similarity is not due to any relationship between the languages but rather by a marketing deal struck between Sun Microsystems and Netscape. Languages like Java make heavy use of classes as a way of creating objects, and also for type-checking that objects conform to a declared interface. More recent interpreted languages such as Ruby and Python forgo the static type-checking but use classes as the foundation of their object-oriented style.

JavaScript, by contrast, focuses more on objects than classes, and solves the problem of how to create objects by using prototypes which can be copied rather than classes. Until 2015 there wasn’t even a class keyword in JavaScript, and now that there is one it’s still implemented as syntactic sugar that uses the prototype mechanism under the hood.

Object-Oriented programming in no way relies on classes. As long as you can somehow build objects that encapsulate both data and behavior and can be use polymorphically, you can programming in an OO way. JavaScript developers tend to use class-based features, such an inheritance, less than developers in a language like Java, but all the principles of good OOP still apply.

What should I do with prototypes?

In theory, prototype-based OO is quite different to class-based, and there are plenty of academic articles discussing the various pros and cons. I’d like to present the slightly controversial point of view that you shouldn’t pay too much attention to prototypes except for the overall understanding that it’s how classes are implemented.

In the wild, you rarely see code that takes advantage of the prototype system to do anything very different to what you’d see in any other object-oriented language. If you’re going to use OOP techniques in your JavaScript, just use the ES6 class keyword and write code as you would in any other dynamically typed OO language.

Missing OO features

Even compared to other similar languages, JavaScript is arguably missing a few fancy features.

Private methods

Private methods are important only insofar as they are functions that you want to add to a class because they operator on the object’s data members, and yet you don’t want to confuse people who might assume that they are part of the object’s public API than they can depend on.

Having this check imposed at compile or run time isn’t all that important. Some JavaScript developers like to prefix such methods with an underscore. Others don’t name them differently, but put them below a /* PRIVATE */ comment in the file to make it clear to a reader. Any of these techniques work, just pick something and stick to it.

Multiple inheritance

JavaScript doesn’t have multiple inheritance, and I can’t say  I’ve ever really missed it. Inheriting a class from more than one super-class is often confusing, and so even in the cases where it is an elegant solution other developers might look on it with suspicion. Inheritance hierarchies are not uses as much in dynamically typed OO languages. Instead, you simple want to add behaviour to your objects, not identity. I recommend merging functionality into an object from another using Object.assign(), in much the same way I would use include in ruby.

Interfaces

JavaScript has no way to specify that a given object is an implementation of a particular interface in order to provide statically type-checked polymorphism. Despite what you learned at school, this may not matter that much. Much like Python, Ruby, and many other dynamic languages, JavaScript OO relies heavily on duck-typing. This gives the programmer a lot of flexibility. If you do want type checking, and perhaps you do on larger projects in particular, then you can achieve it with Flow or Type Script as I describe in my article on type safety.

So, what does “good” OO look like?

With our code-reviewer’s hat back on, lets imagine that we’re trying to evaluate the quality of a JavaScript code-base written using a lot of OO techniques.

Much has been written on what makes up good object oriented design, lets go through a some general principles from a JavaScript point of view.

SOLID

The SOLID acronym is a mnemonic to help us remember some important design principles for our OO software.

ingle responsibility principle

Each of our JS classes should have do one thing well. Keys to ensuring that this happens include naming our classes well, and not letting them get too long.

pen/Closed principle

A class should be open for extension but not modification. Examples of this in C++ and Java lean heavily on the use of abstract base classes (interfaces). In JavaScript, we don’t need to manually create such interfaces, but the theory still stands. Keep an eye out for objects that know too much about the internals of other objects to spot this one.

iskov substitution principle

Objects in a program should be replaceable with their sub-types. In JavaScript, where we may not use inheritance as heavily, it is also true that they should be replaceable with their “duck types” (other objects with the same type signature)

nterface segregation principle

Many client-specific interfaces are better than one general-purpose interface. This will actually come quite naturally to JS developers. Whether you are declaring type interfaces with something such as Flow, or just relying on implicit ones, you don’t need to rely on the entire public API of a class, just that bit that matters for this application.

ependency inversion principle

Depend on abstractions, not concrete implementations. In a dynamic language like JS, we basically get this for free.

Cohesion and Coupling

If the above points seem a little complex or rooted in statically typed languages, maybe it’s better to instead focus on the general point that our objects should demonstrate high cohesion and low coupling.

Cohesion is the extent to which methods of an object are related to each other. Having high cohesion is much the same thing as obeying the single responsibility principle. One way to check cohesion is to look at which parts of the object’s data each method touches. If a certain group of methods use several data members, and another set of methods use another set of data methods that don’t overlap much or at all, then you have low cohesion and should potentially have two classes instead of one.

Coupling is the extent to which two different objects are dependent on each other. The “L”, “I” and “D” in SOLID are all strategies for keeping coupling low.

When do we use all this?

Good software design is mainly about maintainability. Well designed software is easier to read, reason about, and modify, which makes it easier to return to the code later to fix bugs and add new features.

Code that is easier to modify often has more abstractions, but until we know that we need to modify that part of our code, building highly generalised code can be less readable. Thus, our code often starts quite concrete, and becomes more abstract or general in the places that appear to change often, or we learn are likely to change often.

The process of building good software thus relies on our ability to refactor code so that it starts of fairly specific to today’s need, and becomes more abstract in the parts that change. We do that using refactoring techniques, and we look for opportunities to refactor based on noticing bad smells in the code.

So, if we’re reviewing object oriented code, as well as looking for examples of good design, we need to keep an eye out for smells that haven’t yet been spotted and refactored.

If someone has worked on all the steps for writing good procedural code that I discussed in Part 1, then they will probably begin to notice some smells that can be solved using Object Oriented techniques.

The first one you’re likely to notice is duplication. Perhaps you’ve seen some JavaScript like this before.

There’s a bad smell here, and we can spot it in a number of different ways. It could be because the high level of cohesion between x and y indicates that they should be in an object together, but the easiest way is simply the duplicate. That’s right, the expression posX + (directionX * delta) is more duplicate code than we should tolerate, we need a 2D vector object here.

A 2D vector object is a classic example because it comes up so frequently in front-end JavaScript development. There are lots of other simple little objects that will make your code better, to represent things just a bit more complex than can be stored in a single variable of a basic type. You might want to use the Value Object pattern for such objects.

Bringing it all together

Good Object-Oriented JavaScript uses OOP techniques to solve code smells that are otherwise hard to solve in procedural code. It’s not an all-or-nothing proposition. It probably doesn’t need a dependency injection, much inheritance, or enterprise application design patterns.

Use OO to make your procedural code easier to read by building up from basic types to give them more functionality. Use encapsulation more often than inheritance. When dealing with detailed objects of basic types, such as those serialised to JSON, consider applying decorators before you use them in order to give them some needed behaviours.

Up next, is Functional Programming the new black?

In part 3, we’ll take a look at functional programming techniques and see if and when they should be applied to your JavaScript.

Yep, I’m busy with food again. I’ll see you back here for part 3.

Related posts