Граф зависимостей
2011-02-05 16:16![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Иногда при разборе какого-нибудь проекта с кучей файлов бывает удобно посмотреть на общую картину — какие файлы зависят от каких других файлов. Это может быть надо для того, чтобы решить, в каком порядке их читать, или разрулить цикл в #include
’ах.
Ну или, например, на прошлой работе (да! Я сменил работу. Об этом в другой раз) надо было сделать большую серию рефакторингов, типа «снять модификатор static
с каждого поля класса», при этом, если поле используется в функции, то сначала надо снять static
с функции; а если функция вызывается из static
-функции другого класса, то сначала нужно устроить, чтобы у вызывающей функции был объект, на котором можно было бы вызывать эту функцию, то есть, как правило, снять static
и с неё тоже. Если начать делать, не подумав, то одно потянет за собой второе, второе потянет третье и в результате получится патч, который невозможно review’ить. Если же сразу определить правильный порядок рефакторингов, то патчи получаются маленькие и простые.
Поскольку все доступные инструменты UML-моделирования — полное г***о с точки зрения юзабилити, рисовать будем Graphviz’ом. Заодно он нам сам оптимально разложит всё по слоям и упорядочит так, чтоб было меньше пересечений рёбер.
Однако, у Graphviz’а не слишком удобный синтаксис для подобных задач. Я его постоянно забываю.
digraph "Dependencies"
{
"ControlProgram.cpp" -> "UpdateServerCommander.h"
"UpdateServerCommander.cpp" -> "UpdateServerCommander.h"
"UpdateServerCommander.cpp" -> "Infofile.h"
"UpdateServerCommander.cpp" -> "CdShellTitle.h"
"UpdateServerCommander.h" -> "Infofile.h"
"Infofile.cpp" -> "Infofile.h"
}
В традициях UML, стрелочки рисуем от зависимого к требуемому.
Что здесь плохо? Ну, как минимум, приходится повторять имя вершины для каждой её зависимости. Хочется один раз написать вершину, а потом все её зависимости скопом. Ну и, иногда, наоборот: написать все зависящие, а потом то общее, от чего они зависят.
Вот, например, какой-то такой make
-подобный синтаксис:
ControlProgram.cpp:
UpdateServerCommander.h
UpdateServerCommander.cpp:
UpdateServerCommander.h
Infofile.h
CdShellTitle.h
UpdateServerCommander.h:
Infofile.cpp:
Infofile.h
Да, а ещё Graphviz по умолчанию направляет рёбра сверху вниз. Для графов зависимостей это не очень удобно — то, от чего зависят все, должно быть слева и/или сверху, а то, что зависит от всего — наоборот, справа и/или внизу. Это определяется атрибутом графа rankdir
(ему надо задать значение BT
(bottom to top) или RL
(right to left)). Опыт показывает, что RL
даёт более компактные картинки.
Ну и, для пущего соответствия нотации UML, пусть вершины будут прямоугольными, а рёбра — пунктирными и ломаными (а не сплайнами).
Итого, для конвертации написался вот такой скрипт:
#! /usr/bin/python
import fileinput, re
def dump(dependents, dependencies):
print "".join(["\t\"%s\" -> \"%s\"\n" % (dependent, dependency)
for dependent in dependents
for dependency in dependencies])
dependent_regexp = re.compile("^(.*?):$")
dependency_regexp = re.compile("^\t(.*?)$")
dependents = []
dependencies = []
print "digraph \"Dependencies\""
print "{"
print "\tgraph [rankdir = RL, splines = polyline]" # Place most dependent nodes rightmost
print "\tnode [shape = box]"
print "\tedge [arrowhead = open, style = dashed]" # UML Dependency arrow style
for line in fileinput.input():
m = dependent_regexp.match(line)
if m:
if len(dependencies) > 0:
dump(dependents, dependencies)
dependents = [m.group(1)]
dependencies = []
else:
dependents.append(m.group(1))
continue
m = dependency_regexp.match(line)
if m:
dependencies.append(m.group(1))
continue
dump(dependents, dependencies)
print "}"
Вызывается так:
$ ./dep2dot ControlProgram.dep |dot -Tsvg -oControlProgram.svg $ firefox ./ControlProgram.svg