Classes Start With Functions, Not Data

A common mistake developers make when designing classes is to start with a data model in mind and then try to attach functions to that data (e.g., a Zoo has a Keeper, who has a first name and a last name, etc). This data-centred view of classes tends to lead us towards anaemic models, where classes are nothing more than data containers and the logic that uses the data is distributed throughout the system. This lack of encapsulation creates huge amounts of low-level coupling.

Try instead to start with the function you need, and see what data it requires. This can be illustrated with a bit of TDD. In this example, we want to buy a CD. I start by writing the buy function, without any class to hang that on.


class BuyCdTest {
@Test
void buyCdPaymentAccepted() {
int stock = 10;
double price = 9.99;
String creditCardNumber = "1234";
Payments payments = new PaymentsStub(PaymentResponse.ACCEPTED);
stock = buy(stock, price, creditCardNumber, payments);
assertEquals(9, stock );
}
private int buy(int stock, double price, String creditCardNumber, Payments payments) {
if(payments.process(price, creditCardNumber) == PaymentResponse.ACCEPTED)
stock–;
return stock;
}
}

view raw

BuyCdTest.java

hosted with ❤ by GitHub

The parameters for buy() tell us what data this function needs. If we want to encapsulate some of that data, so that clients don’t need to know about all of them, we can introduce a parameter object to group related params.


@Test
void buyCdPaymentAccepted() {
int stock = 10;
double price = 9.99;
String creditCardNumber = "1234";
Payments payments = new PaymentsStub(PaymentResponse.ACCEPTED);
stock = buy(new CompactDisc(stock, price, payments), creditCardNumber);
assertEquals(9, stock );
}
private int buy(CompactDisc cd, String creditCardNumber) {
int stock = cd.getStock();
if(cd.getPayments().process(cd.getPrice(), creditCardNumber) == PaymentResponse.ACCEPTED)
stock–;
return stock;
}

view raw

BuyCdTest.java

hosted with ❤ by GitHub

This has greatly simplified the signature of the buy() function, and we can easily move buy() to the cd parameter.


@Test
void buyCdPaymentAccepted() {
int stock = 10;
double price = 9.99;
String creditCardNumber = "1234";
Payments payments = new PaymentsStub(PaymentResponse.ACCEPTED);
CompactDisc cd = new CompactDisc(stock, price, payments);
stock = cd.buy(creditCardNumber);
assertEquals(9, stock );
}

view raw

BuyCdTest.java

hosted with ❤ by GitHub

Inside the new CompactDisc class…


public class CompactDisc {
private int stock;
private double price;
private Payments payments;
public CompactDisc(int stock, double price, Payments payments) {
this.stock = stock;
this.price = price;
this.payments = payments;
}
public int getStock() {
return stock;
}
public double getPrice() {
return price;
}
public Payments getPayments() {
return payments;
}
int buy(String creditCardNumber) {
int stock = getStock();
if(getPayments().process(getPrice(), creditCardNumber) == PaymentResponse.ACCEPTED)
stock–;
return stock;
}
}

We have a bunch of getters we don’t need any more. Let’s inline them.


public class CompactDisc {
private int stock;
private final double price;
private final Payments payments;
public CompactDisc(int stock, double price, Payments payments) {
this.stock = stock;
this.price = price;
this.payments = payments;
}
int buy(String creditCardNumber) {
if(payments.process(price, creditCardNumber) == PaymentResponse.ACCEPTED)
stock–;
return stock;
}
}

Now, you may argue that you would have come up with this data model for a CD anyway. Maybe. But the point is that the data model is specifically there to support buying a CD.

When we start with the data, there’s a greater risk of ending up with the wrong data (e.g., many devs who try this exercise start by asking “What can we know about a CD?” and give it fields the functions don’t use), or with the right data in the wrong place – which is where we end up with Feature Envy and message chains and other coupling code smells galore.

S.O.L.I.D. JavaScript – OO Version

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:


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);
}
}

view raw

basket.js

hosted with ❤ by GitHub

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.)


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);
});

view raw

basket.js

hosted with ❤ by GitHub

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:


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);
}
}

view raw

basket.js

hosted with ❤ by GitHub

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!