Статический анализ программ

Статический анализ программ.

1. Метрики для измерения качества программного обеспечения.

Эта статья является одной из серии статей посвященных статическому анализу программного обеспечения. В данной статье делается обзор метрик, которые предложил R.C.Martin в книге [1]. Эти метрики могут использоваться для оценки качества проектов объектно-ориентированного программного обеспечения на основе анализа взаимозависимости подсистем проекта. Проекты, подсистемы в которых имеют сильную зависимость друг от друга, в дальнейшем сложно модифицировать, повторно использовать  и сопровождать. Однако полностью избежать зависимости подсистем в проекте невозможно. Таким образом, можно находить в проекте нежелательные зависимости между подсистемами и удалять их.

Введение.

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

на зависимостях, которые используются подсистемами для взаимодействия  друг с другом, 

на изоляции повторно используемых частей системы от не используемых повторно

на локализации изменений в системе при ее сопровождении.

 

Для измерения таких характеристик предлагается ряд метрик, которые легко применить к проекту. По существу эти метрики есть метрики для измерения качества проекта.

Зависимости.

Что делает проект "твердым", "хрупким" и затрудняет его повторное использование. Это делает взаимозависимость подсистем внутри того проекта.

Проект "тверд", если не может быть легко изменен. Такая жесткость есть следствие того, что отдельное изменение системе с сильными зависимостями приводит к каскаду изменений в зависимых подсистемах. Когда размеры такого каскада изменений не могут быть предсказаны проектировщиками системы или группой сопровождения системы, невозможно оценить стоимость необходимого изменения. Руководители проекта, столкнувшись с такой непредсказуемостью, будут неохотно разрешать вносить в систему изменения. В результате этого проект становится твердым.

Проект "хрупок", если единственное изменение в проекте приводит к неработоспособности  большого числа других частей системы. Часто появляются новые ошибки в тех частях, которые не имеют никаких концептуальных связей с измененной частью. Такая хрупкость сильно уменьшает доверие к проектирующей и сопровождающей организации. Пользователи и менеджеры неспособны предсказать качество системы, которую они создают и используют. Попытка исправить ошибку в системе приводит к появлению еще большего числа новых ошибок.

Проект трудно повторно использовать, если части проекта, которые желательно использовать повторно, сильно зависят от частей проекта, использование которых повторно не требуется. Стоимость отделения частей системы, необходимых для повторного использования, может быть сравнимой с разработкой новой системы заново.

Пример: программа копирования.

Простой пример поможет пояснить вышеизложенное. Рассмотрим простую программу, которая должна копировать символы, напечатанные на клавиатуре, на принтер. Следует принять во внимание то, операционная система в нашем случае не поддерживает независимость устройств. В этом случае мы могли бы предложить структуру программы приведенную ниже:

 

Имеются три модуля. Модуль Copy вызывает функции двух других модулей. Можно представить цикл в модуле Copy. В теле этого модуля происходит вызов модуля ReadKeyboard, который считывает символ клавиатуры и посылает этот символ в модуль WritePrinter.

Два модуля нижнего уровня подходят для повторного использования. Это та разновидность повторного использования, которую мы получаем от библиотек подпрограмм. Однако модуль Copy не может быть многократно использован ни для чего, кроме как для копирования с клавиатуры на принтер. Это недостаток, поскольку "интеллект" системы находится именно в этом модуле. Модуль Copy представляет интересное поведение, которое желательно использовать повторно.

Например, рассмотрите новую программу, которая копирует символы с клавиатуры в файл на диске. Конечно было бы желательно использовать модуль Copy, поскольку он инкапсулирует на высоком уровне желательное для нас поведение. Он "знает", как копировать символы из одного места в другое. К сожалению, модуль Copy зависим от модуля WritePrinter, и поэтому не может повторно использоваться в новом контексте.

Конечно, можно изменить модуль Copy, добавив в него новые функциональные возможности. Например, можно было бы добавить в модуль условный оператор, который делал выбор между модулями WritePrinter и WriteDisk в зависимости от некоторого признака. Однако это лишь добавит к системе новые зависимости. По мере появления новых модулей для копирования зависимость модуля Copy от модулей более низкого уровня будет возрастать. В результате это модуль станет "твердым" и "хрупким".

