Исходный код
function query_safe()
{
$time_before = $this->get_real_time();
if (!$this->connected) $this->connect(DBUSER, DBPASS, DBNAME, DBHOST);
$args = func_get_args();
$tmpl =& $args[0];
$tmpl = str_replace("%", "%%", $tmpl);
$tmpl = str_replace("?", "%s", $tmpl);
foreach ($argsas$i=>$v) {
if (!$i) continue;
if (is_int($v)) continue;
$args[$i] = "'".mysql_escape_string($v)."'";
}
for ($i=$c=count($args)-1; $i<$c+20; $i++)
$args[$i+1] = "UNKNOWN_SQL_COMMAND_$i";
$sql = call_user_func_array("sprintf", $args);
if (!($this->query_id = mysql_query($sql, $this->db_id) )) {
$this->mysql_error = mysql_error();
$this->mysql_error_num = mysql_errno();
if ($show_error) {
$this->display_error($this->mysql_error, $this->mysql_error_num, $query);
}
}
$this->MySQL_time_taken += $this->get_real_time() - $time_before;
$this->query_num ++;
return $this->query_id;
}
Что не так в исходном коде
Этот метод довольно длинный, одно это уже настораживает.
В самом начале метода я вижу подключение к базе данных, а потом какие-то пока неясные манипуляции со строками — это второй звоночек. Зачем подключаться к базе данных, если ты ещё не собираешься отправлять запрос?
Беглый осмотр оставшегося кода показывает, что здесь достигаются две независимые друг от друга цели: сформировать SQL-запрос из параметризованной строки и собственно выполнить готовый SQL-запрос. Связаны они только тем, что выход одной является входом другой, внутренние же особенности никак друг на друга не влияют.
Для меня представляется совершенно естественным, что у меня будут два отдельных метода: для получения окончательного запроса из шаблона с параметрами, и для выполнения полностью готового запроса. Первый может легко понадобиться, например, при отладке, а второй — когда у меня уже есть полностью готовый запрос.
Углубляясь в код подготовки SQL-запроса можно видеть, что там алгоритм тоже работает в несколько этапов:
- подготовка шаблонной строки,
- подготовка параметров,
- обработка ошибочной ситуации с параметрами,
- собственно подстановка параметров.
Аналогичная картина и с выполнением запроса:
- подготовка к выполнению запроса,
- собственно выполнение запроса,
- обработка ошибок,
- завершающие действия.
Все эти подробности нет большого смысла делать доступными извне, но есть смысл всё равно раскидать их по отдельным методам, чтобы изолировать друг от друга содержащиеся в них знания.
Вариант рефакторинга исходного кода
function query_safe()
{
$raw_sql = call_user_func_array(array($this, 'make_sql_safe'), func_get_args());
return $this->query_raw_sql($raw_sql);
}
function make_sql_safe()
{
$sql_params = func_get_args();
$sql_template = $this->prepare_sql_template(array_shift($sql_params));
$sql_params = $this->prepare_sql_params($sql_params);
$raw_sql = vsprintf($sql_template, $sql_params);
return $raw_sql;
}
function query_raw_sql($raw_sql)
{
$this->before_query();
if (!($this->query_id = mysql_query($raw_sql, $this->db_id) )) {
$this->handle_mysql_error($raw_sql);
}
$this->after_query();
return $this->query_id;
}
const SQL_TEMPLATE_REPLACES = [
'%' => '%%',
'?' => '%s',
]
private function prepare_sql_template($sql_template)
{
return strtr($sql_template, self::SQL_TEMPLATE_REPLACES);
}
private function prepare_sql_params($sql_params)
{
foreach ($sql_paramsas$i => $v) {
if (!$i) continue;
if (is_int($v)) continue;
$sql_params[$i] = "'".mysql_escape_string($v)."'";
}
for ($i = 0; $i < 20; $i++)
$sql_params[] = "MISSING_PARAM";
return $sql_params;
}
private function before_query()
{
if (!$this->connected) $this->connect(DBUSER, DBPASS, DBNAME, DBHOST);
$this->query_started_at = $this->get_real_time();
}
private function after_query()
{
$query_time_taken = $this->get_real_time() - $this->query_started_at;
$this->MySQL_time_taken += $query_time_taken;
$this->query_num ++;
}
private function handle_mysql_error($raw_sql)
{
$this->mysql_error = mysql_error();
$this->mysql_error_num = mysql_errno();
if ($this->show_error) {
$this->display_error($this->mysql_error, $this->mysql_error_num, $raw_sql);
}
}
Мне совершенно не нравится выбранный в исходном коде способ работы с пропущенными параметрами, однако мне лень его улучшать, так как я всё равно не знаю контекста, в котором происходит обработка ошибок приложения. Поэтому я просто максимально его упростил, выбросив лишние на мой вгляд детали.
Теория
- Рефакторинг «извлечение функции»
- Принцип единственного знания
- Принцип единственного уровня