«Звичайно, поганий код можна відчистити. Але це дуже дорого», — писав Роберт Мартін або Дядько Боб, визнаний у спільноті програмістів експерт із чистоти коду. Навіщо слідкувати за якістю коду? Щоби заощадити час, гроші і зусилля.
У статті для DOU Денис Оленін, Senior PHP-розробник в AMO, компанії з екосистеми Genesis, поділився своїм баченням базових умов, за яких код буде «чистим» і таким, що легко підтримується. Публікуємо короткий переказ матеріалу.
Чистота коду має значення
Які основні риси «чистого коду»? По-перше, він легко читається, і нові розробники мають змогу швидко розібратися в ньому. По-друге, він оформлений за стандартами спільноти. По-третє, за умови його розширення або внесення змін не виникає супутніх проблем. І останнє — він має передбачувану «поведінку».
Навіщо взагалі потрібно слідкувати за чистотою коду? Коли команда розробників починає простий проєкт з нуля, спочатку все супер. Але бізнес-задачі змінюються, і фахівці стикаються із тим, що зміни до коду вносити дедалі складніше. Якщо якість коду низька, то створення однієї простої кнопки може забрати години або дні. Чистота коду — величина суб’єктивна і така, що складно вимірюється, проте є деякі правила, що допоможуть зробити код більш гнучким та зрозумілим.
Базові вимоги до «чистого» коду
Змістовні імена
Імена для змінних, функцій та класів потрібно обирати таким чином, щоб саме ім’я максимально точно пояснювало, для чого використовується цей код.
// Bad
const DMYDATE = “d.m.Y”;
// Good
const PUBLIC_DATE_FORMAT = “d.m.Y”;
Функції/методи
Функції та методи мають виконувати лише одну операцію та бути максимально короткими. Функції не повинні містити вкладених структур, оскільки це призводить до їх збільшення.
// Bad
public function notify(array $usersId): void
{
$users = DB::table(‘users’)->whereIn(‘id’, $usersId)->get();
foreach ($users as $user) {
notify($user);
}
}
// Good
public function getUsers(array $usersId): Collection
{
return DB::table(‘users’)->whereIn(‘id’, $usersId)->get();
}
public function notify(Collection $users): void
{
foreach ($users as $user) {
notify($user);
}
}
Блоки та відступи
Максимальний рівень відступів у функції — один або два. Це спрощує її читання та розуміння. Блоки в командах if, else, while мають складатися з одного рядка, в якому зазвичай міститься виклик функції.
// Bad
$user = DB::table(‘users’)->find($id);
if ($user) {
$post = $user->post()->first();
if ($post) {
return $post->created_at;
} else {
throw new ModelNotFoundException();
}
} else {
throw new ModelNotFoundException();
}
// Good
$user = DB::table(‘users’)->find($id);
if (! $user) {
throw new ModelNotFoundException();
}
$post = $user->post()->first();
if (! $post) {
throw new ModelNotFoundException();
}
return $post->created_at;
// Best
$user = DB::table(‘users’)->findOrFail($id);
$post = $user->post()->first();
if (! $post) {
throw new ModelNotFoundException();
}
return $post->created_at;
Один рівень абстракції на функцію
Потрібно приховувати другорядні подробиці у функціях або методах. Не варто змішувати рівні абстракції у функціях, тому що це робить код більш заплутаним.
// Bad
function saveFile(Request $request)
{
file_put_content(‘someFileName’, $request->file(‘file’)->body);
$file = new File;
$file->body = $request->file(‘’)->body;
$file->save();
}
// Good
class Storage {
public function store(string $name, string $body) {
file_put_content($name, $body);
}
}
function saveFile(Request $request)
{
(new Storage)->store(‘someFileName’, $request->file(‘file’)->body);
$file = new File;
$file->body = $request->file(‘file’)->body;
$file->save();
}
Читання коду згори вниз
Роберт Мартін, автор книги «Чистий код», наголошує на «правилі пониження». Тобто, за кожною наступною функцією мають слідувати функції, які викликались вище. Так ми можемо читати код послідовно, як оповідь.
// Bad
function isAvailablePost(int $id): bool
{
return DB::table(‘posts’)
->where(‘id’, $id)
->where(‘status’, ‘active’)
->exists();
}
function getPost(int $id)
{
return DB::table(‘posts’)->find($id);
}
function update(Request $request)
{
if (isAvailablePost($request->get(‘post_id’))) {
$post = getPost($request);
$post->update([‘title’ => ‘Some new title’]);
}
}
// Good
function update(Request $request)
{
if (isAvailablePost($request->get(‘post_id’))) {
$post = getPost($request);
$post->update([‘title’ => ‘Some new title’]);
}
}
function isAvailablePost(int $id): bool
{
return DB::table(‘posts’)
->where(‘id’, $id)
->where(‘status’, ‘active’)
->exists();
}
function getPost(int $id)
{
return DB::table(‘posts’)->find($id);
}
Команди switch
Функція зі switch за замовчуванням не може виконувати одну операцію, навіть якщо switch має лише декілька умов. Якщо обійтися без switch неможливо, варто опустити його в низькорівневу логіку додатку.
Аргументи функцій
В ідеальному світі функції і методи не мають містити аргументи, або в крайньому випадку мінімальну кількість аргументів.
Об’єкти як аргументи
Якщо кількість аргументів функції все ж більше двох-трьох, то варто об’єднати деякі аргументи в окрему абстракцію або клас.
// Bad
function sendNotification(string $userName, string $email, string $message);
// Good
function sendNotification(User $user, string $message);
Використання аргументів-прапорців
Аргументи-прапорці можуть спричиняти плутанину в коді, тому варто не вживати їх зовсім. Такі аргументи ускладнюють сигнатуру методу та говорять про те, що функція використовує більш ніж одну операцію.
// Bad
public function context(Request $request): void
{
$someFlag = $request->get(‘someParam’);
$this->someProcess($request, $someFlag);
}
private function someProcess(string $someString, bool $flag)
{
if (! $flag) {
doSomeStuff();
}
doSomeAnotherStuff();
}
// Good
public function context(Request $request): void
{
$someFlag = $request->get(‘someParam’);
if (! $someFlag) {
$this->someProcess($request);
}
$this->someAnotherProcess($request);
}
Виключити побічні ефекти
Функції та методи мають робити лише те, для чого вони написані, виходячи з їх назви.
// Bad
function getPost(int $id)
{
$post = DB::table(‘posts’)->find($id);
$post->views += 1;
$post->save();
return $post;
}
// Good
function getPost(int $id)
{
return DB::table(‘posts’)->find($id);
}
Блоки try/catch
Намагайтеся ізолювати блоки try/catch в окремій функції або методі. Якщо цього не зробити, ви створите плутанину в коді, поєднуючи нормальну обробку із обробкою помилок.
Непотрібні коментарі
Якщо ви додаєте коментарі, які описують процес роботи вашого методу або функції, то код варто переробити. Якісний код не потребує коментарів.
Обов'язкові коментарі
Не варто писати для кожної функції або змінної коментар PHPDoc. В PHP 7+ є всі необхідні конструкції мови, або позбутись таких «обов'язкових» коментарів. Їх варто писати лише тоді, коли розробляється API.
Закон Деметри
Або принцип найменшого знання. Якщо модуль «А» знає про модуль «B», а модуль «B» знає про модуль «С», то модуль «А» не має знати про модуль «С».
// Bad
class Author {
private Post $post;
...
public function (Image $image)
{
$this->post->image->setUrl($image->getUrl());
}
}
// Good
class Post {
private Image $image;
...
public function setImage(Image $image)
{
$this->image->setUrl($image->getUrl());
}
}
class Author {
private Post $post;
...
public function (Image $image)
{
$this->post->setImage($image);
}
}
Null вам не потрібен
Уникайте використання null в вашій бізнес-логіці. Це зайва робота та проблеми на стороні, що викликає. Замість безлічі перевірок на null краще кинути виключення. У крайньому випадку опустіть цю змінну на низький рівень абстракції.
// Bad
function findUser(int $id): ?User
{
return User::find($id);
}
// Good
/**
* @param int $id
* @throws ModelNotFoundException
*/
function findUser(int $id): User
{
$user = User::find($id);
if (! $user) {
throw new ModelNotFoundException;
}
return $user;
}
// Best
class UnknownUser extends User {
public function getName()
{
return ‘Some default name’;
}
}
function findUser(int $id): User
{
$user = User::find($id);
if (! $user) {
return new UnknownUser;
}
return $user;
}
Всі ці принципи — лише база, яку варто опанувати кожному розробнику. Але якщо ви будете їх дотримуватись, це вже дозволить вам писати більш стабільний і зрозумілий код.
Що почитати та подивитись?
«Чистий код. Створення, аналіз, рефакторінг». Роберт Мартін — основне джерело за цією темою.
Handling Exceptional Conditions with Grace and Style. Nikola Poša — корисна доповідь про використання null у коді.
SOLID Principles in PHP. Jeffrey Way — відеоуроки із SOLID з якісними прикладами.