Writing clean code means writing clean functions or methods. And to do so, I have chosen the ONE rule. It's not one rule, it's the Air Force One of function rules. A rule to rule them all.
The code you write is meant to be read several times, so it is important to pay attention to how you write it. Especially where you put the logic, your functions, and methods. Logic must be easy to grab and change. Maintainable functions are the foundation of maintainable code.
In this article, I will show you some guides to writing simple, clean methods and functions in TypeScript. All of the tips are made to adhere to this classical principle:
Functions should do one thing. They should do it well. They should do it only.
When you're done, you'll be able to spot bad smells on your code and have criteria to clean them up. And to remember these guidelines you only have to remember the ONE.
1️⃣ Input argument
A function must do one thing. Having a lot of arguments may hide having several responsibilities. And by hiding, I mean that it is not clearly defined in its name.
// ❌
function saveTripBooking(
clientBooking: Booking,
paymentData: Payment,
confirmationMessage: Message
) {
// make payment
// save booking
// send a confirmation email
}
// ✅
function makePayment(clientBooking: Booking) {}
function saveBooking(paymentData: Payment) {}
function sendConfirmationEmail(confirmationMessage: Message) {}
Sometimes all the arguments are related to one mission. In such cases, those variables should be encapsulated in a structure, avoiding the primitive obsession code smell.
// ❌
function sendMessage(
senderName: string,
senderAddress: string,
recipientAddress: string,
subject: string,
body: string
) {}
// ✅
type Message = {
senderName: string;
senderAddress: string;
recipientAddress: string;
subject: string;
body: string;
};
function sendMessage(message: Message) {}
1️⃣ Structural block level
Conditional and repetitive structures are our bread and butter. So keeping them clean is a worthwhile effort. Problems arise when you start nesting one structure in one another like Russian dolls.
You avoid this pyramid of hell with two simple hygienic habits.
- Extract the inner block content to a new function.
- Early return from invalid or trivial cases.
// ❌
function sendTripDetails() {
const passengers: Passenger[] = getPassengers();
if (passengers.length >= 0) {
for (i = 0; i <= passengers.length; i++) {
if (passenger[1].acceptedCommunications) {
if (passengers[i].emailAddress) {
sendTripDetailsByEmail(passengers[i].emailAddress);
}
if (passengers[i].phoneNumber) {
sendTripDetailsBySMS(passengers[i].phoneNumber);
}
}
}
}
}
// ✅
function sendTripDetails() {
const passengers: Passenger[] = getPassengers();
if (passengers.length === 0) return;
for (i = 0; i <= passengers.length; i++) {
sendTripDetailsToPassenger(passenger);
}
}
function sendTripDetailsToPassenger(passenger: Passenger) {
if (passenger.acceptedCommunications == false) return;
if (passenger.emailAddress) {
sendTripDetailsByEmail(passengers[i].emailAddress);
}
if (passenger.phoneNumber) {
sendTripDetailsBySMS(passengers[i].phoneNumber);
}
}
1️⃣ Level of abstraction
Here we come face-to-face with abstraction. You can imagine your code as a big classic hierarchical corporation. Orders go from top to bottom, but each level has its responsibilities (and usually its vocabulary). You know, white collars don`t get their hands dirty.
// ❌
function getAvailablePlaces(tripId: string) {
const queryTrips = "select capacity from trips where tripId=" + tripId;
const capacity = db.select(queryTrips);
const queryBookings =
"select sum(seats) from bookings where tripId=" + tripId;
const tripBookedSeats = db.select(queryBookings);
const free = capacity - tripBookedSeats;
const OVERBOOKING_FACTOR = 1.05;
return free * OVERBOOKING_FACTOR;
}
// ✅
// presentation level
function getAvailablePlaces(tripId: string) {
const freeSeats = getFreeSeats(tripId);
return calculateAvailable(freeSeats);
}
// logical level
function getFreeSeats() {
const capacity = selectTripCapacity(tripId);
const tripBookedSeats = selectTripBookedSeats();
return capacity - tripBookedSeats;
}
function calculateAvailable(freeSeats: number) {
const OVERBOOKING_FACTOR = 1.05;
return freeSeats * OVERBOOKING_FACTOR;
}
// data level
function selectTripCapacity(tripId: string) {
const queryCapacity = "select capacity from trips where tripId=" + tripId;
return db.select(queryCapacity);
}
function selectTripBookedSeats(tripId: string) {
const queryBookings =
"select sum(seats) from bookings where tripId=" + tripId;
return db.select(queryBookings);
}
1️⃣ Responsibility; query or command.
Changing the state and consulting the state of any system are two different things. So, a function must choose its role.
// ❌
function saveBooking(booking: Booking): number {
db.insertBooking(booking); // ❗ mutation
return db.selectAvailableSeats(booking.tripId); // ❓ question
}
// ✅
function saveBooking(booking: Booking): void {
db.insertBooking(booking); // ❗ do things
}
function getAvailablePlaces(tripId: string): number {
return db.selectAvailableSeats(tripId); // ❓ ask things
}
This is even worse when it results in mutating the state of any argument.
// ❌
function discountBooking(booking: Booking): number {
const discount = booking.price * 0.1;
booking.price = booking.price - discount; // ❗ mutation
return discount; // ❓ question like its not mutating
}
// ✅
function discountBooking(booking: Booking): void {
const discount = calculateDiscount(booking);
applyDiscount(booking, discount);
}
function calculateDiscount(booking: Booking): number {
const discount = booking.price * 0.1;
return discount; ❓
}
function applyDiscount(booking: Booking, discount: number): void {
booking.price = booking.price - discount; ❗
}
1️⃣ Digit length and ...
Well if you get here, you will see that following these guides, you will end with lots more functions than you begin with. That should mean that each function has to be smaller. And this is a good outcome. Having small functions or methods makes them easy to understand, change and test. And yes, more likely to be reused.
As a rule of thumb, you must pay attention to functions with more than 10 instructions. Has this function only one clear responsibility? Is she doing too much? Can you extract some instructions into a helper low-level function?
You sure do. Keep your functions with 9 or fewer statements; i.e length of 1 digit.
// ❌
function calculateDiscount(booking: Booking) {
const discount = 0;
if (booking.passengers.length > 2) {
discount += 5;
}
const payment = getPaymentByBookingId(booking.id);
if (payment.method === PaymentMethods.Cash) {
discount += 5;
}
const client = getClientById(booking.clientId);
if (client.isVip) {
discount += 10;
}
const trip = getTripById(booking.tripId);
const season = getTripSeason(trip);
if (season === Seasons.Winter) {
discount += 10;
}
const totalDiscount = (discount * booking.price) / 100;
return totalDiscount;
}
// ✅
function calculateDiscount(tripId: string) {
const discountPercent = 0;
const booking = getBookingById(bookingId);
discountPercent += calculatePassengersDiscount(booking);
discountPercent += calculatePaymentDiscount(booking);
discountPercent += calculateClientDiscount(booking);
discountPercent += calculateSeasonDiscount(booking);
const discount = (discountPercent * booking.price) / 100;
return discount;
}
// logical level
function calculatePassengersDiscount(booking: Booking) {
if (booking.passengers.length > 2) {
return 5;
}
return 0;
}
function calculatePaymentDiscount(booking: Booking) {
const payment = getPaymentByBookingId(booking.id);
if (payment.method === PaymentMethods.Cash) {
return 5;
}
return 0;
}
function calculateClientDiscount(booking: Booking) {
const client = getClientById(booking.clientId);
if (client.isVip) {
return 5;
}
return 0;
}
function calculateSeasonDiscount(booking: Booking) {
const trip = getTripById(booking.tripId);
const season = getTripSeason(trip);
if (season === Seasons.Winter) {
return 5;
}
return 0;
}
1️⃣ ... good name
Naming is the concrete of clean code buildings. Functions and methods should have a name that describes what they do. Doing means action, so they should contain a verb, be a phrase name, or use a boolean verb for boolean functions.
// ❌
function get() {
// what is getting?
return db.select("select * from trips");
}
function trip(id: string) {
// what is doing?
return db.select("select * from trips where id=" + id);
}
function cancelled(booking: Booking) {
// could be clearer?
return booking.status === BookingStatus.Cancelled;
}
// ✅
function selectTrips() {
return db.select("select * from trips");
}
function selectTripById(id: string) {
return db.select("select * from trips where id=" + id);
}
function isCancelled(booking: Booking) {
return booking.status === BookingStatus.Cancelled;
}
🌅 Conclusion
Clean code is essential to have programs maintained by different programmers that last for years. Functions and methods are the logical building blocks of any software. Keep them small, clear, and well-named. It is easy if you remember the rule of ONE.
In this article, you learned how you can write clean functions. Keep improving to write clean code.
- 1️⃣ Entry argument
- 1️⃣ Level of nesting
- 1️⃣ Level of abstraction
- 1️⃣ Responsibility
- 1️⃣ Digit length
- 1️⃣ Verb in the name
learn, code, enjoy, repeat
Alberto Basalo