Skip to content

Latest commit

 

History

History
295 lines (240 loc) · 16 KB

3-painless-refactoring.md

File metadata and controls

295 lines (240 loc) · 16 KB

Безболезненный рефакторинг

A> "Надежно зафиксированный больной не нуждается в анестезии."

"Статическая" типизация

Маленькие и большие программные проекты отличаются многим, в том числе и стилем работы. Большую часть времени в небольших проектах занимается собственно кодирование - набор кодовой базы. В больших проектах же - навигация по этой кодовой базе: перемещение от одного класса к другому, от вызова метода к его коду, от кода метода к его вызовам (функция Find Usages). Иногда больше 95% времени на задачу занимает именно блуждание по лабиринту кода.

А для того, чтобы это блуждание было менее болезненным, проекты постоянно нуждаются в рефакторинге:

  • извлечение методов и классов из других методов и классов;
  • переименование их, добавление и удаление параметров;

Современные среды разработки (IDE) имеют на борту кучу инструментов для продвинутой навигации и рефакторинга, которые облегчают его, а иногда и выполняют его полностью автоматически. Однако, динамическая природа PHP часто вставляет палки в колёса.

public function publishPost($id)
{
    $post = Post::find($id);
    $post->publish();
}

// или

public function publishPost($post)
{
    $post->publish();
}

В обоих этих случаях IDE не может самостоятельно понять, что был вызван метод publish класса Post. Для того чтобы добавить новый параметр в этот метод, нужно будет найти все использования этого метода.

public function publish(User $publishedBy)

IDE не сможет сама найти их. Разработчику придётся искать по всему проекту слово «publish» и найти среди результатов именно вызовы данного метода. Для каких-то более распространённых слов (name или create) и при большом размере проекта это может быть весьма мучительно.

Представим ситуацию когда команда обнаруживает, что в поле email в базе данных находятся значения, не являющиеся корректными email-адресами. Как это произошло? Необходимо найти все возможные присвоения полю email класса User и проверить их. Весьма непростая задача, если учесть, что поле email виртуальное и оно может быть присвоено вот так:

$user = User::create($request->all());
//or
$user->fill($request->all());

Эта автомагия, которая помогала нам так быстро создавать приложения, показывает свою истинную сущность, преподнося такие вот сюрпризы. Такие баги в продакшене иногда очень критичны и каждая минута важна, а я до сих пор помню, как целый день в огромном проекте, который длится уже лет 10, искал все возможные присвоения одного поля, пытаясь найти, где оно получает некорректное значение.

После нескольких подобных случаев тяжелого дебага, а также сложных рефакторингов, я выработал себе правило: делать PHP-код как можно более статичным. IDE должна знать все про каждый метод и каждое поле, которое я использую.

public function publish(Post $post)
{
    $post->publish();
}

// или с phpDoc

public function publish($id)
{
    /**
     * @var Post $post
     */
    $post = Post::find($id);
    $post->publish();
}

Комментарии phpDoc могут помочь и в сложных случаях:

/**
 * @var Post[] $posts
 */
$posts = Post::all();
foreach($posts as $post) {
    $post->// Здесь IDE должна подсказывать
           // все методы и поля класса Post
}

Подсказки IDE приятны при написании кода, но намного важнее, что подсказывая их, она понимает откуда они и всегда найдёт их использования.

Если функция возвращает объект какого-то класса, он должен быть объявлен как return-тип (начиная с PHP7) или в @return теге phpDoc-комментария функции.

public function getPost($id): Post
{
    //...
}

/**
 * @return Post[] | Collection
 */
public function getPostsBySomeCriteria(...)
{
    return Post::where(...)->get();
}

Меня пару раз спрашивали: зачем я делаю Java из PHP? Это не совсем так. Я просто создаю маленькие комментарии, чтобы иметь удобные подсказки от IDE прямо сейчас и огромную помощь в будущем, при навигации, рефакторинге и дебаггинге. Даже для небольших проектов они невероятно полезны.

Шаблоны

На сегодняшний день все больше и больше проектов имеют только API-интерфейс, однако количество проектов, напрямую генерирующих HTML все ещё велико. Они используют шаблоны, в которых много вызовов методов и полей. Типичный вызов шаблона в Laravel:

return view('posts.create', [
    'author' => \Auth::user(),
    'categories' => Category::all(),
]);

Он выглядит как вызов некоей функции. Сравните с этим псевдо-кодом:

/**
* @param User $author
* @param Category[] | Collection $categories
 */
function showPostCreateView(User $author, $categories): string
{
    //
}

return showPostCreateView(\Auth::user(), Category::all());

Хочется так же описать и параметры шаблонов. Это легко, когда шаблоны написаны на чистом PHP — комментарии phpDoc легко бы помогли. Для шаблонных движков, таких как Blade, это не так просто и зависит от IDE. Я работаю в PhpStorm, поэтому могу говорить только про него. С недавних пор там тоже можно объявлять типы через phpDoc:

<?php
/**
 * @var \App\Models\User $author
 * @var \App\Models\Category[] $categories
 */
?>

