A little while back I wrote a post on the old blog about how we could apply the same design principles – near enough – to functional programming as we might to object oriented programming, using JavaScript examples.
That encouraged a couple of people to get in touch saying “But we don’t do FP in JavaScript!”, and suggesting therefore that – strangely – these principles don’t apply to them. The mind boggles.
But, for completeness, here’s how I might apply S.O.L.I.D. principles to OO JavaScript code. To make things backwards compatible, I’ve not used the class syntax of later versions of JS.
First of all, the big tomale: swappable dependencies (Dependency Inversion).
Consider this snippet of code for a simplistic shopping basket:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function Basket(customer, items){ | |
| this.add = function(item) { | |
| items.push(item); | |
| } | |
| this.checkout = function(){ | |
| const payments = new PayPalPayments(); | |
| return payments.pay(this.total(), customer.creditCard) | |
| } | |
| this.total = function() { | |
| return items.reduce((sum, item) => sum + (item.price * item.quantity), 0); | |
| } | |
| } |
The problem here is what happens if we want to change the way we process payments? Maybe we don’t want to use PayPal any more, for example. Or what if we don’t want to use a real payment processor in a unit test? In this design, we’d have to change the Basket class. That breaks the Open-Closed Principle of SOLID (classes should be open to extension, but closed for modification.)
If we inject the payment processor, then it becomes easy to swap the implementation for whatever purpose (in this example, to stub the processor for a test.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function Basket(customer, items){ | |
| this.add = function(item) { | |
| items.push(item); | |
| } | |
| this.checkout = function (payments) { | |
| return payments.pay(this.total(), customer.creditCard) | |
| } | |
| this.total = function() { | |
| return items.reduce((sum, item) => sum + (item.price * item.quantity), 0); | |
| } | |
| } | |
| test('should process payment on checkout', () => { | |
| let basket = new Basket({creditCard: '1234'}, []); | |
| basket.add({price: 10.0, quantity: 10}); | |
| expect(basket.checkout(new PaymentsStub(true))).toBe(true); | |
| }); |
And there we have it: three fifths of SOLID is about making dependencies swappable – Open-Closed, Liskov Substitution and Dependency Inversion. (or “OLD”, if you like.)
And can we agree classes should have a Single Responsibility? That’s not really an OO principle. The same’s true of functions and modules and microservices and any other discrete unit of executable code.
Finally, the Interface Segregation Principle: classes should present client-specific interfaces. That is, interfaces should only include the methods a client uses. With duck typing, it doesn’t really matter of a class presents methods a client doesn’t use. This is true whether we’re talking about methods of classes, or functions in modules.
It might help to make the code easier to understand of we document protocols by explicitly defining pure abstract classes that describe what methods any implementation would need to support. But it’s not necessary for our code to compile and run.
But, as with the functional examples I used, there is a case for saying that modules shouldn’t reference implementations they’re not using. Let’s suppose that after I refactored my Basket to use dependency injection, I forgot to remove the import for PayPalPayments:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const paypalPayments = require('./paypalpayments.js'); | |
| function Basket(customer, items){ | |
| this.add = function(item) { | |
| items.push(item); | |
| } | |
| this.checkout = function (payments) { | |
| return payments.pay(this.total(), customer.creditCard) | |
| } | |
| this.total = function() { | |
| return items.reduce((sum, item) => sum + (item.price * item.quantity), 0); | |
| } | |
| } |
It’s important to remember to clean up your imports regularly to avoid situations where changes to things we don’t use could break our code.
So, the sum up: the same principles apply in JavaScript regardless of whether you’re doing FP or OOP.
No excuses!