Пишемо чистий код в Angular

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Мене звати Романа Мадай, я Senior Front End developer в Intellias. Рада працювати в команді професіоналів, де цінується написання чистого коду. У цій статті я розповім про те, що таке чистий код та як писати його в Angular.

Що таке чистий код

Написання чистого коду має дещо спільне з живописом, адже потребує знань, досвіду та таланту. Тому, певною мірою, програміст, який пише код — справжній художник. Однак, здатність відрізнити чистий код від «брудного» ще не означає вміння його писати. Роберт Мартін у своїй книзі «Чистий код» наводить гарне поняття: «відчуття коду». Воно є трохи абстрактним, але загалом описує те, що здобувається з досвідом, коли ми працюємо з різними командами, людьми і підходами. Саме цей досвід дозволяє нам відчувати код. Тобто, коли програміст читає його, він одразу помічає, що можна покращити. Розробник, який не має цього відчуття або має недостатній досвід, може упустити деякі моменти та пропустити невдалий код у реліз.


У своїй книзі Роберт Мартін описує чистий код такими тезами:

  • Це є елегантний код, робота з яким приносить задоволення.
  • Чистий код має читатися, як добре написана проза. Варто звертати увагу на послідовність коду і дотримуватися кращих практик. Наприклад, в класі спочатку ініціалізуємо публічні методи, потім protected, а в кінці вже private. При читанні коду, якщо один метод викликає інший, очікується, що його реалізація буде нижче і читачу не потрібно буде перестрибувати з двадцятого чи п’ятдесятого рядка на десятий. Це як сторінка книги, яку ви читаєте згори до низу. Ми витрачаємо близько 30% часу на написання коду, а 70% на його читання.
  • Чистий код, над яким ретельно працювали.
  • Читання чистого коду вас абсолютно не здивує. У вас не виникне бажання його одразу переписати та не буде питань до його реалізації.
  • Відсутність дублювання. Потрібно слідувати принципу «don’t repeat yourself», тобто DRY. Код не повинен дублюватися. Якщо ж він все-таки повторюється, потрібно звернути на це увагу. Можливо, перенести його в окремий метод чи клас і спростити дану реалізацію.

Чому важливо писати чистий код

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

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

Відоме так зване правило бойскаутів, яке говорить: «Залиш місце стоянки чистішим, ніж воно було, коли ти прийшов». Тобто, коли ви працюєте з кодом, і бачите, що можна щось покращити, наприклад, найменування змінної чи методу, спростити складну структуру умов, просто візьміть і зробіть кращим.

Теорія розбитих вікон

Теорія розбитих вікон бере свій початок у поведінковій психології мас. Припустимо, стояв будинок 10-15 років, але в один момент хтось розбив вікно. Піде ланцюгова реакція, люди почнуть розбивати інші вікна. Якщо вчасно це не зупинити, почнеться мародерство і будинок перетвориться на руїни.

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

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

Так само це працює з unit тестами. Якщо ви не покрили unit тестами окремий метод, це, власне, сигнал для інших розробників, що так можна робити. Тут знову ж таки діє психологія мас — хтось один почав, інші повторюють. Для того, щоб уникати подібних випадків, існують review процеси. Якщо в команді немає review процесу, потрібно його впроваджувати. Вкрай важливим є звертати увагу на такі невеликі осередки в коді, щоб інші не повторювали цього.

«Запахи коду»

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

Приклад 1

Імпорти звичайних модулів з відносним шляхом. В даному випадку погана конфігурація TypeScript. Є поняття Path mapping і ми можемо в tsconfig.json файлі вказати paths відносно baseUrl.

import { SharedModule } from '../../../../../../shared.module';
import { PermissionsModule } from '../../../../../../permissions/permissions.module';
import { ConsumerModule } from '../../../../../../consumer/consumer.module';
import { ProviderModule } from '../../../../../../provider/provider.module';

Якщо вказаний «baseUrl» — «src», то відносно цього baseUrl вказуються paths, наприклад, для shared, permission модуля тощо. Це покращить читабельність та вигляд коду. Варто звертати увагу на всі корисні властивості мови, яку ви використовуєте і намагатися застосовувати це на практиці.