@foreach($categories as $category)
    {{$category->//Category class fields and methods autocomplete}}
@endforeach

Я понимаю, что многим это кажется уже перебором и бесполезной тратой времени, но после всех этих усилий по статической «типизации» мой код в разы более гибкий. Я легко нахожу все использования полей и методов, могу переименовать все автоматически. Каждый рефакторинг приносит минимум боли.

Поля моделей

Использование магических методов __get, __set, __call и других соблазнительно, но опасно — находить такие магические вызовы будет сложно. Если вы используете их, лучше снабдить эти классы нужными phpDoc комментариями. Пример с небольшой Eloquent моделью:

class User extends Model
{
    public function roles()
    {
        return $this->hasMany(Role::class);
    }
}

Этот класс имеет несколько виртуальных полей, представляющих поля таблицы users, а также поле roles. С помощью пакета laravel-ide-helper можно автоматически сгенерировать phpDoc для этого класса. Всего один вызов artisan команды и для всех моделей будут сгенерированы комментарии:

/**
 * App\User
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property-read Collection|\App\Role[] $roles
 * @method static Builder|\App\User whereEmail($value)
 * @method static Builder|\App\User whereId($value)
 * @method static Builder|\App\User whereName($value)
 * @mixin \Eloquent
 */
class User extends Model
{
    public function roles()
    {
        return $this->hasMany(Role::class);
    }
}

$user = new User();
$user->// Здесь IDE подскажет все поля!

Возвратимся к примеру из прошлой главы:

public function store(Request $request, ImageUploader $imageUploader) 
{
    $this->validate($request, [
        'email' => 'required|email',
        'name' => 'required',
        'avatar' => 'required|image',
    ]);
    
    $avatarFileName = ...;    
    $imageUploader->upload($avatarFileName, $request->file('avatar'));
        
    $user = new User($request->except('avatar'));
    $user->avatarUrl = $avatarFileName;
    
    if (!$user->save()) {
        return redirect()->back()->withMessage('...');
    }
    
    \Email::send($user->email, 'Hi email');
        
    return redirect()->route('users');
}

Создание сущности User выглядит странновато. До некоторых изменений оно выглядело хотя бы красиво:

User::create($request->all());

Потом пришлось его поменять, поскольку поле avatarUrl нельзя присваивать напрямую из объекта запроса.

$user = new User($request->except('avatar'));
$user->avatarUrl = $avatarFileName;

Оно не только выглядит странно, но и небезопасно. Этот метод используется в обычной регистрации пользователя. В будущем может быть добавлено поле admin, которое будет выделять администраторов от обычных смертных. Какой-нибудь сообразительный хакер может просто сам добавить новое поле в форму регистрации:

<input type="hidden" name="admin" value="1"> 

Он станет администратором сразу же после регистрации. По этим причинам некоторые эксперты советуют перечислять все нужные поля (есть ещё метод $request->validated(), но его изъяны будут понятны позже в книге, если будете читать внимательно):

$request->only(['email', 'name']);

Но если мы и так перечисляем все поля, может просто сделаем создание объекта более цивилизованным?

$user = new User();
$user->email = $request['email'];
$user->name = $request['name'];
$user->avatarUrl = $avatarFileName;

Этот код уже можно показывать в приличном обществе. Он будет понятен любому PHP-разработчику. IDE теперь всегда найдёт, что в этом месте полю email класса User было присвоено значение.

«Что, если у сущности 50 полей?» Вероятно, стоит немного поменять интерфейс пользователя? 50 полей - многовато для любого, будь то пользователь или разработчик. Если не согласны, то дальше в книге будут показаны пару приемов, с помощью которых можно сократить данный код даже для большого количества полей.

Laravel Idea

Это было настолько важным для меня, что я разработал плагин для PhpStorm - Laravel Idea. Он весьма неплохо разбирается в магии Laravel и устраняет необходимость во всех phpDoc, о которых я писал выше, предлагает кучу кодо-генераций и сотни других функций. Приведу пару примеров.

User::where('email', $email);

Плагин виртуально свяжет строку 'email' в этом коде с полем $email класса User. Это позволяет подсказывать все поля сущности для первого аргумента метода where, а также находить все подобные использования поля $email и даже автоматически переименовать все такие строки, если пользователь переименует $email в какой-нибудь $firstEmail. Это работает даже для сложных случаев:

Post::with('author:email');

Post::with([
    'author' => function (Builder $query) {
        $query->where('email', 'some@email');
    }]);

В обоих этих случаях PhpStorm найдёт, что здесь было использовано поле $email. То же самое с роутингом:

Route::get('/', 'HomeController@index');

Здесь присутствуют ссылки на класс HomeController и метод index в нём. Если попросить PhpStorm найти места, где используется метод index - он найдет место в этом файле роутов. Такие фичи, на первый взгляд не нужные, позволяют держать приложение под бОльшим контролем, который просто необходим для приложений среднего или большого размеров.

Мы сделали наш код более удобным для будущих рефакторингов или дебага. Эта «статическая типизация» не является обязательной, но она крайне полезна. Необходимо хотя бы попробовать.