О чём это?
Многие начинают писать проект для работы с 1 малой задачей, не подразумевая, что данная история приведёт к многопользовательской системе управления, ну допустим, контентом или упаси, производством. И всё вроде здорово и классно, всё работает, пока не начинаешь понимать, что тот код, который написан — состоит целиком и полностью из костылей и хардкода. Возникает насущная проблема: при добавлении новой фичи, приходится с этим кодом очень долго и долго возиться, вспоминая “а что же там такое написано то было?” и проклинать себя в прошлом. В частности данная статья предназначена для начинающих разработчиков, которым необходимость рефакторинга на первом этапе не очевидна, а сегодня они попали в неприятную ситуацию.
Допустим, на входе мы имеем жёсткий код с вёрсткой, запросами, костылями, неподдающийся иногда даже прочтению.
Как это поможет
Что приобретёт система в результате рефакторинга?
— Структурированность и модульность кода.
— Разделение UI и логики
— Возможность написания UNIT тесты.
— Возможность написания API только для авторизированных в API приложений.
— Более простой подход к переносу на другую БД.
Частный пример случая ‘а как бы это сделать и с чего начать?’. Я предлагаю заниматься подобными исправлении последовательно, и при этом, не завершив второй этап, не начинать браться за третий. Код мы будем исправлять в рабочей системе, заранее лучше написать функциональные тесты, чтобы понимать что всё работает как надо.
Этапы
1. Алгоритмический подход.
Просмотрим наш ужасный исходный код с вёрсткой, неподдающийся тестированию (и вероятно повторяющийся в других местах). Просто и незамысловато, но последовательность подобного кода — порождает кучу известных неприятностей.
index.php
<h1>Пользователи</h1>
<?
$DB = new DBConnector;
$DB->query(‘SELECT * FROM users LIMIT 10’);
if($DB->get_num_rows()){
while($user = $DB->fetch_row()){
echo ‘<p>’.$user[‘name’].’</p>’;
}
}
?>
2. Функциональный подход.
Почему бы не написать функцию и не вызывать её в других местах данной системы? Переход к функциональному программированию поможет сократить код, хоть и на примере 1 вызова — выглядит страшнее.
functions.php
$DB = new DBConnector
function GetUsers(){
global $DB;
$DB->query(‘SELECT * FROM users LIMIT 10’);
if($DB->get_num_rows()){
while($user[] = $DB->fetch_row());
return $users;
} else {
return array();
}
}
?>
index.php
<h1>Пользователи</h1>
<?
include ‘functions.php’;
$users = GetUsers();
foreach($users as $u) {
echo ‘<p>’.$u.’</p>’;
}
?>
Комментарий: Уже лучше. Если перевести все запросы к такому виду, то точно получится список неких functions.php, работающих с каким-то модулем. Таким образом модель уже отделена от UI и каждая функция может быть протестирована, так же как и последовательность вызовов таких функций.
3. Объектно-ориентированный подход.
Зачем нужно подключать все функции в системе, если достаточно работать только с необходимым набором функциональности? Например на странице простого списка пользователей — нет необходимости подключать весь список функций, таких, например как задача, или проект. Почему бы не создать класс работы с модулем, от которого будет создаваться объект работы только с этим модулем? Можно построить архитектуру так, что наследование функций одноимённых параметров методов Get (add / edit) просто-напросто будет наследоваться от какого-то суперкласса, который порождает классы работы с модулями. Также, есть вероятность, что не всегда потребуется обращение к БД для какой-либо функциональности, так значит мы сможем вообще к ней подключаться.
Напишем базовый абстрактный суперкласс, который будет подключаться к БД и иметь метод получения списка некоторых записей. От него наследуем класс работы с пользователями, создадим объект и получим записи.
BaseClass.php
abstract class BaseClass{
protected $dataBase;
protected $moduleName;
function __construct(){
$this->dataBase = new DBConnector;
}
// Наша функция получения списка с заданием количества
function Get($limit=10){
$DB->query(‘SELECT * FROM ‘.$this->moduleName.’ LIMIT ’.$limit);
if($DB->get_num_rows()){
while($user[] = $DB->fetch_row());
return $users;
} else {
return array();
}
}
}
UserClass.php
include ‘BaseClass.php’
class UserClass extend BaseClass{
function __construct (){
parent::construct(‘users’);
}
}
index.php
<h1>Пользователи</h1>
<?
include ‘UserClass.php’;
$users = new UserClass;
$users = $users->get(); //например так
foreach($users as $u) {
echo ‘<p>’.$u.’</p>’;
}
?>
Комментарий: Ещё лучше, хоть и много кода, верно? Дело в том, что эти самые два класса — есть первый шаг к написанию API. Почему так? Дело в том, что запросы через RESTFull API делаются по адресу и очень часто там встречаются названия модулей. В зависимости от адреса можно подключать какой-либо класс (или подключить ClassLoader, благо их сейчас пруд-пруди) и вызывать тот набор функций, который дозволен данному пользователю (можно вставить обработчик прав или ролей, но это уже другая история).
Соль
Суть статьи в том, что рефакторинг практически всегда возможен и зачастую необходим даже в самой безвыходной ситуации. Все любят красивый код, но зачастую времени его писать нет и несомненно это плохая практика.
В данном случае приведены 3 этапа, в результате выполнения которых, система получит разграничение UI с логикой приложения. Это позволит подготовить вашу, перегруженную кривым кодом, систему к возможности написания Unit тестов, написания RESTfull API и к чувству самоудовлетворённости за проделанную работу. Для многих — это, конечно же, может быть очевидно, но когда просят написать стороннее приложение, или предоставить API, иногда приходится краснеть.
PS: Буду рад любым комментариям и, возможно, ссылкам на другие методики и варианты решения проблемы схожего характера.
Автор: svartedauen