Освоєння мистецтва чистого коду: розкриття потужності принципів програмування

💡 Усі статті, обговорення, новини для початківців — в одному місці. Приєднуйтесь до Junior спільноти!

Кожна мова програмування має свій власний набір найкращих практик. Вони допомагають розробникам писати масштабований, легкочитний і організований код. Деякі з цих практик є універсальними й можуть бути застосовані в будь-якій мові програмування. Вони відомі як «Принципи програмування».

Принципи програмування — це набір правил і найкращих практик, які можна використовувати у будь-якій сфері програмування та з будь-якою мовою. Вони є фундаментальними знаннями для розробки програмного забезпечення. Незалежно від того, чи ви тільки почали програмувати, чи вже досвідчений розробник, розуміння та застосування цих принципів значно покращить вашу роботу.

У цій статті ми розглянемо три принципи програмування, які можуть і повинні бути використані в щоденній реалізації програм та які значно покращать якість вашого коду. Ці принципи, відомі як KISS (Keep It Simple, Stupid), DRY (Don’t Repeat Yourself) та SRP (Single Responsibility Principle), дають чудову настанову щодо того, як писати підтримуваний, адаптивний та надійний код, та можуть бути використані в усіх галузях розробки.

Ми спробуємо відрефакторити цей фрагмент коду, який може бути використаний для виконання HTTP-запитів:

// src/packages/users/libs/types/user-dto.type.ts
type UserDto = {
  id: number;
  name: string;
};
// src/packages/posts/libs/types/post-dto.type.ts
type PostDto = {
  title: string;
  content: string;
};
// src/libs/packages/http/http.package.ts
class Http {
  baseUrl: string;
  headers: Headers;
  public constructor() {
    this.baseUrl = '';
    this.headers = new Headers();
  }
  public setBaseUrl(url: string): void {
    this.baseUrl = url;
  }
  public setHeaders(headers: Headers): void {
    this.headers = headers;
  }
  public async get<T = unknown>(url: string): Promise<T> {
    const response = await fetch(this.baseUrl + url, {
      method: 'GET',
      headers: this.headers,
    });
    const data = await response.json();
    console.log(`GET ${this.baseUrl + url} - Status: ${response.status}`);
    console.log('Response Data:', data);
    return data as T;
  }
  public async post<T = unknown>(url: string, data: T): Promise<T> {
    const response = await fetch(this.baseUrl + url, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify(data),
    });
    const responseData = await response.json();
    console.log(`POST ${this.baseUrl + url} - Status: ${response.status}`);
    console.log('Response Data:', responseData);
    return responseData as T;
  }
}
// src/libs/packages/http/http.ts
const http = new Http();
http.setBaseUrl('https://api.example.com');
const headers = new Headers();
headers.append('Content-Type', 'application/json');
http.setHeaders(headers);
// src/slices/users/actions.ts
const getUserById = (id: number): Promise<UserDto> => {
  return http.get<UserDto>(`/user/${id}`);
};
// src/slices/posts/actions.ts
const createPost = (title: string, content: string): Promise<PostDto> => {
  return http.post<PostDto>('/posts', { title, content });
};

KISS — Keep It Simple, Stupid!

Часто розробники надто ускладнюють код, який вони пишуть. Якщо код написаний складно, це робить його подальше відлагодження важким та подовжує час розробки в цілому. Принцип KISS закликає розробників писати код якнайпростіше. У більшості випадків це може бути важко, або навіть неможливо, але ви завжди повинні намагатися зробити свій код простим. Простота коду гарантує більш швидку розробку, легше відлагодження та збільшує читабельність коду в кілька разів.

Що таке простий код

Простий код — це не лише логіка коду, а також чітке іменування змінних та дотримання критеріїв якості. Ці кроки забезпечують створення читабельного та підтримуваного коду. Простий код не завжди означає короткий код. Особливо це слід враховувати під час написання однорядкових виразів. Виконане завдання лише в одному рядку коду може виглядати стильно. Але коли йдеться про читабельність, важливо, щоб він був простим. Переваги дотримання принципу KISS:

  1. Простота та читабельність: код, який є простим і легким для розуміння, зменшує ризик появи помилок і полегшує відлагодження.
  2. Легша підтримка: принцип KISS зменшує складність кодової бази. Коли код простий, його підтримка, оновлення та рефакторинг стають набагато простішими.
  3. Швидша розробка: простий код розробляється, тестується та оновлюється швидше, оскільки він мінімізує непотрібну складність.

