JavaRush/Java блог/Random UA/Правила написання коду: від створення системи до роботи з...
Константин
36 рівень

Правила написання коду: від створення системи до роботи з об'єктами

Стаття з групи Random UA
учасників
Усім доброго дня: сьогодні мені хотілося б поговорити з вами про правильне написання коду. Коли я тільки починав програмувати, ніде не було чітко написано, що так писати можна, а якщо ось так напишеш, я знайду тебе і…. У результаті в голові я мала велику кількість питань: а як правильно писати, яких принципів варто дотримуватися в тій чи іншій ділянці програми і т.д. Правила написання коду: від створення системи до роботи з об'єктами.Ну, не всім ось так відразу хочеться вгризатися в книги типу Clean Code, тому що написано в них багато, а спочатку мало зрозуміло. Та й доки дочитаєш, все бажання кодувати можна відбити. Тому виходячи зі всього вищесказаного, сьогодні я хочу надати вам невеликий посібник (зведення невеликих рекомендацій) для написання більш високорівневого коду. У цій статті пройдемося за основними правилами та концепціями, які стосуються створення системи, роботи з інтерфейсами, класами та об'єктами. Прочитання цього матеріалу не займе у вас багато часу і, сподіваюся, не дасть нудьгувати. Я піду “згори донизу”, тобто від загальної структури додатка до більш вузьконаправлених деталей. Правила написання коду: від створення системи до роботи з об'єктами.

Система

Загальні бажані характеристики системи такі:
  • мінімальна складність – необхідно уникати надто ускладнених проектів. Головне - це простота та зрозумілість (краще = просте);
  • простота супроводу - при створенні додатка необхідно пам'ятати, що його потрібно буде підтримувати (навіть якщо це не ви), тому код повинен бути зрозумілим і очевидним;
  • слабке сполучення – це мінімальна кількість зв'язків між різними частинами програми (максимальне використання принципів ОВП);
  • можливість перевикористання - проектування системи з можливістю перевикористовувати її фрагменти в інших додатках;
  • портованість - система повинна бути легко адаптована до іншого середовища;
  • єдиний стиль — проектування системи у єдиному стилі у її фрагментах;
  • розширюваність (масштабованість) — покращення системи без порушення її базової структури (якщо додати або змінити якийсь фрагмент, це не повинно впливати на інші).
Побудувати програму, яка не потребує доробок, без додавання функціоналу, — фактично неможливо. Нам завжди потрібно буде вводити нові елементи, щоб наше дітище могло йти в ногу з часом. І тут на сцену виходить масштабованість . Маштабованість - це по суті розширення програми, додавання нового функціоналу, робота з великою кількістю ресурсів (або, по-іншому, з більшим навантаженням). Тобто ми повинні дотримуватись деяких правил, таких як зменшення зв'язаності системи за рахунок збільшення модульності, щоб було простіше додавати нову логіку.

Етапи проектування системи

  1. Програмна система - проектування програми у загальному вигляді.
  2. Поділ на підсистеми/пакети - визначення частин, що логічно розділяються, і визначення правил взаємодії між ними.
  3. Поділ підсистем на класи — поділ частин системи, на конкретні класи та інтерфейси, і навіть визначення взаємодії з-поміж них.
  4. Поділ класів на методи - повне визначення необхідних методів для класу, виходячи із завдання цього класу. Проектування методів – детальне визначення функціональності окремих методів.
Зазвичай проектуванням і займаються рядові розробники, а тими пунктами, що описані вище, — архітектор програми.

Основні принципи та концепції проектування системи

