Do good code: 8 правил хорошого коду

Практично всім, хто навчався програмування, відома книга Стіва Макконнелла «Досконалий код». Вона завжди справляє враження, перш за все, значною товщиною (близько 900 сторінок). На жаль, реальність така, що іноді враження цим і обмежуються. А даремно. У подальшій професійній діяльності програмісти стикаються практично з усіма ситуаціями, описаними в книзі, і приходять досвідченим шляхом до тих самих висновків. У той час як більш тісне знайомство могло б заощадити час і сили. Ми в GeekBrains дотримуємося комплексного підходу в навчанні, тому провели для слухачів вебінар за правилами створення гарного коду.

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

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

Існує безліч відомих підходів до критеріїв якості коду, про яких рано чи пізно дізнається практично будь-розробник. Наприклад, є програмісти, які дотримуються принципу проектування KISS (Keep It Simple, Stupid! — Роби це простіше, тупиця!). Цей метод розробки цілком справедливий і заслуговує поваги, до того ж відображає універсальне правило хорошого коду — простоту і ясність. Однак простота повинна мати межі — порядок в програмі і читабельність коду не повинні бути результатом спрощення. Крім простоти, існує ще кілька нескладних правил. І вони вирішують ряд завдань.

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

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

  • Гарантувати легкість супроводу. Продумана і реалізована структура програми дозволяє вирішувати питання, пов'язані з роботою програми на новому апаратному забезпеченні або новій платформі.

  • Спрощувати процес внесення подальших змін. Чим краще оптимізована структура, тим простіше змінювати код, додавати нові властивості, підвищувати швидкодію і змінювати архітектуру.

  • Забезпечувати стійкість програми. При внесенні змін або можливі неполадки можна легко внести виправлення. А правильна обробка помилок значно полегшує експлуатацію програмного продукту.

  • Забезпечувати можливість підтримки проекту кількома розробниками або цілими співтовариствами (особливо важливо для проектів з відкритим вихідним кодом).
Будь-код — це реалізація ідей розробника, який має визначену мету: створити розвага, написати корпоративний софт, розвинути навички програмування, створити промислове та ін… Важливо спочатку прийняти правила створення гарного коду і застосовувати їх — така звичка буде працювати на програміста тим інтенсивніше, ніж великих масштабів буде досягати проект.

8 правил хорошого коду за версією GeekBrains
Дотримуйтеся єдиний Code style. Якщо програміст приходить працювати в організацію, особливо велику, то найчастіше його знайомлять з правилами оформлення коду в конкретному проекті (угода по code style). Це не випадковий каприз роботодавця, а свідчення серйозного підходу.
Ось кілька загальних правил, з якими ви можете зіткнутися:
дотримуйтесь переноси фігурних дужок і відступи — це значно покращує сприйняття окремих блоків коду
дотримуйтесь правило вертикалі — частини одного запиту або умови повинні знаходитися на одному відступі
if (typeof a ! == "undefined" &&
typeof b ! == "undefined" &&
typeof c === "string") { 
//your stuff
}

дотримуйтесь розрядку — ставте пропуски там, де вони покращують читабельність коду; особливо це важливо в складених умов, наприклад, умови циклу.
for (var i = 0; i < 100; i++) {
}

У деяких середовищах розробки можна задати правила форматування коду, завантаживши налаштування окремим файлом (доступно в Visual Studio). Таким чином, у всіх програмістів проекту автоматично виходить однотипний код, що значно покращує сприйняття. Відомо, що досить важко перевчатися після довгих років практики і звикати до нових правил. Однак у будь-якої компанії code style — це закон, якому треба слідувати неухильно.

Не використовуйте «магічні числа». Магічні числа не випадково відносять до анти-паттернам програмування, простіше кажучи, правил того, як не треба писати програмний код. Найчастіше магічне число як анти-патерн являє собою використовувану в коді константу, сенс якої неясний без коментаря. Такі числа не тільки ускладнюють розуміння коду і погіршують його читабельність, але і приносять проблеми під час рефакторінгу.
Наприклад, у коді є рядок:
DrawWindow( 50, 70, 1000, 500 );

Очевидно, вона не викличе помилок в роботі коду, але і її зміст не всім зрозумілий. Набагато краще не полінуватися і відразу написати таким чином:
int left = 50;
int top = 70;
int width = 1000;
int height = 500;

DrawWindow( left, top, width, height );

Іноді магічні числа виникають при використанні загальноприйнятих констант, наприклад, при запису числа π. Припустимо, у проекті було внесено:
SquareCircle = 3.14*rad*rad 

Що тут поганого? А є погане. Наприклад, якщо в ході роботи знадобиться зробити розрахунок з високою точністю, доведеться шукати всі входження константи в коді, а це трата трудового ресурсу. Тому краще записати так:
int pi = 3.14;
SquareCircle = pi*rad*rad 

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