У нашому фрагменті коду нам не потрібно мати методи setBaseUrl та setHeaders. За допомогою цих методів нам може знадобитися встановлювати різний baseUrl та headers для кожного запиту. Замість цього ми можемо відправляти baseUrl під час ініціалізації екземпляр сервісу та обчислювати headers всередині нього.

Ось як виглядає код з початку цієї статті, якщо застосувати принцип KISS:

// src/libs/types/content-type.type.ts
type ContentType = 'application/json' | 'multipart/form-data';
// src/libs/packages/http/http.package.ts
type Constructor = {
  baseUrl: string;
};
class Http {
    private baseUrl: string;
    public constructor({ baseUrl }: Constructor) {
        this.baseUrl = baseUrl;
    }
    // Other methods
    private getHeaders(contentType?: ContentType): Headers {
    const headers = new Headers();
    if (contentType) {
      headers.append('Content-Type', contentType);
    }
    return headers;
  }
    // Other methods
}
// src/libs/packages/http/http.ts
const http = new Http({ baseUrl: 'https://api.example.com' });

Виправлена версія набагато легше підтримується. Ми можемо легко використовувати цей клас для різних кінцевих точок, створюючи нові екземпляри з різним baseUrl, і ми легко можемо додавати нові заголовки, наприклад, якщо наш Backend вимагає аутентифікації. Нові заголовки додаються в одному місці, і нам не потрібно окремо додавати їх в кожному місці, де ми використовуємо цей клас.

Як застосовувати принцип KISS

  1. Розбивайте великі шматки коду на кілька менших.
  2. Використовуйте читабельні та змістовні імена змінних.
  3. Уникайте непотрібних ускладнень.

DRY — Don’t Repeat Yourself

Як і сказано в назві цього принципу, він закликає розробників не повторювати один і той самий код двічі. Згідно із цим принципом, певна логіка повинна бути написана лише один раз в кодовій базі. Найпростіший спосіб порушити принцип DRY — це копіювати та вставляти власний код.

Переваги написання коду, який дотримується принципу DRY:

  1. Можливість повторного використання коду: уникнення дубльованого коду заощаджує час розробки, забезпечує послідовність у поведінці й зменшує ризик появи нових помилок.
  2. Легше відлагодження та підтримка в цілому: пошук помилки або додавання нової функціональності до коду, який не дотримується принципу DRY, зазвичай передбачає внесення тих самих змін у багато різних файлів.
  3. Послідовність коду: DRY-код забезпечує послідовність логіки у всіх файлах та різних частинах програми. Це зменшує ризик помилок, спричинених неузгодженістю між дубльованим кодом.

У нашому фрагменті коду ми повторюємо логіку для запитів GET та POST. Ми можемо об’єднати методи для запитів GET та POST і створити один з назвою load, який буде обробляти всі методи запитів.

Ось як виглядає код з початку цієї статті, якщо застосувати принцип DRY:

