🪚

3.6. Бензопила Оккама или ”Выкинь все, абсолютно все”

Понятие “Пила Оккама” выходит из философии, поэтому и “Бензопила Оккама” – тоже философское понятие, коротко:
 
Если вы можете решить вопрос, не используя что-то, то не надо это использовать.
 
Каждый раз когда я что-то выкидыва и как-то себя “ограничиваю”, чаще всего, на самом деле, я создаю себе новые возможности, которые делают мой код более простым, понятным и явным.
 
Примеров тут может быть много, перечислю основные, которые я встречаю в процессе кодинга.
 
Дисклеймер
Некоторые пункты списка подходят только под TypeScript, поэтому их альтернативы мы рассмотрим в разделе “Языки” к каждому конкретному ЯП.
 
Для начала обсудим фичи языков:
 
  1. Классы – это сахар над описанием данных, объектами и функциями.
    1.  
      // Классовая нотация ... class User { constructor(public name: string) {} sayHello() { console.log(`Hello! My name is ${this.name}`) } } // ... заменяется на ... type User = ReturnType<typeof User> const User = (name: string) => { return { name: string, sayHello: () => { console.log(`Hello! My name is ${name}`) } } } // ... или вообще на type User = { name: string } const User = { sayHello: (user: User) => { console.log(`Hello! My name is ${user.name}`) } }
       
      О преимуществах данного подхода поговорим в главах про каждый отдельный язык, но кто-то уже мог догадаться.
       
  1. Декораторы – это сахар над композицией функций.
    1.  
      // # Например, данный декоратор будет логировать все аргументы и резальтат @LoggerDecorator() const someFunction = (name: string) => { return `Hello! My name is ${name}` } // # А вот как это сделать композицией 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 } } // # А теперь применяем функцию обертку const someFunction = log((name: string) => { return `Hello! My name is ${name}` })
       
  1. Enum – это сахар над Union Type.
    1.  
      // # Проблемы enum: // 1. Они создаются как в типах, так и в runtime // 2. Требуется указывать конкретное значение, которое легко перепутать // 3. Они не расширяются / мерджаться enum UserStatusE { active = "active", banned = "banned" } // # У Union Type таких проблем нет + они существуют только в типах type UserStatus = "active" | "banned" // # Хотим расширить типы, без проблем type AdminStatus = UserStatus | "retired"
       
  1. Наследование – это сахар над Композицией.
    1.  
      class User { email: string password: string banned: boolean changeEmail = (newEmail: string): void => { // ... } } // Пример наследования class Admin extends User { banUser = (user: User): void => { //... } } class Client extends User { bankAccountNumber: number changeBackAccountNumber = (newNumber: number): void => { // ... } }
       
      Наследование нужно для Полиморфизма и требует специальных механизмов (в случае с JS / TS это extends), но Полиморфизма можно добиться и используя Композицию, которая не требует никаких дополнительных механизмов языка:
       
      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 => { // ... } }
      Теперь функционал User доступен через вызов вложенного аргумента (admin.user.changeEmail(…)) или можно сделать методы-делегаты (а если вообще не использовать классовую нотацию, то становится еще проще, но тут я думаю, вы сами догадаетесь)
       
  1. Перегрузка функция – это усложненное описание отдельных функций.
    1.  
      function add(a:string, b:string):string; function add(a:number, b:number): number; function add(a: any, b:any): any { return a + b; }
       
      Да, это, конечно, может быть удобно в каких-то кейсах, но я ни разу не встречал ситуацию при которой перегрузку функций нельзя было заменить на несколько отдельных методов:
       
      function addStrings(a:string, b:string):string => { return a + b; } function addNumbers(a:number, b:number): number => { return a + b; }
       
      Это гораздо более очевидно и удобно, а главное, не требует дополнительных механик языка.
 
Тоже самое относится и к разным паттернам и технологиям:
 
  1. Clean / Hexagonal / Onion Architecture – это дополнительный слой абстракции над бизнес и системной логикой. Почему не стоит это использовать? Об этом будет еще отдельная статья, но как самый простой ответ – это та сущность, без которой мы можем спокойно обойтись. И как мы поговорим в следующих разделах, подобное абстрагирование слоев можно заменить на более понятные и явные паттерны.
    1.  
  1. IoC container – это сахар над аргументами функции. IoC container призван “помогать” в инициализации и пробрасывание зависимостей, но очень часто он привносит только хаос тем, что забирает у нас контроль над работой с зависимостями. В 95% случаев вы обнаружите, что проинициализировать зависимости и прокинуть их, как аргументы функции – гораздо проще, а главное, понятнее.
    1.  
  1. ORM – это сахар над драйвером. Редкая ORM дает функционал, который делает ее использование более правильным решением, чем использование чистого драйвера. Примером хороших фич ORM можно назвать Unit of Work (как в MikroORM), Интроспекция (как в Prisma), Автогенерация методов по шаблонам (как в sqlboiler), Конвертация процедур в SQL (как в LINQ), etc. Это конкретные фичи, которые “добавляют” пользы, нежели просто “абстрагируют” то, что вы итак могли бы использовать через драйвер.
 
 

👈 Предыдущая глава
Следующая глава 👉

 
💡
Подписывайтесь на телеграм канал 🦾 IT-Качалка Давида Шекунца 💪, где будут выходить анонсы новых глав и обновления существующих. Спасибо за внимание и мощной вам прокачки 💪