Ідіома відкладеної ініціалізації Програма не витрачає час на створення об'єкта до моменту його безпосереднього використання, що прискорює процес ініціалізації та зменшує завантаження збирача сміття. Але свою чергу, з цим не варто перегинати, оскільки це може призвести до порушення модульності. Можливо, варто перенести всі моменти конструювання в певну частину, наприклад, main, або в клас, що працює за принципом фабрики . Один з аспектів якісного коду - відсутність шаблонного коду, що часто повторюється. Як правило, такий код виноситься до окремого класу, щоб його можна було викликати в потрібний момент. АОП Окремо хотілося б відзначити аспектно орієнтоване програмування. Це програмування шляхом впровадження наскрізної логіки, тобто код, що повторюється, виноситься в класи — аспекти, і викликається при досягненні певних умов. Наприклад, при зверненні до методу із певною назвою або звернення до змінної певного типу. Іноді аспекти можуть плутати, оскільки не відразу зрозуміло, звідки викликається код, проте це дуже корисний функціонал. Зокрема, при кешуванні чи логуванні: ми навішуємо цей функціонал, при цьому не додаючи додаткову логіку до звичайних класів. Почитати більше про оап можна тут . 4 правила проектування простої архітектури Згідно з Кентом Беком
  1. Виразність - необхідність чітко вираженої мети класу, досягається шляхом правильного іменування, невеликого розміру та дотримання принципу single responsibility (трохи нижче розглянемо докладніше).
  2. Мінімум класів і методів - у своєму прагненні розбити класи на якомога дрібніші і односпрямовані можна зайти занадто далеко (антипатерн - стрілянина дробом). Цей принцип закликає все ж таки зберігати компактність системи і не заходити занадто далеко, створюючи за класом на кожен чих.
  3. Відсутність дублювання - зайвий код, який плутає, - ознака не кращого проектування системи, що виноситься в окреме місце.
  4. Виконання всіх тестів - система, що пройшла всі тести, контрольована, оскільки будь-яка зміна може спричинити падіння тестів, що може показати нам - зміна внутрішньої логіки методу потягнула і зміна очікуваної поведінки.
