SOLID JavaScript
(bouncy music) - Thanks very much and welcome.
And thank you everyone for having me here this afternoon, and coming on here.
My name is Steve.
And I seem to be the odd one out, because I'm a full stack developer.
And I'm not talking about GraphQL today.
What I am talking about is SOLID.
The SOLID principles that help us improve our object oriented designs.
It's a beginner type one on one talk with lots of code fragments throughout.
As we clear, this talk wasn't actually my idea.
It was someone else's.
And I thought, sure, five principles, 20 minutes.
Challenge accepted.
It turns out for you lucky folks that 20 minutes isn't actually a long time. I mean, I don't know what I was thinking.
So if you have any questions, please hang on until the end. SOLID is a little like database normalisation. There are several rules that help remove bugs that arise from data inconsistencies.
How many of you are familiar with data normalisation? A few, fantastic.
It's a little like that.
Except it has nothing to do with data or databases. And everything to do with objects and their behaviour. SOLIDs particularly useful in languages like C# and Java, with strongly typed class based inheritance, which is what most people think about when we're talking about object oriented analysis. But in JavaScript, we often seem to forget about the rules.
For the benefits, it seems particularly easy to shoot yourself in the foot and just create a big bowl of spaghettified mud. And it's not that other languages are better, they're not. They're just different.
And we're here today to talk about how we can apply those learnings to JavaScript to make our lives easier.
Now, I'm not here to pick on JavaScript And it's worth noting that it is an Object-Oriented Language, that meets the three key requirements.
Inheritance of course, is right to extending the behaviour of a class. A Kangaroo extends must reveal extends mammal ,extends animal.
Polymorphism is all about looking at the objects through different lenses.
A kangaroo as an animal, as a mammal, or as an animal.
And encapsulation is about keeping closely related functionality in one place. Marsupials are different from arachnids.
Mammals are different from mollusks.
In JavaScript, it supports all three.
Inheritance prerototypes, polymorphism to duck typing and encapsulation through classes.
We only got classes recently with the S6, but they've always been other ways, such as the constructor functions and the revealing module pattern.
And just a quick reminder, objects of state and behaviour.
State is what they are, behaviour is what they do.
This means that they have methods and functions to interact with, to model what they do and how they do it.
An object with just properties and no methods doesn't do anything.
It serves a purpose, sure.
Perhaps as a data transfer object.
But there's no behaviour.
It's just data.
Object Oriented Design is about modelling objects from the real world.
And real world objects have state and behaviour.
A car might be running, stopped or parked.
But it can also accelerate the steering brake. An engine of chassis of gearbox with that, their associated behaviour is broken.
And we need to get out and push.
Which is exactly what happens when our objects don't model their own behaviour. We end up poking them around from the outside with some kind of car manager.
Or car loader or CarMarshal.
And a few places where that's what we should be doing. Objects of state and behaviour.
And that's important because the SOLID principles have a strong emphasis on that behaviour.
So, what actually is SOLID? Five principles S for single Responsibility.
O for Open Closed L for Liskov Substitution.
I interface segregation.
And D for Dependency Inversion.
We'll spend a few minutes on each.
Starting with this, Single Responsibility, which states that, an object should have just one business reason to change. It's credited to Robert C. Martin.
We've all heard the phrase high cohesion and low coupling to describe good object design. Yes? Couple of notes? Okay.
We want objects that are tightly encapsulated pieces of functionality with only a few connections to other objects. single Responsibility is all about the high cohesion, ensuring that objects don't do more than they should. We all know to avoid a giant monolith that does all the things.
But even though we know we should be isolating behaviours, we often do so by layering them through inheritance, rather than by assembling them with composition. Objects of state and behaviour.
And when we build multiple layers of inheritance to support all the variations of what a thing might be, we ignore what it does.
Tim Halt spoke about this much better than I yesterday describing component libraries at the ABC with killer robot dogs, and banana gorillas. If you didn't see it, check out the talk when the video comes out. Yeah, show me describing computer like the previous one is clearly contrived because it's obvious we have components such as disc memory and network.
And the business reason for upgrading the disc would be that we've outgrown its capacity.
The business reason for upgrading the ram would be that we want more performance.
And the network card might just be too slow for a modern network demands.
There's only one reason to change any of those components. But even though the example is obvious and contrived, we're often not good at identifying those types of components.
single responsibility emphasises the need to make things simpler by isolating behaviour into smaller distinct classes with one reason to change.
Which in turn makes it easier to understand and maintain the system.
And when we also favour composition and inheritance over way, we also favour composition by inheritance and assemble objects from the things they do. Life just gets better.
If you take it one step further and keep each class in its own file, we also reduce the chance of a merge conflict. It's not for everyone, but a big advocate of one type per per file. Because it's just better for me in terms of clarity, focus, cohesion, and discoverability.
Regardless, the Single Responsibility principle is a gateway to a lot of good things.
I am what I am and that's all what I am.
Popeye wasn't a sailor, he was a programmer. And he's been telling us about Single Responsibility since the 1920s.
Okay,O is for Open Closed.
The Open Closed Principle says that we should leave an object open for extension, but closed for modification.
Bertrand Meyer is credited with this one.
And in JavaScript, it's easy to think we've done this simply by extending a class.
C# is the the same.
That's no notes such as there.
Maybe,a kangaroo extends an animal.
And when we move we hope. Right? At its core, this seems to be the original concept of Open Closed, which seems to have focused on inheritance. But if we forget to call the superclass, or deliberately choose not to, the behaviour of our object has now changed. And there's no guarantee that it still does what it was originally meant to do.
It's not actually closed for modification.
And so Open Closed has since evolved to focus on the interchangeability of key pieces of behaviour.
To do this, we use patterns like the Template Method, Strategy or Visitor, where we define the skeleton of the algorithm and defer the implementation to something else, not committed override.
For richer for all maybe, where the action to be performed is defined by the consumer. Or maybe a traversal or sort algorithm that we can swap at runtime.
Now, very few of us genuinely ends up swapping algorithms at runtime. But we all deal with logging.
And logging frameworks are open for extensibility. A console sync, a file sync or something on the on the network somewhere, and then closed for modification.
Open Closed is almost like a Plug-in model. And it's about allowing the capability of an object to be extended without it having to be changed. It's extensibility without modifications.
L, Liskov Substitution.
Now, okay, we're at a point where have lots of little objects and the ability to inject new implementations to extend our behaviour through Open Closed. We know we favour composition over inheritance. And we can also add new.
And we know we favour composition over inheritance. So how do we make sure that when we do extend our systems behaviour, we do it in a way that doesn't break anything? The Liskov Substitution Principle states that, so this one's more of mouthful, objects in a programme should be replaceable with instances of their subtypes without altering the correctness of that programme. Barbara Liskov is responsible for this one. It's about not changing the behaviour of an object to something that we're not expecting. And it's about ensuring that the programme remains correct when we extend it. Or don't break things with inheritance.
It seems obvious, but can be subtle.
And it's annoying because it goes hand in hand with faulty abstractions. We never realise we broken Liskov until we have a problem fitting a new behaviour into our existing models.
If anybody reads up on this, you'll see examples about squares and rectangles and birds and penguins, maybe.
But who's seen Scrubs? Do you remember the knife wrench? Yes, it's clearly wrong, right? But why? It's not because we've strapped a knife to a wrench. It's because we can't trust the wrench anymore we've changed its behaviour to do something that we weren't expecting.
It used to be safe for the Janitor to stick the wrench's pocket.
But now when he pockets it, that very same wrench will stab him in the fire. Okay, how about Thor and Loki, who disguised himself as a snake when they were kids. Then waited for Thor to pick him up because Thor like snakes, then changed back into Loki.
And stabbed his own brother.
Or Julius Caesar giving a hug to his friend Brutus, who hugs him then stabs him in the back.
Yes, there's a theme.
They all look legitimate, but they all cause us pain because they do things we weren't expecting them to do. We change the behaviour from the original and can't trust them anymore.
The examples are clearly contrived.
But here's when we might actually see in our normal jobs. A note of transaction.
Maybe someone wanted it for some in-memory testing. But we've clearly broken the intent.
The original intent of a transaction.
And we've done so because our abstraction is wrong. This isn't the place where we should be doing the in-memory thing. It's probably in the repository somewhere.
With great power, comes great responsibility, or Liskov of substitution.
Don't break things with inheritance.
I is for interface segregation.
The interface segregation principle states that, many clients specific interfaces are better than one general purpose interface. This one's also from Robert C. Martin.
And it's important to note that interface here means interactions rather than the interface constructs that we find in other languages.
Interface segregation is about reducing entanglement between objects by having a smaller surface area of clear, concise interactions.
Remember high cohesion and low coupling.
This is all about the low coupling.
And Martin describes a particular solution to a particular problem.
He was on a project working with printer software and was consistently frustrated by having to deal with a massive God object with all the things, all the responsibilities, all the properties, all the methods, all the interactions.
This examples, obviously contrived and I assume the real one had many, many more. And the problem was that someone building a feature for a thin slice of behaviour, the stapler, the facts, or whatever, had to wade through everything on that got objects just to find the few pieces of functionality that they cared about.
He tried to make the job easier by identifying subsets of related functionality and extracting them into separate classes.
He built them as a facade of sorts, each which wrapped the original printer object and exposed only the pieces of functionality that they cared about.
I assume he did do that particular way with the facade because he had additional constraints with legacy code. Because interface segregation isn't about the wrapping of those objects.
It's about defining the small, discrete interfaces for the win.
And that's exactly what his outcome was.
It's almost like Single Responsibility, but for interactions.
And just like Single Responsibility, interface segregation is a design concern that makes our lives easier by producing, by helping produce smaller classes with fewer interactions. We wouldn't genuinely consider a single class on a system that does absolutely everything.
We all know that's insane.
And the merge conflicts alone, my God, that would be horrifying.
But, we're also often not very good at identifying and slicing our concerns into small enough pieces. And while we don't want one file per function, that's probably left pad is .
We want something in between that defines, that helps define our behaviour in the small discrete pieces of functionality. And that's where this comes in.
Descartes said, I think, therefore I am.
If we take that and apply it here.
We get something more along the lines of, I do the thing, therefore I am Interface segregation favour lots of small interfaces over one large one.
Finally, D is for Dependency Inversion.
Any guesses as to who's responsible for this one? Any guesses? Take? Fine.
It's Robert C. Martin again.
Not to be confused with dependency injection. Dependency inversion states that, we should depend upon abstractions, not concrete implementations.
So we code against the superclass.
The animal, instead of a kangaroo.
Or the car instead of the Toyota.
It's a good object design.
It's supposed to work of course.
But in JavaScript, it's very easy to break this principle. Because if we have something like a logo, with standard info, warning and methods in our methods, when we extend it with something extra, then newest up and passed around.
By default, we get access to all the additional methods, which is great.
But remember interface segregation.
Lots of small interfaces over one large one. In this example, DOFOOD is given the large interface.
It doesn't actually need the methods on the smiley logo. And we've no way to exclude it from that code to restrict it for just original logo.
It might not seem like a big deal here.
But limiting the interface to the original abstraction reduces the complexity and the coupling between those objects.
Which makes in turn, makes our code simpler and allows us to swap implementations, the Open Closed Principle with ease.
But JavaScript is duck typing.
Which lets us use an object any way we want without any real guidance as to what we should be limiting ourselves to. And this is an exceptionally powerful feature. We should also be trying to make it easy to do the right thing by making obvious the shape of the object that we're expecting.
I looked into all sorts of things.
Adapters, proxies, decorators, linting, assertions, or God's but they all had shortcomings.
And where I landed is objects destruction, which lets us easily bind and objects properties or methods to a set of local variables.
And so in this example, invoking info as a local function has the same effect as invoking it directly on the logo object.
And when we take this feature, and move it into a function declarations like this, we get something that describes the expected interface in code.
In the actual function signature.
And I like this.
Because, it's self documenting, and it reveals our intent.
And feels like the closest thing we have to begin with to declare a dependency on the logo itself. Or at least its interface.
It's not foolproof, we can destructure something that doesn't exist and get an undefined.
So if we want to throw an exception when something's missing, we still need to do it the old fashioned way with God's and assertions.
Or maybe decorators.
Nevertheless, limiting our interactions to just the logo abstraction helps reduce coupling between objects.
And now it's even easier to extend and maintain our system. Dependency inversion.
Depend upon the abstraction, not the implementation.
So, bringing it all together.
Who likes Batman? I hope I don't annoy you too much then.
He's got a few gadgets.
One of them is the grapnel gun, which fires a grapnel hook on another cable up buildings or over walls.
Helps him escape and reach difficult places. It's fictional.
But we assemble it with the composition not inheritance of a few key behaviours and components. There's a chassis, propellant, cable, and hook. Each of those key things has a single reason to change. The chassis might be upgraded to a lighter material. The cable might be changed from some lightweight monofilament that holds exactly one Batman to a thicker cable that might carry two people. The propellant might be changed from a quiet, short range, stealthy cartridge to something with a bit more oomph that can reach 100 metres.
Or maybe the grapnel hooks can change to something that will actually punch through a wall, open up and lock themselves back into place. The point is, each of the objects in the system has just one reason to change.
And the system is open for extensibility and closed for modification.
We've just described how we can extend its capabilities without changing the chassis.
How we can add new propellant, new cable or hooks that we never thought of at the time we built it. That's the Open Closed Principle, right there. Now, Liskov.
We need to be able to trust the system when we extend it. Traditionally, Batman doesn't kill people nor does he to use real guns.
So if we take the grapnel system and change one of the hooks to fire a bullet, we break Liskov.
We've changed the behaviour and the intent of the original design.
Batman doesn't kill people but the grapnel system does? No, no.
Interface segregation.
We're already favouring lots of small interfaces. The propellant, the hook, the cable.
The chassis knows exactly what pieces were and how to interact.
Hooked up release, propellant to fire.
We've reduced complexity, and we've managed to keep those pieces small and simple. And as for dependency inversion, the system doesn't require the long distance propellant to work.
It cares only that, any propellant cartridge has been loaded.
Likewise, any hook and any kind of cable.
And as long as they all respect those underlying abstractions of propellant hooking cable, the system just works.
Batman's design team rocks.
So there we are folks.
The five SOLID principles in 20 minutes.
It's all about good object design.
Like I said, it's a little like database normalisation except for object behaviour.
And just like normalisation, some of these things are intuitive and obvious, and some are more subtle.
But if we stick to these principles, we find ourselves with simpler, cleaner systems that are much more enjoyable to work on.
Let's use SOLID to help our JavaScript be better. Thank you for listening.
(audience applauding) (bouncy music)
While the web’s been around 30 years, and JavaScript more than 20, it’s only in relatively recent years we’ve been using JavaScript, and other web technologies to build complex, sophisticated applications.
But the discipline of Software Engineering has been grappling with this challenge for well over half a century, and we Front End Developers can learn a great deal from the patterns and practice that have emerged from that discipline.
In this presentation we’ll focus on SOLID, an approach to object oriented programming that emphasises 5 principles for building more understandable, flexible and maintainable apps, and how we can apply these principles to developing for the web.