tsconfig.json файл

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "src",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "paths": {
      "@shared/*": ["app/shared/*"],
      "@permissions/*": ["app/permissions/*"]
    }
  }
}

import { SharedModule } from '@shared"/shared.module';
import { PermissionsModule } from '@permissions/permissions/permissions.module';
import { ConsumerModule } from '@consumer/consumer/consumer.module';
import { ProviderModule } from '@provider/provider/provider.module';

Приклад 2

Ми знаємо, що в Angular є два основні типи модулів: shared i core. Shared модуль використовується для компонент, директив, pipe, які використовуються повторно. Core модуль слід імпортувати лише один раз і це повинно відбуватися лише в root або app модулі. Він використовується виключно для поставки сервісів. Коли розростається сам продукт, а з ним і кодова база, shared модуль стає перевантаженим, виникає дуже багато імпортів, які важко читати і підтримувати. Варто організовувати ці модулі таким чином, щоб вони відповідали за певні задачі. Наприклад, один модуль відповідальний за форми, інший на покупки і так далі. Згодом, якщо виникне потреба, таку модульну структуру легко перенести в окрему бібліотеку.

Приклад 3

В даному прикладі представлений компонент, в конструкторі якого ми підключили 10 залежностей.

  constructor(
    private router: Router,
    private activatedRouter: ActivatedRoute,
    private location: Location,
    private listingService: ListingService,
    private subscriptionService: SubscriptionService,
    private marketplaceService: MarketplaceService,
    private attachmentsService: AttachmentsService,
    private notificationsService: NotificationsService,
    private shopCardService: ShopCardService,
    private readonly userContext: UserContext) {}

Тут чітко видно проблему непродуманого дизайну компоненти, де порушено перший принцип SOLID — принцип єдиної відповідальності (single responsibility). І наш компонент має занадто багато зон відповідальності.

  
constructor(
    private router: Router,
    private activatedRouter: ActivatedRoute,
    private location: Location,
    private listingService: ListingService) {}

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

Приклад 4

Цей приклад стосується більше TypeScript. Ми маємо функцію, в яку передаємо аргумент і вертаємо значення.

  
  square(n) {
    return n * n;
  }

Що ж тут не так? Здебільшого проблемою є відсутність типів. Варто вказати, який тип ми приймаємо та що повертаємо. Якщо вирішили працювати з площею, то варто було вказати тип number.

 
  square(n: number): number {
    return n * n;
  }

Приклад 5

Приклад, який можна часто зустріти, — це subscribe в subscribe-і. В даному випадку ми викликаємо API сервіс, отримуємо випадковий факт і вже в subscribe знову викликаємо API для отримання всіх категорій. Це призводить до Memory leaks.

 private fetchCategories(): void {
    this.apiService.getRandomFact(this.fetchFormValue)
      .subscribe((randomFact: CheckNorrisFact) => {
        this.apiService.getCategories(randomFact.id).subscribe((categories: string[]) => {
          this.categories = categories;
        });
      });
 }

В даному випадку ми можемо використати оператор switchMap. Один з так званих higher order observables: switchMap, concatMap, mergeMap, exhaustMap — оператори, які працюють з потоками. І, власне, switchMap в даному випадку, коли отримує новий потік, відписується від попереднього.

 
 private fetchCategories(): void {
    const categories$ = this.apiService.getRandomfact(this.fetchFormValue)
      .pipe(switchMap((randomFact: ChuckNorrisFact) => this.apiService.getCategories(randomFact.id)));

   categories$.subscribe((categories: string[]) => this.categories = categories);
 }

Приклад 6

Допустимо, ви відкриваєте сайт, написаний на Angular і він відкривається повільно. Очевидна проблема з перфоменсом. Одна з причин — це відсутність Lazy Loading. Усі модулі вантажаться одночасно, навіть якщо вони не потрібні на даній сторінці. До появи Ivy, Angular проєкти не могли існувати без модулів, оскільки компілятор ViewEngine добавляє всі метадані до модулів. З появою Ivy ліниве завантаження можна організувати для окремих компонентів. Тобто сам компонент може існувати без модуля. Це можливо завдяки концепції Locality, що забезпечує більше гнучкості та кращу оптимізацію.

