The concept of "Occam's Razor" comes from philosophy, so "Occam's Chainsaw" is also a philosophical concept, briefly:
If you can solve a problem without using something, then don't use it.
Every time I throw something away and somehow "limit" myself, most often, in fact, I create new possibilities that make my code simpler, clearer, and more explicit.
There can be many examples here, I'll list the main ones that I encounter during coding.
Disclaimer
Some items on the list are only applicable to TypeScript, so we'll look at their alternatives in the "Languages" section for each specific programming language.
Let's start by discussing language features:
- Classes – are syntactic sugar for describing data, objects, and functions.
// Class notation ... class User { constructor(public name: string) {} sayHello() { console.log(`Hello! My name is ${this.name}`) } } // ... can be replaced with ... type User = ReturnType<typeof User> const User = (name: string) => { return { name: string, sayHello: () => { console.log(`Hello! My name is ${name}`) } } } // ... or even with type User = { name: string } const User = { sayHello: (user: User) => { console.log(`Hello! My name is ${user.name}`) } }
We'll talk about the advantages of this approach in the chapters about each individual language, but some may have already guessed.
- Decorators – are syntactic sugar for function composition.
// # For example, this decorator will log all arguments and results @LoggerDecorator() const someFunction = (name: string) => { return `Hello! My name is ${name}` } // # And here's how to do it with composition const log = <Args extends any[], Result>(fn: (...args: Args) => Result) => { return (...args: Args) => { console.log(...args) const res = fn(...args) console.log(res) return res } } // # Now we apply the wrapper function const someFunction = log((name: string) => { return `Hello! My name is ${name}` })
- Enum – is syntactic sugar for Union Type.
// # Problems with enum: // 1. They exist both in types and runtime // 2. You need to specify a specific value which is easy to confuse // 3. They cannot be extended/merged enum UserStatusE { active = "active", banned = "banned" } // # Union Types don't have these problems + they only exist in types type UserStatus = "active" | "banned" // # Want to extend types? No problem type AdminStatus = UserStatus | "retired"
- Inheritance – is syntactic sugar for Composition.
class User { email: string password: string banned: boolean changeEmail = (newEmail: string): void => { // ... } } // Example of inheritance class Admin extends User { banUser = (user: User): void => { //... } } class Client extends User { bankAccountNumber: number changeBackAccountNumber = (newNumber: number): void => { // ... } }
Inheritance is needed for Polymorphism and requires special mechanisms (in the case of JS / TS it's
extends
), but Polymorphism can also be achieved using Composition, which doesn't require any additional language mechanisms:
class User { email: string password: string banned: boolean changeEmail = (newEmail: string): void => { // ... } } class Admin { user: User banUser = (user: User): void => { //... } } class Client { user: User bankAccountNumber: number changeBackAccountNumber = (newNumber: number): void => { // ... } }
Now the User functionality is available through calling the nested argument (
admin.user.changeEmail(…)
) or you can make delegate methods (and if you don't use class notation at all, it becomes even easier, but I think you can figure it out yourself)- Function overloading – is a complicated description of separate functions.
function add(a:string, b:string):string; function add(a:number, b:number): number; function add(a: any, b:any): any { return a + b; }
Yes, this might be convenient in some cases, but I've never encountered a situation where function overloading couldn't be replaced with several separate methods:
function addStrings(a:string, b:string):string => { return a + b; } function addNumbers(a:number, b:number): number => { return a + b; }
This is much more obvious and convenient, and most importantly, doesn't require additional language mechanics.
The same applies to various patterns and technologies:
- Clean / Hexagonal / Onion Architecture – is an additional abstraction layer over business and system logic.
Why shouldn't this be used? There will be a separate article about this, but the simplest answer is that it's an entity we can easily do without.
And as we'll discuss in the following sections, such abstraction of layers can be replaced with more understandable and explicit patterns.
- IoC container – is syntactic sugar over function arguments.
The IoC container is designed to "help" with initialization and passing dependencies, but very often it only brings chaos by taking away our control over working with dependencies.
In 95% of cases, you'll find that initializing dependencies and passing them as function arguments is much simpler, and most importantly, clearer.
- ORM – is syntactic sugar over a driver.
Few ORMs provide functionality that makes using them a better solution than using a pure driver.
Examples of good ORM features include Unit of Work (as in MikroORM), Introspection (as in Prisma), Automatic method generation from templates (as in sqlboiler), Procedure conversion to SQL (as in LINQ), etc. These are specific features that "add" value rather than just "abstracting" what you could already use through the driver.
👈 Previous chapter
Next chapter 👉