Використовувати осмислені імена змінних, функцій, класів. Всім програмістам відомий термін «обфускація коду» — свідоме заплутування програмного року за допомогою програми-обфускатора. Вона робиться з метою приховати реалізацію і перетворює код в невиразний набір символів, перейменовує змінні, змінює імена методів, функцій та ін… На жаль, трапляється так, що код і без обфускации виглядає заплутано — саме за рахунок безглуздих імен змінних і функцій: var_3698, myBestClass, NewMethodFinal і т. д… Це не тільки заважає розробникам, які беруть участь в проекті, але і призводить до нескінченного кількості коментарів. Між тим, перейменувавши функцію, можна позбутися від коментарів — її ім'я буде саме говорити про те, що вона робить.

Вийде так званий самодокументируемый код — ситуація, при якій змінні і функції іменуються таким чином, що при погляді на код зрозуміло, як він працює. У ідеї самодокументируемого коду є багато прихильників і супротивників, до аргументів яких варто прислухатися. Ми рекомендуємо в цілому дотримувати баланс і розумно використовувати і коментарі, і «говорять» імена змінних, і можливості самодокументируемого коду там, де це виправдано.
Наприклад, візьмемо код такого виду:
// знаходимо лист, записуємо в r 
if ( x != null ) {
while ( x.a != null ) {
x = x.a; 
r = x.n;
} 
} 
else { 
r = ""; 
}

З коментаря повинно бути зрозуміло, що саме робить код. Але зовсім неясно, що позначають x.a і x.n. Спробуємо внести зміни таким чином:
// знаходимо лист, записуємо ім'я в leafName
if ( node != null ) {
while ( node.next != null ) {
node = node.next;
leafName = node.name;
}
}
else {
leafName = "";
}

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



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

Для ілюстрації цих правил візьмемо приклад з попереднього пункту і створимо метод, код якого не потребує коментарів:
string GetLeafName ( Node node ) {
if ( node != null ) {
while ( node.next != null ) {
node = node.next;
}
return node.name;
} 
return";
}

І, нарешті, приховаємо реалізацію методу:
... leafName = GetLeafName( node ); ...

На початку методів перевіряйте вхідні дані. На рівні коду потрібно обов'язково робити перевірки вхідних даних у всіх або майже у всіх методах. Це пов'язано з користувальницьким поведінкою: майбутні користувачі можуть вводити будь-які дані, які можуть викликати збої у роботі програми. В будь-якому методі, навіть у тому, який використовувався лише один раз, обов'язково потрібно організовувати перевірку даних і створювати обробку помилок. Це варто зробити, оскільки метод не тільки виступає як рівень абстракції, але і необхідний для перевикористання. В принципі, можливо розділити методи на ті, в яких потрібно робити перевірку, і ті, в яких її робити необов'язково, але для повної впевненості і захисту від «хитрого користувача» краще перевіряти всіх вхідні дані.
У прикладі нижче вставляємо перевірку на те, щоб на вході не отримати null.
List<int> GetEvenItems( List<int> items ) {
Assert( items != null);

List<int> result = new List < int>();
foreach ( int i in items ) {
if ( i % 2 == 0 ) {
result.add(i);
}
}
return result;
}

Реалізуйте за допомогою спадкування лише відношення «є». В інших випадках композиція. Композиція є одним з ключових патернів, націлених на полегшення сприйняття коду і, на відміну від спадкування, не порушує принцип інкапсуляції. Припустимо, у вас є клас Кермо і клас Колесо. Клас Автомобіль можна реалізувати як спадкоємець класу-предка Кермо, але ж Автомобіля потрібні і властивості класу Колесо. Відповідно, програміст починає плодити спадкування. А адже навіть з обивательської точки зору логіки клас Автомобіль — це композиція елементів. Припустимо, є такий код, коли новий клас створюється з використанням спадкування (клас ScreenElement успадковує поля і методи класу Coordinate і розширює цей клас):
сlass Coordinate {
public int x;
public int y;
}
class ScreenElement : Coordinate {
public char symbol;
}

Використовуємо композицію:
сlass Coordinate {
public int x;
public int y;
}
class ScreenElement {
public Coordinate coordinate;
public char symbol;
}

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

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

Візьмемо клас:
class Square {
public float edge;
public float area;
}

Ще один клас:
class Square {
public float GetEdge();
public float GetArea();
public void SetEdge( float e );
public void SetArea( float a );
private float edge;
private float area;
}

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

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

Для тих, хто сприймає інформацію через відео і хоче почути деталі вебінару — відеоверсія:



Інші наші вебінари можна подивитися здесь.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.