Статьи

Временная корреляция в репозиториях Git

Майкл Фезерс представил свою повторяющуюся идею выяснить, какие элементы дизайна меняются вместе: его цель — выяснить, какие классы или методы действительно связаны, путем анализа эмпирических данных вместо статического анализа. Поскольку он не публиковал код, я пытаюсь повторить анализ с помощью независимого от языка сценария.

Другой подход к обнаружению зависимостей

Данные, которые он добывает для этой информации, фиксируются в хранилище исходного кода; Этот подход отличается от статического анализа. В последнем сценарии анализ состоит в использовании отражения для аннотирования того, какой класс расширяет или реализует другой; в результате два элемента связаны или не связаны просто в зависимости от их деклараций, без учета того, как часто они меняются или когда переход в один из них превращается в другой.

На мой взгляд, другая проблема со статическим анализом состоит в том, что в очень динамичных языках нет даже отношений, которые можно проанализировать . В 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);