Tips to write good code
That you can start applying today
Focus on content
- Code like you're writing a story for someone else that doesn't know what you're doing
- Avoid generic variables (
a
, b
, c
), unless the content is obvious
- Avoid
step1
, step2
, ... function names. Make explicit what you're doing
- A developer should know what your functions do just reading the signature
Focus on content
This is ๐ฉ
// I know it's the first step, and I know it's first result!
// I don't understand what first step is!
const result1 = doStep1();
// ๐ฉ same as before...
const result2 = doStep2(result1);
// Come on... Get value of what? ...a? ๐ญ
// I have to read the function bodies to understand...
// Time wasted! ๐คฌ
const a = getValue(result2);
Focus on content
This is ๐คฉ
// Ok, now I know that you're extracting data from a UserForm
const formData = getUserFormData();
// And you're validating it
const validationErrors = validate(formData);
// And checking if we can recover errors
// without user intervention.
// I don't have to read anything else! ๐พ
const shouldAskToUser = shouldPromptToUser(validationErrors);
- They improve code readability
- Assert input as soon as possible, fail fast, then you can focus only on business logic
- They avoid unnecessary code branching
Don't do this
function saveCar(Car car, Price price, InvoiceRepo invoiceRepo): Invoice {
if (car.isValid) {
if (price.isBelowThreshold) {
const invoice = prepareInvoice(car, price);
if (invoiceRepo.isAvailable) {
return invoiceRepo.save(invoice);
}
// Else everywhere!!!
else {
throw new InvoiceUnavailableError();
}
// Nesting everywhere ๐คฏ
}
else {
throw new PriceIsTooLowError(price);
}
// My eyes are burning! ๐ญ
} else {
throw new InvalidCarError(car);
}
}
Do this
function saveCar(Car car, Price price, InvoiceRepo invoiceRepo): Invoice {
// Guard clauses in action!
if (!car.isValid) {
throw new InvalidCarError(car);
}
if (!price.isBelowThreshold) {
throw new PriceIsTooLowError(price);
}
if (!invoiceRepo.isAvailable) {
throw new InvoiceUnavailableError();
}
// And then focus on business logic.
// No nesting, no else, no messy things! ๐พ
return invoiceRepo.save(prepareInvoice(car, price));
}
Exploit language features
- Every language has unique features to improve code quality. Spend time to learn them.
- You'll write less code => less bugs, less code to maintain
- I'll show you some examples from different languages
Exploit language features
Scala: this is ๐คจ...
val carWithGasolineNames = users.flatMap { user =>
user.cars.withFilter(car => car.hasGasoline)
.map(car => car.name)
}
Exploit language features
Scala: this is ๐คฉ
// Same as before, but using for comprehension!
val carWithGasolineNames = {
user <- users
car <- user.cars if car.hasGasoline
} yield car.name
Exploit language features
TypeScript / JS: this is ๐คจ...
const newBook = Object.assign({}, defaultBook);
newBook.title = patchBook.title;
newBook.description = patchBook.description;
newBook.author = patchBook.author;
newBook.releasedAt = patchBook.releasedAt;
return newBook;
Exploit language features
TypeScript / JS: this is ๐คฉ
// Same as before, but using destructuring...
const { title, description, author, releasedAt } = patchBook;
// and spread operator!
return {
...defaultBook,
title,
description,
author,
releasedAt,
};
Exploit language features
Dart: this is ๐คจ...
List additionalCars = myFriendCars;
if (myFriendCars == null) {
additionalCars = otherFriendCars;
}
List myFavoriteCars = [];
myFavoriteCars.add(ferrari488);
myFavoriteCars.add(lamborhiniHuracan);
myFavoriteCars.add(lanciaDeltaHFIntegrale);
myFavoriteCars.addAll(additionalCars);
return myFavoriteCars;
Exploit language features
Dart: this is ๐คฉ
// Same as before, but using cascading operator (..)
return []
..add(ferrari488)
..add(lamborhiniHuracan)
..add(lanciaDeltaHFIntegrale)
// and if null operator!
..addAll(myFriendCars ?? otherFriendCars)
Ask dependencies from constructor
- You make explicit class dependencies from beginning
- You can mock dependencies very easily in unit tests!
Ask dependencies from constructor
This is ๐ฉ...
assert('BookService should save book', () => {
const book = new Book("id", "title", "description");
new BookService().save(book);
// I can't mock BookRepo dependency...
// It's created inside BookService!
// I have to use real BookRepo and the real DB ๐ญ
expect(new BookRepo().get(book.id)).toEqual(book);
});
Ask dependencies from constructor
This is ๐คฉ
assert('BookService should save book', () => {
const book = new Book("id", "title", "description");
const mockedBookRepo = mock(BookRepo);
// I can pass a mocked BookRepo, test are easier now!
new BookService(mockedBookRepo).save(book);
expect(mockedBookRepo.save).calledOnceWith(book);
// No DB involved, all is mocked ๐พ
});
Avoid negative variables names
- Do not call variables like
notFinished, notAllowed
or even worse notInvalid
- Use this:
draft, forbidden, valid
- Negative variable names create unnecessary confusion, especially when we negate them in code.
Avoid passing too many parameters
- Use data classes instead
- Data classes are easier to pass to another function
- If parameters are strictly correlated, it's better to group them inside class
Avoid passing too many parameters
This is ๐ฉ...
// To much parameters!
function filterBooks(name, description, createFrom,
createTo, authorEmail) {
// I have to pass to another function
// I have to rewrite them all! ๐ญ
return bookRepo.filter(
name, description, createFrom,
createTo, authorEmail
);
}
Avoid passing too many parameters
This is ๐คฉ
// All parameters are inside bookFilters data class!
function filterBooks(bookFilters) {
// Passing them is much easier now ๐พ
return bookRepo.filter(bookFilters);
}