Исходный код
function SetFileAccessPermission($path, $arPermissions, $bOverWrite=true)
{
CMain::InitPathVars($site, $path);
$DOC_ROOT = CSite::GetSiteDocRoot($site);
if(strlen($path) <= 0)
$path="/";
if(($p = bxstrrpos($path, "/"))!==false)
{
$path_file = substr($path, $p+1);
$path_dir = substr($path, 0, $p);
}
elsereturnfalse;
if($path_file=="" && $path_dir=="")
$path_file = "/";
$PERM = Array();
if(file_exists($DOC_ROOT.$path_dir."/.access.php"))
@include($DOC_ROOT.$path_dir."/.access.php");
$FILE_PERM = $PERM[$path_file];
if(!is_array($FILE_PERM))
$FILE_PERM=Array();
if(!$bOverWrite && count($FILE_PERM)>0)
returntrue;
$bDiff = false;
$str="<?\n";
foreach($arPermissionsas$group=>$perm)
{
if(strlen($perm) > 0)
$str.="\$PERM[\"".$path_file."\"][\"".$group."\"]=\"".str_replace("\"", "\\\"", $perm)."\";\n";
if(!$bDiff && $FILE_PERM[$group]!=$perm)
$bDiff=true;
}
foreach($PERMas$file=>$arPerm)
{
if(strval($file) !==$path_file)
foreach($arPermas$group=>$perm)
$str.="\$PERM[\"".$file."\"][\"".$group."\"]=\"".str_replace("\"", "\\\"", $perm)."\";\n";
}
if(!$bDiff)
{
foreach($FILE_PERMas$group=>$perm)
if($arPermissions[$group]!=$perm)
{
$bDiff==true;
break;
}
}
$str.="?".">";
$this->SaveFileContent($DOC_ROOT.$path_dir."/.access.php", $str);
$GLOBALS["CACHE_MANAGER"]->CleanDir("menu");
unset($this->FILE_PERMISSION_CACHE[$site."|".$path_dir."/.access.php"]);
if($bDiff)
{
$db_events = GetModuleEvents("main", "OnChangePermissions");
while($arEvent = $db_events->Fetch())
ExecuteModuleEvent($arEvent, Array($site, $path), $arPermissions);
}
returntrue;
}
Как можно улучшить исходный код
При виде столь длинной функции у каждого программиста должна рефлекторно возникать мысль: «О, да здесь много ответственностей, сейчас я их поделю!».
Для выполнения рефакторинга воспользуюсь методом составления плана. После напряжённого анализа кода я выявил в коде функции следующие ответственности:
- сформировать путь к файлу с закешированными правами доступа
$DOC_ROOT.$path_dir."/.access.php"
; - сформировать путь к файлу, права доступа которого нужно закешировать
$path_file
; - проверить наличие прав доступа к нужному файлу
!$bOverWrite && count($FILE_PERM)>0
; - сформировать новый набор права доступа;
- обнаружить наличие расхождений между новыми и старыми правами;
- записать новые права в соответствующий файл;
- сбросить соответствующие кеши;
- оповестить подписчиков события об изменении прав доступа.
Цель рефакторинга «извлечение функции» — не просто распихать код, а скрыть реализацию. Одна из глобальных целей этого мероприятия — обеспечить возможность максимально безболезненного изменения реализаций.
Например, если я захочу изменить способ кеширования прав доступа с файла access.php
на какой-то другой вариант, мне придётся жёстко переписать одну часть SetFileAccessPermission
, связанную с файлом, не сломав другую, связанную с собственно правами доступа. И потом ещё переписать все места, где случаются обращения к этому файлу — а они случаются.
Мой метод — собрать все непосредственные обращения к этому файлу в одну кучу и спрятать их за интерфейсом в терминах кеширования прав доступа. Я хочу, чтобы везде, где это понадобится, я мог бы просто вызывать функции кеширования, не задумываясь о реальном месте хранения данных.
Ещё одна важная мысль, которую хочу донести: нельзя смешивать ответственности на том основании, что в их реализации есть похожие участки кода. Например, здесь плотно смешаны алгоритмы формирования пути к файлу с закешированными правами и пути к файлу, права доступа к которому нужно закешировать. Тот факт, что оба проходят черех похожие манипуляции с $path
нисколько их не сближает — это всё равно две совершенно разные по смыслу величины, а значит, должны рассчитываться двумя независимыми функциями.
Другая пара смешанных ответственностей: формирование файла с правами доступа и признака изменения прав доступа ($str
и $bDiff
) — они тоже должны вычисляться раздельно. Мой рецепт: сначала сформировать массив с правами доступа, и только потом отдельно формировать из него файл и отдельно определять наличие изменений. Здесь общим участком является цикл по массиву $arPermissions
— но опять же, это не основание для смешивания.
Вариант рефакторинга исходного кода
function SetFileAccessPermission($filePath, $arNewFilePermissions, $bOverWrite=true)
{
$permissionSubject = GetPermissionSubject($filePath);
if ($permissionSubject === null)
returnfalse;
$arFullPermissions = ReadPermissions($filePath);
$arOldFilePermissions = isset($arFullPermissions[$permissionSubject]) ? $arFullPermissions[$permissionSubject] : array();
if (!empty($arOldFilePermissions) && !$Overwrite)
returntrue;
$arFullPermissions[$permissionSubject] = $arNewFilePermissions;
WritePermissions($filePath, $arFullPermissions);
if (!PermissionEqual($arNewFilePermissions, $arOldFilePermissions)) {
NotifyChangePermissions($filePath, $arNewFilePermissions);
}
}
function ReadPermissions($filePath)
{
$permissionsFilePath = GetPermissionFilePath($filePath);
$PERM = Array();
if(file_exists($permissionsFilePath))
@include($permissionsFilePath);
return$PERM;
}
function WritePermissions($filePath, $arFullPermissions)
{
$permissionsFilePath = GetPermissionFilePath($filePath);
$permissionsStr = RenderPermissions($arFullPermissions);
$this->SaveFileContent($permissionsFilePath, $permissionsStr);
$GLOBALS["CACHE_MANAGER"]->CleanDir("menu");
unset($this->FILE_PERMISSION_CACHE[]);
}
function RenderPermissions($permissions)
{
$str="<?\n";
foreach($permissionsas$file=>$arPerm)
{
foreach($arPermas$group=>$perm)
$str.="\$PERM[\"".$file."\"][\"".$group."\"]=\"".str_replace("\"", "\\\"", $perm)."\";\n";
}
return$str;
}
function GetPermissionFilePath($filePath)
{
CMain::InitPathVars($site, $filePath);
list($path_dir, $path_file) = SplitPath($filePath);
$DOC_ROOT = CSite::GetSiteDocRoot($site);
return$DOC_ROOT.$path_dir."/.access.php";
}
function GetPermissionCacheKey($filePath)
{
CMain::InitPathVars($site, $filePath);
list($path_dir, $path_file) = SplitPath($filePath);
return$site."|".$path_dir."/.access.php";
}
function GetPermissionSubject($path)
{
CMain::InitPathVars($site, $path);
list($path_dir, $path_file) = SplitPath($path);
if ($path_dir === null)
returnnull;
return$path_file=="" && $path_dir=="" ? "/" : $path_file;
}
function SplitPath($path)
{
if(($p = bxstrrpos($path, "/"))!==false)
returnarray(substr($path, 0, $p), substr($path, $p+1));
elsereturnarray(null, $path);
}
function PermissionEqual($arNewFilePermissions, $arOldFilePermissions)
{
foreach($arNewFilePermissionsas$group=>$perm)
if($arOldFilePermissions[$group]!=$perm)
returnfalse;
foreach($arOldFilePermissionsas$group=>$perm)
if($arNewFilePermissions[$group]!=$perm)
returnfalse;
returntrue;
}
function NotifyChangePermissions($filePath, $arNewFilePermissions)
{
CMain::InitPathVars($site, $filePath);
$db_events = GetModuleEvents("main", "OnChangePermissions");
while($arEvent = $db_events->Fetch())
ExecuteModuleEvent($arEvent, Array($site, $filePath), $arNewFilePermissions);
}
Как нетрудно видеть, отрефакторенный код не соответствует один-в-один первоначально составленному плану: какие-то пункты пришлось разбить на подпункты.
Что ещё можно улучшить
Получившаяся версия функции SetFileAccessPermission
всё ещё не идеальна. Она знает слишком много подробностей о том, как устроено кеширование прав доступа, хотя её предназначение — работать с правами доступа к одиночному файлу. Попробую избавить её от лишних знаний.
function SetFileAccessPermission($filePath, $arNewFilePermissions, $bOverWrite=true)
{
$arOldFilePermissions = GetFilePermissions($filePath);
if (!empty($arOldFilePermissions) && !$Overwrite)
returntrue;
SaveFilePermissions($filePath, $arNewFilePermissions);
if (!PermissionEqual($arNewFilePermissions, $arOldFilePermissions)) {
NotifyChangePermissions($filePath, $arNewFilePermissions);
}
}
Вот теперь ничего лишнего, вся работа идёт на одном уровне без погружения в ненужные детали.
Предвижу возражения в том плане, что для реализации функций GetFilePermissions
и SaveFilePermissions
придётся дважды читать файл с диска. Мой ответ: «проблемы индейцев шерифа не волнуют». Высокоуровневая функция не должна заботиться о том, как будут реализованы вызываемые ей низкоуровневые функции. Эта забота и есть та самая преждевременная оптимизация, о вреде которой часто упоминают.
В первую очередь код каждого метода должен быть реализован максимально ясно — то есть кратко. Если в результате код в целом показывает неоптимальное расходование ресурсов, неоптимальности должны по возможности оптимизироваться точечно на своих уровнях, без «поднятия» оптимизаций на вышестоящие уровни.
При работе «неоптимального» кода в реальных условиях может оказаться, что ясность алгоритмов ценнее тех незначительных накладных расходов, привносимых не всегда «оптимальными» реализациями. Ясность кода — важное условие высокой скорости разработки, и пока что высокая скорость выгоднее для бизнеса, чем дополнительное ядро процессора на сервере.
Реализацию GetFilePermissions
и SaveFilePermissions
оставляю заинтересованному читателю в качестве упражнения.
Теория
- Метод составления плана
- Принцип единственного знания
- Принцип единственного уровня
- Утечка знания
- Рефакторинг «извлечение функции»