Аргументи, функції та null. 15 принципів «чистого» коду



«Звичайно, поганий код можна відчистити. Але це дуже дорого», — писав Роберт Мартін або Дядько Боб, визнаний у спільноті програмістів експерт із чистоти коду. Навіщо слідкувати за якістю коду? Щоби заощадити час, гроші і зусилля.

У статті для 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;

}


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


Що почитати та подивитись?


Підписуйся на нашу розсилку

та отримуй корисні матеріали першим!

Дякуємо, що підписалися.

image-from-rawpixel-id-5996033-png.png