// src/libs/packages/http/libs/types/http-request-parameters.ts
type HttpRequestParameters = {
  method: HttpMethod;
  url: string;
  payload?: BodyInit | null;
  contentType?: ContentType;
};
// src/libs/packages/http/http.package.ts
class Http {
    // Other methods
    public async load<T = unknown>({
    method,
    url,
    payload,
    contentType,
  }: HttpRequestParameters): Promise<T> {
    const headers = this.getHeaders(contentType);
    const response = await fetch(`${this.baseUrl}${url}`, {
      method,
      headers,
      body: payload ? JSON.stringify(payload) : null,
    });
    console.log(`${method} ${url} - Status: ${response.status}`);
    console.log('Response:', response);
    return response.json();
  }
    // Other methods
}
// src/libs/packages/http/http.ts
const http = new Http({ baseUrl: '
const http = new Http({ baseUrl: 'https://api.example.com' });

Як писати код, який дотримується принципу DRY

  1. Завжди пишіть код, який може бути використаний повторно, де це можливо.
  2. Ніколи не копіюйте той самий код.
  3. Розбивайте свій код на модулі та функції.

SRP — Single Responsibility Principle

Принцип єдиної відповідальності (SRP) стверджує, що клас або модуль повинен мати лише одну причину для зміни. Іншими словами, кожен клас або модуль повинен мати одну єдину відповідальність або завдання для виконання. Цей принцип допомагає підтримувати кодову базу організованою, підтримуваною та масштабованою.

За допомогою впровадження принципу SRP можна досягти наступних переваг:

  1. Організація коду: кожен клас або модуль має чітко визначену мету, що спрощує знаходження та розуміння його функціональності.
  2. Зручність підтримки: з однією відповідальністю зміни та оновлення певної функціональності можуть бути внесені без впливу на інші частини кодової бази.
  3. Дотримання принципів KISS і DRY: з чіткою та спрямованою відповідальністю стає легше підтримувати простоту коду та уникати дублювання коду.

У нашому фрагменті коду Http-сервіс відповідальний за виконання запитів та логування повідомлень. Функціонал логування повинен бути оброблений у власному класі, Http-сервіс не повинен бути відповідальний за це. Як цей принцип стверджує, кожен сервіс повинен бути відповідальний за виконання однієї задачі.

Ось як виглядає код з початку цієї статті, якщо застосувати принцип SRP:

// src/libs/packages/http/http.package.ts
type Constructor = {
  baseUrl: string;
  logger: Logger;
};
class Http {
    private baseUrl: string;
  private logger: Logger;
    public constructor(baseUrl, logger) {
        this.baseUrl = baseUrl;
        this.logger = logger;
    }
    // Other methods
    public async load<T = unknown>({
    method,
    url,
    payload,
    contentType,
  }: LoadArguments): Promise<T> {
    const headers = this.getHeaders(contentType);
    const response = await fetch(`${this.baseUrl}${url}`, {
      method,
      headers,
      body: payload ? JSON.stringify(payload) : null,
    });
    this.logger.log(`${method} ${url} - Status: ${response.status}`);
    this.logger.log(`Response: ${JSON.stringify(response)}`);
    return response.json();
  }
    // Other methods
}
// src/libs/packages/logger/logger.package.ts
class Logger {
    public log(message: string) {
        console.log('LOGGER: ', message);
    }
}
// src/libs/packages/logger/logger.ts
const logger = new Logger();
// src/libs/packages/http/http.ts
const http = new Http({
	baseUrl: 'https://api.example.com',
	logger,
});
;

Ми створили окремий клас Logger, який відповідає за логування повідомлень. Тепер запити HTTP та логування повідомлень розділені та обробляються різними класами. Дотримуючись принципу SRP, ви можете створювати більш модульний та підтримуваний код, що призводить до загальної вищої якості програмного забезпечення.

Як застосовувати принцип SRP

  1. Чітко визначайте відповідальності.
  2. Дотримуйтесь принципу SRP в функціях і методах.
  3. Спочатку визначте відповідальності, а потім розділіть їх.

Висновок

Принципи програмування, які ми розглянули (KISS, DRY і SRP), є важливими для написання високоякісного, підтримуваного та читабельного коду. Притримуючись цих принципів, розробники можуть покращити ефективність свого процесу розробки, зменшити дублювання коду та зробити його більш організованим і адаптивним.

Важливо відзначити, що ці принципи не є специфічними для жодної мови програмування і можуть бути універсально застосовані в будь-якій галузі розробки. Впровадження цих принципів значно сприятиме розробникам у їхньому програмувальному шляху.

Існують також інші принципи, як-от SOLID, YAGNI та інші. Ви можете спробувати дізнатися більше про них, але вони будуть вимагати більше вашої уваги, бо самі собою є складнішими. Але як і принципи, що ми з вами розібрали в цій статті, вони є дуже корисними.

Після рефакторингу нашого фрагмента коду, одним принципом за одним, кінцева версія виглядає так:

// src/libs/types/content-type.type.ts
type ContentType = 'application/json' | 'multipart/form-data';
// src/libs/packages/http/libs/types/http-method.type.ts
type HttpMethod = 'GET' | 'POST';
// src/packages/users/libs/types/user-dto.type.ts
type UserDto = {
  id: number;
  name: string;
};
// src/packages/posts/libs/types/post-dto.type.ts
type PostDto = {
  title: string;
  content: string;
};
// src/libs/packages/http/http.package.ts
type LoadArguments = {
  method: HttpMethod;
  url: string;
  payload?: BodyInit | null;
  contentType?: ContentType;
};
type Constructor = {
  baseUrl: string;
  logger: Logger;
};
class Http {
  private baseUrl: string;
  private logger: Logger;
  public constructor({ baseUrl, logger }: Constructor) {
    this.baseUrl = baseUrl;
    this.logger = logger;
  }
  private getHeaders(contentType?: ContentType): Headers {
    const headers = new Headers();
    if (contentType) {
      headers.append('Content-Type', contentType);
    }
    return headers;
  }
  public async load<T = unknown>({
    method,
    url,
    payload,
    contentType,
  }: LoadArguments): Promise<T> {
    const headers = this.getHeaders(contentType);
    const response = await fetch(`${this.baseUrl}${url}`, {
      method,
      headers,
      body: payload ? JSON.stringify(payload) : null,
    });
    this.logger.log(`${method} ${url} - Status: ${response.status}`);
    this.logger.log(`Response: ${JSON.stringify(response)}`);
    return response.json();
  }
}
// src/libs/packages/logger/logger.package.ts
class Logger {
  public log(message: string) {
    console.log('LOGGER: ', message);
  }
}
// src/libs/packages/logger/logger.ts
const logger = new Logger();

// src/libs/packages/http/http.ts
const http = new Http({ baseUrl: 'https://api.example.com', logger });

// src/slices/users/actions.ts
const getUserById = (id: number): Promise<UserDto> => {
  return http.load<UserDto>({
    url: `/user/${id}`,
    method: 'GET',
  });
};
// src/slices/posts/actions.ts
const createPost = (title: string, content: string): Promise<PostDto> => {
  return http.load<PostDto>({
    url: '/posts',
    method: 'POST',
    contentType: 'application/json',
    payload: JSON.stringify({ title, content }),
  });
};
<span data-token-index="0"> </span>
👍ПодобаєтьсяСподобалось6
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Тут багато написали, є ще що добавити, але б звернув увагу на грубійшу помилку. Можливість визначити Content-Type з захаркоданим використання JSON.stringify. Багато говорить «професіоналізм» автора щоб вчити інших.

На прикладі 200 рядків коду ти ніколи не розкриєш потужність. Там усе буде виглядати добре та гнучко.

Гарна стаття, дякую. Новий рівень з кодом.

просто http клиенту мапу з увсіма налаштуваннямі передавати і все (ну бачу ви так і зробили)

складніше це менеджіти все що може трапитись при network call (кеші, ретрай circuit брейкери, таймаути, якісь окремий ретрай для мертвих запитів, e.t.c.)

а якщо це в вас js клиент, то треба зберігати статус запиту якій можна синхронно опросити і показати лоадер на UI наприклад

Якщо чесно, то одна із найслабших технічних статей останнім часом. Таке враження, що написано для тих, хто тільки вчора почав вивчати програмування.

Ось як виглядає код з початку цієї статті, якщо застосувати принцип KISS:

Ви самі у своєму коді суперечите принципу KISS. Навіщо нам окремий тип Constructor, якщо ви завжди передаєте лише baseUrl?

type Constructor = {
baseUrl: string;
};

Ви тільки ускладнюєте цим свій код та робите його менш читабельним.

private baseUrl: string;
public constructor({ baseUrl }: Constructor) {
this.baseUrl = baseUrl;
}

А цей код якраз можна спростити, якщо ви дотримуєтеся принципів KISS:
public constructor(private baseUrl: string) {}

Використовуйте читабельні та змістовні імена змінних.

І тут ви собi суперечите, назвавши клас HTTP. HTTP — це назва протоколу. Ваш клас HTTP описує структуру протоколу? Ні, це фактично HTTP клієнт, тому має називатися, наприклад, HTTPClient.

Принцип єдиної відповідальності (SRP) стверджує, що клас або модуль повинен мати лише одну причину для зміни.

Як можна писати про принцип SRP і жодного разу не згадати, що він входить до принципів дизайну SOLID? Це просто край безграмотності.

Іншими словами, кожен клас або модуль повинен мати одну єдину відповідальність або завдання для виконання

Не тільки клас чи модуль, а будь-який юніт (змінна, функція, компонент чи сервіс)

Принципи програмування, які ми розглянули (KISS, DRY і SRP)

SRP ніколи не був принципом програмування, він завжди був принципом дизайну.

private getHeaders(contentType?: ContentType): Headers {
const headers = new Headers();
if (contentType) {
headers.append(’Content-Type’, contentType);
}
return headers;
}

І де тут дотримання принципу KISS? Навіщо створювати об’єкт Headers, якщо contentType може бути null/undefined і виходить ніяких заголовків пересилати не треба?
Простіше було переписати код як:
if (contentType) {
const headers = new Headers();
headers.append(’Content-Type’, contentType);
}

class Http {
public async load({
return response.json();
}
}

А чи можна дізнатися, чому клас називається Http (то виходячи з назви для виконання HTTP-запитів), а повертає він завжди JSON? А якщо мені потрібно повернути текст? Використовувати зненавиджений вами copy-paste і писати новий метод?

type PostDto = {
title: string;
content: string;
};
const createPost = (title: string, content: string): Promise => {

Ви маєте окремий тип PostDto. Чому ви використовуєте не його як аргумент у createPost, а фактично копіюєте його вміст, перераховуючи його поля? Фактично це порушення того ж DRY?

const createPost = (title: string, content: string): Promise => {
return http.load({

Ви неодноразово підкреслюєте у своєму пості, що код має бути простим для розуміння. Як тоді пояснити, що ви оголошуєте функцію createPost (тобто яка має щось створювати), але ця функція в свою чергу викликає http.load(), яка, виходячи з назви, повинна щось повертати, а не змінювати.

Якщо чесно, то одна із найслабших технічних статей останнім часом

Що там з тестуванням віртуальних потоків у Java 21?

Це все добре, люди вже навчились що відповідати на співбесідах особливо про SOLID, а потім відкриваєш пул реквести на роботі а там функції по 300+ рядків і юніт тести не на часі, або тести які нічого не тестують

Ніколи не копіюйте той самий код.

Одразу показує досвід розробника. Точніше його обмеженість. Є підходи, при яких нові версії коду створюються шляхом копіювання старого, а не його зміною. Тому що треба забезпечити плавний перехід між системами без втрати доступності. До всіх вимог треба підходити з розумом, а не тупо виконувати те, що кажуть інші.

Притримуючись цих принципів, розробники можуть покращити ефективність свого процесу розробки, зменшити дублювання коду та зробити його більш організованим і адаптивним.

Бізнесу на це чхати. Зменшення дублювання коду не призводить до зменшення витрат. Організація коду може бути й поганою, що нівелює всі переваги.

Тепер по коду. Явно автор скорочував все для прикладу, але назвати клас Http... це просто триндець як кріпово. Ось ти бачиш в коді new Http(), що робить цей клас? Що за інстанс ти створив? Там тільки HTTP протокол підтримується, чи HTTPS також? Якщо його призначення це комунікація між клієнтом та сервером, то чому він не називається ServerCaller або Fetcher наприклад? А вебсокети там теж можна? А FTP? Бо функція fetch вміє набагато більше, ніж представлено в даному класі. Та й взагалі, нашо нам городити город, якусь обгортку навколо доволі потужної функції fetch? Незрозуміло...

Привіт! Дякую за ваш відгук!

Є підходи, при яких нові версії коду створюються шляхом копіювання старого, а не його зміною

Так, це правильно. Іноді добре скопіювати код, немає жодних правил, які підходять до всіх випадків. Ідея полягає в тому, щоб не повторювати код для схожих функціональностей. Іноді, для абсолютно різних функціональностей, може бути корисно скопіювати та вставити код.

Бізнесу на це чхати. Зменшення дублювання коду не призводить до зменшення витрат. Організація коду може бути й поганою, що нівелює всі переваги.

Бізнес завжди хоче, щоб все працювало належним чином, але це не означає, що кодова база повинна бути жахливою. Іноді краще мати організований код, ніж код, який просто працює.

Щодо імені класу, завжди існують кілька можливих варіантів для найменування будь-якого класу. Як ви вже зазначили, ми могли б назвати клас ServerCaller або Fetcher. Ми можемо придумати десятки інших назв, які відповідали б меті класу.

але це не означає, що кодова база повинна бути жахливою

А хто, окрім розробника-перфекціоніста, про це буде собі голову ламати? Жахливість кодової бази — це про смак та суб’єктивну думку. Компанія продає не якість коду, а продукт. Так, це не значить, що треба лайнокодити, це значить, що деяким аспектам, які дуже цікаві девелоперам, бізнес зовсім не приділяє уваги. Бо бізнес мислить категоріями витрат та доходів. Якщо ви витрачаєте на задачу місяць замість трьох днів — це погано. Якщо ви зміни вносите тиждень, замість одного дня, це погано. Якщо кожна наступна зміна коду вимагає навіть такого самого часу, як й раніше, це також погано. Погано для бізнесу. Тому задача розробника не покривати все тестами, не перейматися з різними принципами та патернами розробки, не влаштовувати танці навколо код-рев’ю, а думати як забезпечити вимоги бізнесу про швидку розробку. Це не складно насправді, але вимагає зусиль над собою.

А хто, окрім розробника-перфекціоніста, про це буде собі голову ламати? Жахливість кодової бази — це про смак та суб’єктивну думку. Компанія продає не якість коду, а продукт.

Буде ламати голову той хто захоче пофіксити баг який раптово на проді виліз

І що, думаєте зламає? Пофіксять аж бігом.

Жахливість кодової бази — це про смак та суб’єктивну думку.

ні — це про те, скільки часу знадобиться для імплементації чергового change request від бізнесу, про регресії, які при цьому вилізуть. Як казав класик, якість коду вимірюється у кількості матюків на реалізацію одного такого change request.

Він пропагує дублювати код і змінювати його. Старий залишати. Ніби має сенс. Але тоді в кількох місцях потрібно фіксити в разі чого🤷‍♂️

Ви тупо вириваєте фразу з контексту та будуєте власні фантазії навколо. Не треба зайвого фантазувати, я тут та можу дати відповіді на всі ваші запитання. В системах, які побудовані на підході клонування ніхто ніколи не фіксить баги в наявному коді. Він вважається імутабельним протягом всього терміну життя. Ні, це не значить, що цей код буде навічно працювати в системі, його рано чи пізно виведуть з обігу. Але однією з головних переваг цього способу розробки — забезпечення безшовного оновлення системи без зупинки функціональності. Якщо ви жодного разу не будували такі системи, то це не значить, що вони не існують та підхід вважається заздалегідь неправильним або недоречним. ;)

Найстрашніші баги вилазять не в спагетті-коді, а в тому, де рівень абстракцій знаходиться за межами розуміння розробника. Але ви правильно кажете, що треба код робити таким, щоб його було дешево підтримувати. Не легко, а саме дешево. Якщо вас лякають баги регресії — значить ваша архітектура недостатньо гнучка або передбачувана. Хочете не боятися вносити зміни? Значить робіть все, щоб це було максимально передбачувано та легко робити, а головне — дешево.

Важливо відзначити, що ці принципи не є специфічними для жодної мови програмування і можуть бути універсально застосовані в будь-якій галузі розробки.

Якщо чесно, набагато важливіше говорити про обмеження, ніж постулювати універсальність цих (і подібних) рекомендацій. Бо вони не є аксіомами, і є доволі багато «але». Скажімо, якщо у вас є якийсь код, який дуже рідко змінюється, але дуже часто виконується, може бути доцільно принести в жертву простоту коду заради оптимізації. Чи, скажімо, копі-паста на практиці часто є меншим злом порівняно з безглуздими абстракціями — коли сову щосили натягують на глобус, аби за будь яку ціну залишитись DRY.

Я не вважаю, що будь-який з принципів універсально є хорошим. Фактично, не завжди добре застосовувати їх всюди. Але завжди корисно знати про них і впроваджувати, коли вони можуть бути дійсно корисними.

якщо у вас є якийсь код, який дуже рідко змінюється,

то ваш проект вже мертвий. Усі ці принципи — про супровід коду, коли у бізнеса постійно виникає потреба у змінах.

Підписатись на коментарі