Photo by Brina Blum on Unsplash
You might not need Javascript classes
I'm not telling you not to use classes. Some of my best friends use classes. I'm saying that you might not need to.
With so many programmers coming from a background of writing code in languages like Java or C# it's not hard to understand why a lot of Javascript code bases use classes. People are simply used to them.
Consider the following class.
class OrderService {
constructor(
productDao,
orderDao,
paymentService,
emailService
) {
this.productDao = productDao
this.orderDao = orderDao
this.paymentService = paymentService
this.emailService = emailService
}
async placeOrder(order, customerInfo) {
const products =
await this.productDao.getByIds(
order.productIds
)
const totalPrice = products.reduce(
(acc, product) => acc + product.price,
0
)
const receipt =
await this.paymentService.chargeCreditCard(
totalPrice,
customerInfo.creditCard
)
const savedOrder = await this.orderDao.save(
order,
receipt
)
await this.emailService.sendEmail(
customerInfo.email,
`Your order ${savedOrder.id} has been placed`
)
return savedOrder
}
// More methods for handling orders in different
// ways, like deleting, updating etc...
}
This is totally fine and very common to see code like this. However, by using a concept called higher-order functions, you can achieve basically the same thing but without classes. Let's see what that might look like in its simplest form.
function createOrderService(
productDao,
orderDao,
paymentService,
emailService
) {
async function placeOrder(order, customerInfo) {
// Same logic as before, but without the `this` keyword
}
return { placeOrder }
}
To some, this might be less readable. But remember. New concepts also take some getting used to. I used to write Java for a living, but today I find Object Oriented Programming less readable. Also, if you, like me, have been burnt by the this keyword. In this paradigm, you don't use it anymore.
Let's take this one step further, to make the placeOrder function more testable.
const sumTotalPrice = (products) =>
products.reduce(
(acc, curr) => acc + curr.price,
0
)
const createPlaceOrder =
(deps) => async (order, customerInfo) => {
const {
getProductsById,
chargeCreditCard,
saveOrder,
sendEmail,
} = deps
const products = await getProductsById(
order.productIds
)
const receipt = await chargeCreditCard(
sumTotalPrice(products),
customerInfo.creditCard
)
const savedOrder = await saveOrder(
order,
receipt
)
await sendEmail(
customerInfo.email,
`Your order ${order.id} has been placed`
)
return savedOrder
}
We did a couple of things here:
We extracted the price calculation into its own function. The function is pure, making it very easy to test.
We separated dependencies from the business logic using a higher-order function. This makes it easy to see where they're coming from and it's also very easy to mock & test.
We explicitly pass exactly what dependencies the function needs. We could have passed in productDao, paymentService etc, but here we only pass in exactly which functions we will use.
The code examples in this article are very brief to give an idea of what it might look like. If you're interested in a bit more elaborate example I added a gist for you to look at.
In summary, while JavaScript classes have their place, exploring the use of higher-order functions and pure functions opens up a world of simplicity, testability, and clarity in your code. As with any programming paradigm, the key is to find the right balance that works for your specific context. Whether you choose classes, functional programming, or a mix of both, always aim for code that is readable, testable, and adaptable to change.