Из гадкого утёнка в лебедя, или как исправить криворукий код

в 10:28, , рубрики: mvc, php, refactoring, классы, Программирование, Проектирование и рефакторинг, рефакторинг, метки: , , , ,

image

О чём это?

Многие начинают писать проект для работы с 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

* - обязательные к заполнению поля