Инвертирование зависимостей при объектно-ориентированном проектировании.

Заметим, что в приведенной проблеме модуль, описывающий поведение высокого уровня (в примере это модуль Copy), зависит от деталей такого поведения. Если бы мы могли сделать такой модуль менее зависимым от деталей его поведения, такой модуль можно было бы использовать повторно. Можно было бы без ограничений создавать программы, которые используют модуль Copy для копирования с любого устройств ввода на любое устройства вывода. Методы объектно-ориентированного проектирования позволяют нам сделать такое инвертирование зависимостей.

Рассмотрим следующую диаграмму классов:

На диаграмме класс Copy хранит указатели на абстрактные классы Reader и Writer. Можно представить цикл в классе Copy, который получает символы от класса Reader и посылает их классу Writer. Однако класс Copy совершенно не зависит от классов ReadKeyboard и WritePrinter. Таким образом, эти   зависимости были инвертированы. Теперь класс Copy зависит от абстракций. Классы, знающие о деталях чтения и записи, также зависят от тех же абстракций.

Класс Copy теперь готов к повторному использованию. Можно создавать неограниченное число новых разновидностей классов для чтения и записи, и предоставлять их классу Copy. Ни от одного из них класс Copy зависеть не будет. Зависимостей, делающих этот класс "твердым" и "хрупким", уже нет.

Хорошие зависимости

Что делает объектно-ориентированную версию программы копирования устойчивой к ошибкам, сопровождаемой и повторно используемой? Отсутствие в нем лишних зависимостей. Тем не менее, некоторые зависимости в программе существуют, но не влияют на необходимое качество программы. Почему не влияют? Потому что цели этих зависимостей очень стабильны. Их изменение маловероятно.

Рассмотрим природу классов Writer и Reader. На С++ они могли быть представлены следующим образом:

class Writer {public: virtual void Write(char) = 0;};

class Reader {public: virtual char Read()      = 0;};

Эти два класса вряд ли изменятся. Что может их заставить измениться? При нормальном ходе событий эти классы чрезвычайно устойчивы.

Таким образом, существует немного причин, которые вынудили бы изменить класс Copy. Класс Copy есть пример работы принципа "Открыт/закрыт".  Этот класс открыт для расширения, поскольку можно создавать новые версии программ для чтения и записи. Этот класс закрыт для модификации. Поскольку нам не нужно изменять его, что бы добиться такого расширения. Поэтому можно сказать, что хорошая зависимость – это зависимость от чего-то устойчивого. Чем стабильней цель этого отношения зависимости, тем лучше эта зависимость. И наоборот.

Стабильность

Как достичь стабильности? Почему классы Writer и Reader так устойчивы? Рассмотрите снова причины, которые могли заставить их измениться. Они   вообще ни от чего не зависят, поскольку изменение в зависимых от них классах не сможет заставить их изменяться. Назовем это свойство классов независимостью. Независимые классы – те, которые не зависят от чего-нибудь еще.

Другая причина устойчивости классов Writer  и Reader состоит в том, что от них зависят многие другие классы. Например, классы Copy, KeyboardReader и KeyboardWriter. Чем больше существует классов для чтения и записи, тем больше зависимых имеют классы Writer  и Reader. И тем сложнее их менять, поскольку тогда придется менять все зависимые от них классы. Эта очень веская причина, удерживающая нас от изменения таких классов, и заставляющая нас бороться за их стабильность.

Назовем такие классы ответственными. Ответственные классы имеют тенденцию к стабильности, поскольку любое их изменение вызовет большие последствия.

Наиболее устойчивые классы те классы, которые являются и независимыми, и ответственными. Такие классы не имеют никаких причин, чтобы изменяться, и огромное число причин, что бы не изменять их.

Категории классов: единицы повторного использования и реализации

Редко класс может быть повторно использован изолированно от других классов. Класс Copy предоставляет хороший пример этого. Он должен повторно использоваться с абстрактными классами Reader и Writer. Вообще говоря, любой класс имеет группу классов, с которыми он работает в кооперации, и от которых он не может быть легко отделен. Для повторного использования таких классов необходимо повторно использовать всю группу классов. Такая группа классов сильно связна и называется категорией классов.

