Майкл Фезерс представил свою повторяющуюся идею выяснить, какие элементы дизайна меняются вместе: его цель — выяснить, какие классы или методы действительно связаны, путем анализа эмпирических данных вместо статического анализа. Поскольку он не публиковал код, я пытаюсь повторить анализ с помощью независимого от языка сценария.
Другой подход к обнаружению зависимостей
Данные, которые он добывает для этой информации, фиксируются в хранилище исходного кода; Этот подход отличается от статического анализа. В последнем сценарии анализ состоит в использовании отражения для аннотирования того, какой класс расширяет или реализует другой; в результате два элемента связаны или не связаны просто в зависимости от их деклараций, без учета того, как часто они меняются или когда переход в один из них превращается в другой.
На мой взгляд, другая проблема со статическим анализом состоит в том, что в очень динамичных языках нет даже отношений, которые можно проанализировать . В 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);