Приклад 7

Цей приклад стосується структури проєкту на Angular. Питання досить гостре, оскільки кожен має свою думку щодо цього. Якщо взяти до уваги цей приклад, тут виникає питання, як можна було організувати структуру проєкту ще краще?

В даному випадку це компоненти, директиви, моделі, пайпи і сервіси. Їх можна було б організувати, розбивши структуру на окремі зони відповідальності, де кожна з них буде мати окрему папку.

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

Рефакторинг та його причини

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

Причин для впровадження рефакторингу може бути чимало. Ось деякі з них:

  1. Коли є потреба змінювати клас знову і знову без додаткових вимог. У даному випадку порушується Open-Closed Principle і потрібно знайти причини.
  2. Якщо ламається один функціонал, коли виправляється інший. Таке може траплятися тому, що порушується Single Responsibility Principle.
  3. Так зване «Правило трьох ударів», яке говорить: «Коли ви робите щось вперше, ви це просто робите. Коли аналогічне потрібно зробити вдруге, ви розумієте, що це копіювання, але робите це. Коли потрібно зробити таке ж втретє, ви робите рефакторинг».
  4. Код розвивається невеликими частинами протягом тривалого часу. Тобто класи та методи розширюються новим функціоналом, що в результаті призводить до нерозуміння коду та його складності.
  5. Змінюються вимоги. Нові вимоги до існуючого функціоналу кожного разу створюють додаткові умови, в результаті чого методи стають складними і з часом незрозумілими.
  6. Змінюється команда. Коли змінюється команда, важливо, щоб люди дотримувались єдиного підходу до виконання роботи, а не робили кожен на свій розсуд. Тут варто звертати увагу на організацію процесів в команді.
  7. Розуміння власних помилок, як наслідок професійного росту. Це, без сумніву, один з найкращих випадків, коли треба рефакторити. Дивитесь на свій код та бачите свої помилки — це дуже хороша ознака.

Рефакторинг читабельності методів та правило трьох секунд

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

Слідуйте принципу KISS (Keep it simple, stupid), якого можна досягти наступним чином:

  • Давайте методам та змінним зрозумілі імена.
  • Уникайте ситуацій, коли в методі буде, скажімо, і калькуляція сум, і http запити. Краще винести в окремий метод калькуляцію, а в інший — http call.
  • Методи не повинні перевищувати 20 рядків.

Рекомендації

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

Кілька порад для написання чистого коду:

  • Пишіть короткі функції. Це загальна рекомендація не лише для Angular, а й для будь-яких інших проєктів на будь-яких інших мовах програмування.
  • Уникайте коментарів в коді. Коментарі мають сказати те, що не може сказати сам код.
  • Пишіть код, який легко читається.
  • Розділяйте логіку (SRP).
  • Використовуйте TypeScript зі всіма його функціями та можливостями.
  • Користуйтесь статичними аналізаторами коду.
  • Використовуйте RxJS.
  • Не завадить почитати книгу Роберта Мартіна «Чистий код». Це хороша книга, в якій висвітлюється багато цікавих речей. Ваш код не стане відразу ідеальним, але ви отримаєте чимало корисних рекомендацій для того, щоб покращити написання коду.

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

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

👍НравитсяПонравилось11
В избранноеВ избранном7
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

Начинайте переходить на nx monorepo, будет намного проще придерживаться solid принципов и всяких core модулей не будет.
По поводу subscribe в subscribe, memory leak то из-за того, что отписки не было, а не потому что сама структура сабскрайбов такая. Но со свитчмэпом все куда нагляднее (как и со всем RxJS).

Щодо структури проекту, то Angular має досить хороші рекомендації з цього приводу. Я використовую приблизно ці ж рекомендації і для бекенда теж, вони там досить добре лягають, хоча інколи потрібно компонувати модулі щоб у них були функції і сервісу, і маршрутизації (наприклад модуль для i18n).

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