SOLID При проектуванні системи варто враховувати й загальновідомі принципи SOLID: S – single responsibility – принцип єдиної відповідальності; O - open-closed - принцип відкритості/закритості; L - Liskov substitution - принцип підстановки Барбари Лисков; I - interface segregation - принцип поділу інтерфейсу; D - dependency inversion - принцип інверсії залежностей; Саме на кожному принципі зупинятися не будемо (це трохи виходить за межі цієї статті, але тут можна ознайомитися докладніше

Interface

Мабуть, один із найважливіших етапів створення адекватного класу — це створення адекватного інтерфейсу, який представлятиме хорошу абстракцію, що приховує деталі реалізації класу, і при цьому представлятиме групу методів, які чітко узгоджуються між собою. Розглянемо докладніше один із принципів SOLID - interface segregation : клієнти (класи) не повинні реалізовувати непотрібні методи, які вони не будуть використовувати. Тобто, якщо йдеться про побудову інтерфейсів з мінімальною кількістю методів, спрямованих на виконання єдиного завдання цього інтерфейсу (як на мене, дуже схоже з single responsibility), краще замість одного роздутого інтерфейсу створити пару дрібніших. Добре, що клас може реалізовувати не один інтерфейс, як у випадку з успадкуванням. Також потрібно пам'ятати про правильне іменування інтерфейсів: назва повинна якнайточніше відображати його завдання. І, звичайно, чим воно буде коротшим, тим менше плутанини викличе. Саме на рівні інтерфейсів зазвичай пишуть коментарі для документації , які, у свою чергу, допомагають нам докладно описати, що метод повинен робити, які аргументи приймає і що він поверне.

Клас

Правила написання коду: від створення системи до роботи з об'єктами.Розгляньмо внутрішню організацію класів. А точніше, деякі погляди та правила, яких варто дотримуватись при побудові класів. Як правило, клас повинен починатися зі списку змінних, розташованих у визначеному порядку:
  1. public static константи;
  2. private static константи;
  3. private змінні екземпляра.
Далі йдуть різноманітні конструктори в порядку від меншої кількості аргументів до більшого. Після них випливають методи від більш відкритого доступу до найбільш закритих: як правило приватні методи, що приховують реалізацію деякого функціоналу, який ми хочемо обмежити, стоять у самому низу.

Розмір класу

Тепер хотілося б поговорити про розмір класу. Правила написання коду: від створення системи до роботи з об'єктами.Згадаймо один із принципів SOLID - single responsibility . Single responsibility - принцип єдиної відповідальності. Він свідчить, що з кожного об'єкта є лише одне мета (відповідальність), і логіка всіх його методів спрямовано її забезпечення. Тобто виходячи з цього, ми повинні уникати великих, роздутих класів (що за своєю природою – антипатерн – «божественний об'єкт»), і якщо у нас дуже багато методів різноманітної, різнотипної логіки в класі, потрібно замислитися над тим, щоб розбити його на кілька логічних елементів (класів). Це, у свою чергу, підвищить читаність коду, тому що нам не потрібно багато часу, щоб зрозуміти мету методу, якщо ми знаємо зразкове призначення даного класу. Також потрібно істежити за ім'ям класу: воно повинно відображати логіку, що міститься в ньому. Скажімо, якщо у нас клас, на ім'я якого 20+ слів, потрібно подумати про рефакторинг. У кожного класу, що поважає себе, має бути не така вже й велика кількість внутрішніх змінних. Фактично кожен метод працює з однією з них або з декількома, що викликає велику пов'язаність усередині класу (що і власне і має бути, тому що клас має бути як єдине ціле). Як результат, підвищення пов'язаності класу призводить до зменшення його як такого, та й, ясна річ, у нас збільшується кількість класів. Деяких це напружує, так треба більше ходити по класах, щоб побачити, як працює конкретне велике завдання. Крім того, кожен клас - це невеликий модуль, який повинен бути мінімально пов'язаний з іншими. Подібна ізольованість зменшує кількість змін,

Об'єкти

Правила написання коду: від створення системи до роботи з об'єктами.

Інкапсуляція

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

Закон Деметри

Також можна розглянути закон «Деметри»: це невеликий набір правил, який допомагає в управлінні складністю на рівні класів та методів. Отже, припустимо, що у нас є об'єкт Car, і у нього є метод move(Object arg1, Object arg2). Відповідно до закону Деметри, цей метод обмежується викликом:
  • методів самого об'єкта Car(інакше кажучи – this);
  • методів об'єктів, створених у move;
  • методів переданих об'єктів як аргументи - arg1, arg2;
  • методів внутрішніх об'єктів Car(той же цей).
Інакше кажучи, закон «Деметри» — це щось подібне до дитячого правила — розмовляти можна з друзями, але не з чужинцями .

Структура даних

Структура даних – це набір пов'язаних елементів. Під час розгляду об'єкта як структури даних — набір елементів даних, із якими працюють методи, існування яких мається на увазі неявно. Тобто це об'єкт, метою якого є зберігання та робота (обробка) даних, що зберігаються. Ключова відмінність від звичайного об'єкта полягає в тому, що об'єкт - це набір методів, які працюють з елементами даних, існування яких мається на увазі неявно. Розумієте? У звичайному об'єкті головним аспектом є методи, і внутрішні змінні спрямовані на їхню правильну роботу, а в структурі даних навпаки: методи підтримують, допомагають працювати з елементами, які тут і є головними. Один із різновидів структур даних — Data Transfer Object (DTO). Це клас з відкритими змінними та без методів (або тільки методами для читання/запису), які передають дані при роботі з базами даних, працюють з парсингом повідомлень із сокетів тощо. Зазвичай у таких об'єктах дані довго не зберігаються і майже відразу конвертуються по суті, з якою і працює наша програма. Сутність, у свою чергу, теж виходить структурою даних, але її призначення — брати участь у бізнес-логіці на різних рівнях програми, а DTO — транспортувати дані в/з програми. Приклад DTO:
@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Все як би відомо, але тут ми дізнаємося про існування гібридів. Гібриди – це об'єкти, які містять методи для обробки важливої ​​логіки та зберігають внутрішні елементи та методи доступу до них (get/set). Подібні об'єкти сумбурні і ускладнюють додавання нових методів. Не варто використовувати їх, тому що незрозуміло, для чого вони призначені для зберігання елементів або виконання якоїсь логіки. Про можливі типи об'єктів можна почитати тут .

Принципи створення змінних

Правила написання коду: від створення системи до роботи з об'єктами.Давайте трохи поміркуємо на тему змінних, а точніше подумаємо: які можуть бути принципи їх створення:
  1. В ідеалі необхідно оголошувати та ініціалізувати змінну безпосередньо перед її використанням (а не створабо, і забули про неї).
  2. По можливості оголошувати змінні як final, щоб запобігти зміні значення після ініціалізації.
  3. Не забувати про змінні-лічильники (зазвичай ми їх використовуємо в якомусь циклі for, тобто потрібно не забувати їх обнулити, інакше це може зламати нам всю логіку).
  4. Потрібно намагатися ініціалізувати змінні у конструкторі.
  5. Якщо існує вибір між використанням об'єкта з посиланням або без ( new SomeObject()), робіть вибір на користь без , оскільки цей об'єкт після використання видаляється під час наступного складання сміття і не використовуватиме ресурси даремно.
  6. Робіть час життя змінних якомога коротше (відстань між створенням змінної та останнім зверненням).
  7. Ініціалізуйте змінні, що використовуються у циклі, безпосередньо перед циклом, а не на початку методу, що містить цикл.
  8. Починайте завжди з обмеженої області видимості і розширюйте її лише за необхідності (потрібно намагатися робити змінну якомога локальнішої).
  9. Використовуйте кожну змінну лише з однією метою.
  10. Уникайте змінних із прихованим змістом (змінна розривається між двома завданнями, отже, для вирішення однієї з них її тип не підходить).
Правила написання коду: від створення системи до роботи з об'єктами - 7

Методи

Правила написання коду: від створення системи до роботи з об'єктами.Перейдемо безпосередньо до реалізації нашої логіки, а саме до методів.
  1. Перше правило – це компактність. В ідеалі один метод не повинен перевищувати 20 рядків, тому якщо, скажімо, public метод значно "розбухне", потрібно задуматися про винесення логіки, що відокремлюється, в приватні методи.

  2. Друге правило - блоки в командах if, else, whileі так далі, не повинні мати велику вкладеність: це значно знижує читання коду. В ідеалі вкладеність має бути не більше двох блоків {}.

    Код у цих блоках теж бажано робити компактним та простим.

  3. Третє правило - метод повинен виконувати лише одну операцію. Тобто, якщо метод виконує складну різноманітну логіку, ми б'ємо його на підметоди. У результаті сам спосіб буде фасадом, мета якого — виклик решти операцій у правильному порядку.

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

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

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

  6. Окремо хотілося б виділити методи, що мають вхідним аргументом якийсь boolean прапор , оскільки це само собою передбачає, що даний метод реалізує більше однієї операції (якщо true то одна, false - інша). Як я писав вище, це не є добре і по можливості цього слід уникати.

  7. Якщо метод має велику кількість вхідних аргументів (крайнє значення — 7, але варто замислюватися вже після 2-3), необхідно згрупувати деякі аргументи в окремому об'єкті.

  8. Якщо є кілька схожих методів (перевантажених) , то схожі параметри необхідно передавати в тому самому порядку: це підвищує читаність і зручність використання.

  9. Коли ви передаєте в метод параметри, ви повинні бути впевнені, що всі вони будуть використані, інакше навіщо потрібен цей аргумент? Випиляйте його з інтерфейсу та й усе.

  10. try/catchвиглядає за своєю природою не дуже красиво, тому непоганим ходом було б винести його в окремий проміжний метод (метод для обробки винятків):

    public void exceptionHandling(SomeObject obj) {
        try {
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
Щодо повторюючого коду я говорив вище, але додам і тут: Якщо у нас є пара методів з частинами коду, що повторюються, необхідно винести його в окремий метод, що підвищить компактність як методу, так і класу. І не варто забувати про правильні найменування. Деталі правильного іменування класів, інтерфейсів, методів та змінних я розповім у наступній частині статті. А нам у мене сьогодні все. Правила написання коду: від створення системи до роботи з об'єктами.Правила написання коду: сила правильних імен, хороші та погані коментарі
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.