Категория классов (далее просто категория) – группа сильно связных классов, которые подчиняются следующим трем правилам:

Классы в пределах категории закрыты от любых попыток изменения все вместе. Это означает, что, если один класс должен измениться, все классы в этой категории с большой вероятностью изменятся. Если любой из классов открыт для некоторой разновидности изменений, они все открыты для такой разновидности изменений.

Классы в категории повторно используются только вместе. Они настолько взаимозависимы и не могут быть отделены друг от друга. Таким образом, если делается любая попытка повторного использования одного класса в категории, все другие классы должны повторно использоваться с ним.

Классы в категории разделяют некоторую общую функцию или достигают некоторой общей цели.

Эти три правила внесены в список в порядке их важности. Правилом 3 может пожертвовать за правило 2, правилом 2 может пожертвовать за правило 1.

Если категории должны повторно использоваться, они должны иметь реализацию. Каждая такая реализация категории должна иметь уникальный номер. Если бы это не было сделано, те, кто повторно использует такую реализацию, не смогут положиться на стабильность повторно используемых категорий, поскольку ее авторы могут изменять ее в любое время.

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

Зависимости между категориями – это то, чем мы должны управлять.

Так как категории являются, и единицей реализации, и единицей и повторного использования, это позволяет сделать следующие выводы: зависимости, которыми мы должны управлять – это зависимости между категориями, а не зависимости внутри категории. Внутри категории классы по определению  будут сильно зависимыми. Такая зависимость не нанесет большого вреда, поскольку классы категории используются повторно вместе, и закрыты вместе от одного и того же вида изменений.

Таким образом, мы можем сместить обсуждение зависимости  на один уровень выше, и обсуждать "Независимость", "Ответственность" и "Стабильность" категорий, а не классов. Категории с самой высокой стабильностью – это те категории, которые являются, и независимыми, и ответственными. Зависимости от устойчивых категорий – "хорошие" зависимости.

Метрики зависимостей

Ответственность, независимость и стабильность категории могут быть измерены путем подсчета зависимостей, которые взаимодействуют с этой категорией. Могут быть определены три метрики :

Ca :Центростремительное сцепление (Afferent Couplings).
Количество классов вне этой категории, которые зависят от классов внутри этой категории.

Ce: Центробежное сцепление (Efferent Couplings). 
Количество классов внутри этой категории, которые зависят от классов вне этой категории.

I:    Нестабильность (Instability ): I = Ce / (Ca+Ce). 
Эта метрика имеет диапазон значений [0,1].
I = 0 указывает максимально стабильную категорию.
I = 1 указывает максимально не стабильную категорию.

Не все категории должны быть устойчивы

Если бы все категории в системе были максимально устойчивы, система была бы неизменна. На самом деле, мы хотим часть проекта иметь достаточно гибкой, чтобы оставшаяся часть оставалась неизменной. Как может категория с максимальной устойчивостью (I=0) быть достаточно гибкой, чтобы противостоять изменениям? Ответ должен быть найден в использовании принципа "открыт/закрыт". Руководствуясь этим принципом, можно и желательно создать классы, которые являются достаточно гибкими, чтобы быть расширенными, и не требовать при этом модификации. Какие классы соответствуют этому принципу? Абстрактные классы.

Рассмотрим программу Copy снова. Классы Reader и Writer – это абстрактные классы. Они очень устойчивы, так как они не зависят ни от чего. От этих абстрактных классов зависят все их потомки и класс Copy. Тем не менее, классы Reader и Writer могут быть расширены без их модификации, чтобы иметь дело со многими видами устройств ввода-вывода.

Таким образом, если категория должна быть устойчивой, она должна состоять из абстрактных классов, чтобы ее можно было затем расширить. Устойчивые категории те, которые являются расширяемыми, гибкими и не ограничивают проект.

Если устойчивые категории должны быть очень абстрактными, то можно сделать вывод, что нестабильные категории должны быть очень конкретны. На самом деле это стоит обсудить подробнее.

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

Мы можем определять метрику, которая измеряет "абстрактность" категории следующим образом:

