Майкл Фезерс представил свою повторяющуюся идею выяснить, какие элементы дизайна меняются вместе: его цель — выяснить, какие классы или методы действительно связаны, путем анализа эмпирических данных вместо статического анализа. Поскольку он не публиковал код, я пытаюсь повторить анализ с помощью независимого от языка сценария.
Другой подход к обнаружению зависимостей
Данные, которые он добывает для этой информации, фиксируются в хранилище исходного кода; Этот подход отличается от статического анализа. В последнем сценарии анализ состоит в использовании отражения для аннотирования того, какой класс расширяет или реализует другой; в результате два элемента связаны или не связаны просто в зависимости от их деклараций, без учета того, как часто они меняются или когда переход в один из них превращается в другой.
На мой взгляд, другая проблема со статическим анализом состоит в том, что в очень динамичных языках нет даже отношений, которые можно проанализировать . В PHP у нас все еще есть явные интерфейсы, но в Ruby или JavaScript таких вещей нет.
Как это работает
Анализ Майкла Фезерса был проведен на уровне метода. Основная идея заключается в том, чтобы рассматривать коммиты как источник данных об изменениях в вашей кодовой базе, а не только в самом коде.
Предположение, которое должно быть правильным в большинстве рабочих процессов, заключается в том, что коммит в коренной операции коррелирует множество классов и методов, которые составляют новую функциональность или рефакторинг. Поэтому каждый коммит, который вы находите, когда классы A и B модифицируются вместе, добавляет оценку отношениям между ними.
В своем примере он работал с классами и методами. Я собрал более быстрый пример, основанный на файлах, поскольку в нашем стандарте один класс всегда соответствует одному файлу.
Если мы сможем определить, какие файлы всегда изменяются вместе , мы можем обнаружить некоторую скрытую связь на объективной основе. Например, меня может заинтересовать код, который изменяется вместе между слоями или между удаленной частью кодовой базы. Сам Перья пишет об информативных данных о связывании доменных объектов.
Моя реализация имеет значение
Основным элементом, который рассматривается в моем коде, является пара файлов (классы чтения), представленные в виде двух имен файлов. Фокус на файле также делает этот анализ независимым от языка.
Каждый коммит указывает, что файлы A, B, C, D … были изменены вместе. Поэтому для каждого коммита я добавляю определенное количество очков к парам A, B ; А, С ; A, D ; B, C ; B, D ; С, D . Я делаю анализ по крайней мере сто (200) коммитов для статистической значимости. Это занимает много времени.
Оценка для каждого коммита нормализуется количеством файлов в коммите:
- Всего двумя файлами пара получает прибавку к 0,5.
- с N файлами каждая возможная пара получает прибавление 1 / N. например, N = 100 приводит к добавлению каждой пары 0,01. Таким образом, большие коммиты, когда вы просто изменяете уведомление об авторском праве или стандарт кодирования для сотен файлов, не имеют большого значения (не так ли?).
Пример
Я запускаю код над основной веткой Doctrine 2 на Github ( 24042863acbabdcd0fa1432135a9836467f3bce7 на момент написания этой статьи). Это были результаты (ограничены первыми 10 парами).
array(10) {
["tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php|lib/Doctrine/ORM/Query/SqlWalker.php"]=>
float(31.318715901959)
["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php"]=>
float(29.233642885566)
["tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php|lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php"]=>
float(27.059301324524)
["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Query/Parser.php"]=>
float(25.077708602949)
["lib/vendor/doctrine-dbal|lib/Doctrine/ORM/Query/SqlWalker.php"]=>
float(22.883601306557)
["lib/Doctrine/ORM/UnitOfWork.php|lib/Doctrine/ORM/Query/SqlWalker.php"]=>
float(22.658756168829)
["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php"]=>
float(22.625098398742)
["tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php|lib/Doctrine/ORM/Query/Parser.php"]=>
float(21.878131735017)
["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Persisters/BasicEntityPersister.php"]=>
float(21.422968340511)
["tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php|lib/Doctrine/ORM/UnitOfWork.php"]=>
float(20.691940478915)
}
1-й, 3-й, 8-й и 10-й экземпляры являются примером связи между тестовым и рабочим кодом, что и следовало ожидать. Если тест действительно не связан с рабочим кодом, я не буду говорить, что это указывает на проблему. SelectSqlGenerationTest — очень важный функциональный тест, который проверяет, что операции, включающие запросы SELECT, выполняются корректно: возможно, новые тестовые примеры приводят к добавлению производственного кода.
Если мы удалим связанные с тестом пары, мы получим:
["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php"]=> float(29.233642885566) ["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Query/Parser.php"]=> float(25.077708602949) ["lib/vendor/doctrine-dbal|lib/Doctrine/ORM/Query/SqlWalker.php"]=> float(22.883601306557) ["lib/Doctrine/ORM/UnitOfWork.php|lib/Doctrine/ORM/Query/SqlWalker.php"]=> float(22.658756168829) ["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php"]=> float(22.625098398742) ["lib/Doctrine/ORM/Query/SqlWalker.php|lib/Doctrine/ORM/Persisters/BasicEntityPersister.php"]=> float(21.422968340511)
Query / SqlWalker — это класс, который всегда присутствует в этих отношениях с другими классами. Его ответственность — большая: преобразование анализируемого дерева DQL (языка для запросов объектов) в запросы SQL.
На самом деле быстрый тест на длину классов возвращает:
[09:21:25][giorgio@Desmond:~/code/doctrine2]$ wc `find . -name '*.php'` | sort -n | tail -n 5 1942 6826 65254 ./lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php 2062 6731 80818 ./lib/Doctrine/ORM/Query/SqlWalker.php 2410 8308 95066 ./lib/Doctrine/ORM/UnitOfWork.php 3004 7535 101628 ./lib/Doctrine/ORM/Query/Parser.php 76445 223518 2587575 total
Таким образом, SqlWalker — один из самых больших классов в кодовой базе, и он очень часто меняется вместе с другими: если цель состоит в том, чтобы получить максимальную отдачу от моего бакса, это будет моей отправной точкой для рефакторинга. Но теперь я должен вернуться к коду моей компании, чтобы выяснить, где болевые точки. ?
Я забыл: вот мой быстро взломанный код для получения этой статистики.
<?php
class IncidenceMatrix
{
private $matrix = array();
public function addHit($element, $otherElement, $score)
{
if ($element > $otherElement) {
$first = $element;
$second = $otherElement;
} else {
$first = $otherElement;
$second = $element;
}
if ($first == '' or $second == '') {
return;
}
$key = $first . '|' . $second;
if (!isset($this->hash[$key])) {
$this->hash[$key] = 0;
}
$this->hash[$key] += $score;
}
public function getTopHits($howMany)
{
$hash = $this->hash;
arsort($hash);
return array_slice($hash, 0, $howMany);
}
}
$commitListCommand = 'git log --oneline | head -n 200';
exec($commitListCommand, $logOfCommits);
$incidenceMatrix = new IncidenceMatrix();
foreach ($logOfCommits as $commitLog) {
list ($commit, ) = explode(' ', $commitLog);
$filesListCommand = "git show --pretty='format:' --name-only $commit";
exec($filesListCommand, $fileList);
$commitScore = 1 / count($fileList);
for ($i = 0; $i < count($fileList); $i++) {
for ($j = $i + 1; $j < count($fileList); $j++) {
$incidenceMatrix->addHit($fileList[$i], $fileList[$j], $commitScore);
}
}
}
$topHits = $incidenceMatrix->getTopHits(10);
var_dump($topHits);