A: Абстрактность (Abstractness): A = nA / nAll.
nA     – количество_абстрактных_классов_в_категории. 
nAll   – oбщее_количество_классов_в_категории.
Значения этой метрики меняются в диапазоне [0,1].
0 – категория полностью конкретна, 
1 – категория полностью абстрактна.

Главная последовательность

Теперь мы в состоянии определить отношение между стабильностью (I) и абстрактностью (A). Мы можем создавать граф с (A) на вертикальной оси и  (I) на горизонтальной оси. Если мы начертим на этом графике два вида "хороших"  категорий, то категории, которые являются максимально устойчивыми и абстрактными, окажутся в верхнем левом углу в (0,1). Категории, которые являются максимально нестабильными и конкретными – в нижнем правом углу (1,0).

Но не все категории окажутся в одной из этих двух позиций. Категории имеют степени абстракции и стабильности. Например, очень часто один абстрактный класс порожден от другого абстрактного класса. Порожденный класс – абстракция, которая имеет зависимость из-за его порождения. Поэтому, хотя он имеет   максимальную абстракцию, он не будет максимально стабилен. Эта его зависимость уменьшит его стабильность.

Рассмотрим категорию с A=0 и I=0. Это – очень стабильная и конкретная категория. Такая категория не желательна из-за своей "твердости". Она не может быть расширена, потому что не абстрактна. И очень трудно изменяема из-за своей стабильности.

Рассмотрим категорию с A=1 и I=1. Эта категория также нежелательна (и даже невозможна), потому что она – максимально абстрактна, и, тем не менее, ее никто не использует. Она также твердая, потому что такие абстракции невозможно расширить.

Что можно сказать относительно категории A =.5 и I =.5? Эта категория частично расширяема, потому что она частично абстрактна. Кроме того, она частично стабильна, поскольку расширения не являются максимально неустойчивыми. Такая категория выглядит "сбалансированной". Ее стабильность находится в балансе с ее абстрактностью.

Рассмотрим снова граф (A-I), показанный ниже на рисунке. Мы можем нарисовать линию из (0,1) в (1,0). Эта линия (A + I = 1)представляет категории, чья абстрактность сбалансирована с их стабильностью. Из-за сходства с графом, используемым в астрономии, эту линию можно назвать Главная последовательность.

Категория, которая расположена на главной последовательности, не "слишком абстрактна" для ее стабильности, ни – "слишком нестабильна" для ее абстрактности.

Она имеет "правильное" количество конкретных и абстрактных классов пропорционально ее центробежным и центростремительным зависимостям. Ясно, наиболее желательные положения для категории на одном из концов главной последовательности. Однако, судя по опыту, только приблизительно половина категорий проекта может иметь такие идеальные характеристики. Оставшиеся категории имеют лучшие характеристики,  если они или расположены близко к главной последовательности, или расположены прямо на ней.

Расстояние от главной последовательности

Теперь можно ввести последние метрики. Если желательно для категории быть на главной последовательности или быть близко к главной последовательности, мы можем создать метрику оценивающую близость категории к этому идеалу.

D: Расстояние (Distance): D = | (A+I-1) / sqrt(2) |.
Это перпендикулярное расстояние категории от главной последовательности. Диапазон значений этой метрики [0, ~0.707].

Dn: Нормализованное расстояние.
Можно нормализовать метрику D, поместив ее значения между [0,1], либо используя более простую форму | (A+I-1) |.

С помощью данной метрики категории проекта могут быть проанализированы на их полное соответствие главной последовательности. Метрика  D может быть рассчитана для каждой категории. Любая категория, которая имеет значение D не близкое к нулю, может быть повторно исследована и реструктурирована.

Возможен  также и статистический анализ проекта. Можно вычислять среднее величину и отклонения всех D метрик в пределах проекта. Отклонение может использоваться для того, чтобы установить диапазон, который позволит идентифицировать категории  "исключительные" по сравнению со всеми другими категориями.

Заключение и предостережение

Метрика, описанная здесь, измеряет соответствие проекта "хорошим" образцам  зависимости и абстракции. Опыт показал, что некоторые зависимости являются хорошими, а другие плохими. Приведенные образцы отражают тот опыт. Однако, метрика – не бог, а просто измерение на соответствие некоторому стандарту. Для некоторых приложений стандарт будет хорош, для других плох.

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