В гонитві за якістю коду: Безпечне програмування за допомогою АОП (исходники), Різне, Програмування, статті

Хоча захисне програмування надійно гарантує стан вхідних даних методу, його застосування до цілих серій методів вимагає повторюваних операцій. У статті цього місяця Ендрю Гловер показує простий спосіб додавання до коду багаторазово використовуваних перевірочних обмежень за допомогою АОП, контрактних специфікацій (design by contract) і корисної бібліотеки під назвою OVal


Основний недолік попереднього тестування ПО полягає в тому, що переважна більшість тестів представляють “безхмарні” сценарії. В таких ситуаціях дефекти виявляються рідко – проблеми, як правило, виникають саме в крайніх випадках.


Що таке крайній випадок? Це ситуація, коли, наприклад, передається значення null в метод, не запрограмований для обробки значень null. Багато розробники тестують такі сценарії, оскільки не бачать в них сенсу. Але є сенс чи ні, таке трапляється, видається NullPointerException, І програма вилітає.


У цьому місяці автор представляє багатогранний підхід до усунення труднопредськазуємих дефектів в коді. Дізнатися, що дає об’єднання принципів захисного програмування, контрактній специфікації і зручною загального середовища перевірки OVal.


Виявлення ворога


Код в лістингу 1 будує ієрархію класів для об’єкта Class (Пропускаючи java.lang.Object, Його в кінцевому рахунку розширюють все). Якщо подивитися уважно, можна помітити затаївся потенційний дефект, обумовлений зробленими в методі припущеннями щодо значень об’єктів.


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





public static Hierarchy buildHierarchy(Class clzz){
Hierarchy hier = new Hierarchy();
hier.setBaseClass(clzz);
Class superclass = clzz.getSuperclass();
if(superclass != null && superclass.getName().equals(“java.lang.Object”)){
return hier;
}else{
while((clzz.getSuperclass() != null) &&
(!clzz.getSuperclass().getName().equals(“java.lang.Object”))){
clzz = clzz.getSuperclass();
hier.addClass(clzz);
}
return hier;
}
}

При написанні цього коду я не помітив цього дефекту, але оскільки я – фанатик попереднього тестування, то написав стандартний тест за допомогою TestNG. Більше того, я використовував зручну функцію DataProvider TestNG, що дозволяє створити типовий тест і потім змінювати його параметри за допомогою іншого методу. Тест, наведений у лістингу 2, двічі пройшов успішно! Все йде нормально, так?


Лістинг 2. Тест TestNG для перевірки двох значень





import java.util.Vector;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class BuildHierarchyTest {

@DataProvider(name = “class-hierarchies”)
public Object[][] dataValues(){
return new Object[][]{
{Vector.class, new String[] {“java.util.AbstractList”,
“java.util.AbstractCollection”}},
{String.class, new String[] {}}
};
}
@Test(dataProvider = “class-hierarchies”})
public void verifyHierarchies(Class clzz, String[] names) throws Exception{
Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz);
assertEquals(hier.getHierarchyClassNames(), names, “values were not equal”);
}
}


Дефект як і раніше не виявлений, але щось в коді мені не подобається. Що якщо хтось ненароком передасть значення null в параметр Class? Виклик clzz.getSuperclass() в четвертому рядку лістингу 1 викличе NullPointerException, Чи не так?


Перевірити цю теорію досить просто, не потрібно навіть починати з нуля. Я просто додаю {null, null} до багатовимірного масиву Object в методі dataValues вихідного тесту BuildHierarchyTestлістингу 1) І запускаю тестування ще раз. Зрозуміло, я отримую неприємний виняток NullPointerException, Представлене на малюнку 1:


Рисунок 1. Жахливий NullPointerException
Жахливий NullPointerException

Тут можна побачити малюнок повністю.







 



Як щодо статичного аналізу?

Інструменти статичного аналізу, подібні FindBugs, досліджують класи та JAR-файли в пошуках потенційних проблем, порівнюючи байт-код із списком шаблонів помилок. Запуск FindBugs для коду даного прикладу не виявив NullPointerException, Показаного в лістингу 1.


Захисне програмування


Після того як проблема виявлена, наступний крок полягає у виборі стратегії по її усуненню. Проблема полягає в неможливості контролювати тип даних, що вводяться, які отримує даний метод. Для вирішення проблем такого типу розробники часто застосовують методи захисного програмування, призначені для попереджувального виявлення потенційних помилок.


Класична стратегія захисного програмування, призначена для обробки невизначеностей – це перевірка об’єктів. Відповідно я додав перевірку рівності clzz значенням null (Див. лістинг 3). Якщо значення дорівнює null, То викликається метод RuntimeException для попередження всіх зацікавлених сторін про потенційну проблему.


Лістинг 3. Додаємо перевірку на рівність null





public static Hierarchy buildHierarchy(Class clzz){

if(clzz == null){
throw new RuntimeException(“Class parameter can not be null”);
}
Hierarchy hier = new Hierarchy();
hier.setBaseClass(clzz);
Class superclass = clzz.getSuperclass();
if(superclass != null && superclass.getName().equals(“java.lang.Object”)){
return hier;
}else{
while((clzz.getSuperclass() != null) &&
(!clzz.getSuperclass().getName().equals(“java.lang.Object”))){
clzz = clzz.getSuperclass();
hier.addClass(clzz);
}
return hier;
}
}


Природно, я також написав швидкий тест, щоб упевнитися, що моя перевірка дійсно запобігає NullPointerException (Див. лістинг 4):


Лістинг 4. Підтвердження дієвості перевірки на null





@Test(expectedExceptions={RuntimeException.class})
public void verifyHierarchyNull() throws Exception{
Class clzz = null;
HierarchyBuilder.buildHierarchy(null);
}

В даному випадку захисне програмування, схоже, виконує свою роль. Але використання виключно даної стратегії має свої витрати.


Витрати захисного програмування







 



Як щодо операторів контролю?

У лістингу 3 для перевірки значення clzz використовується умовний оператор, хоча також можна було б використати assert. При використанні операторів assert не потрібно задавати умова або оператор виключення. Усі завдання захисного програмування повністю реалізуються на рівні JVM, якщо ми включили оператори контролю.


Хоча захисне програмування надійно гарантує стан вхідних даних методу, його застосування до цілих серій методів вимагає повторюваних операцій. Знайомі з аспектно-орієнтованим програмуванням, або АОП, дізнаються в цьому наскрізну задачу (crosscutting concern), що означає, що методи захисного програмування простягаються горизонтально по всьому масиву коду. Ця семантика використовується в багатьох різних об’єктах, але з чисто об’єктно-орієнтованої точки зору вона не має нічого спільного з самими об’єктами.


Більше того, концепція наскрізних задач починає просочуватися в поняття контрактних специфікацій програмних компонентів (design by contract, DBC). DBC – це технологія, яка покликана гарантувати, що всі компоненти в системі виконують саме те, що вони повинні виконувати, за рахунок явної вказівки в інтерфейсі кожного компонента його необхідної функціональності і очікувань клієнта. Мовою DBC необхідна функціональність компонента називається постусловіем і є, по суті, “зобов’язаннями” компонента, а очікування клієнта збирацько називаються передумовою. Більш того, в термінах чистого DBC клас, діє відповідно до правил DBC, має “контракт” з навколишнім світом щодо підтримання їм внутрішньої узгодженості, відомий як інваріант класу.


Контрактні специфікації


Поняття DBC я ввів деякий час тому в статті про програмування на Nice, об’єктно-орієнтованому, сумісному з JRE мовою програмування, розробленому з акцентом на модульність, виразні можливості і безпеку. Цікаво, що в Nice використовуються функціональні методи розробки, включаючи деякі методи аспектно-орієнтованого програмування. Серед іншого, функціональна розробка дозволяє задавати передумови і післяумови для методів.


Хоча мова Nice підтримує DBC, він суттєво відрізняється від Java ™, тому його складно впровадити в групах з розробки ПЗ. На щастя, є бібліотеки для Java, що полегшують реалізацію DBC. Кожна бібліотека має свої “за” і “проти”, різні бібліотеки можуть використовувати різні підходи до реалізації DBC для Java, а проте останні новинки в цій галузі дозволяють використовувати для реалізації завдань DBC кошти АОП, що діють як “упаковки” методів.


Передумова перевіряється перед виконанням “упакованого” методу, постусловіем – після завершення виконання методу. Одна з приємних особливостей використання АОП для створення конструкцій DBC полягає в тому, що самі конструкції можна відключити в середовищах, де DBC не потрібно (подібно до того, як можна відключити оператори контролю). Однак даний витонченість реалізації принципів захисного програмування за допомогою наскрізних завдань полягає в можливості ефективного багаторазового використання цих коштів. Як відомо, багаторазове використання є одним з основних принципів об’єктно-орієнтованого програмування. Чи не правда, АОП здорово доповнює ООП?


АОП з використанням OVal


OVal являє собою загальну середу перевірки коду, що підтримує прості конструкції DBC допомогою АОП і дозволяє виконувати наступні операції:



Більш того, OVal включає безліч стандартних обмежень, що значно спрощує процес створення нових обмежень.


Оскільки в OVal для визначення елементів advices (рекомендацій) для понять DBC використовується реалізація АОП AspectJ, в проект, який використовує OVal, необхідно включити AspectJ. Доброю новиною для тих, хто не знайомий з АОП і AspectJ, є те, що потрібно мінімум зусиль, а використання OVal (навіть для створення нових обмежень) не зажадає фактичного кодування аспектів, за винятком створення простого аспекту початкового завантаження, який підключає до коду аспекти, що входять до складу OVal.


Перед створенням такого аспекту початкового завантаження необхідно завантажити AspectJ. Зокрема, необхідно включити JAR-файли aspectjtools і aspectjrt в збірку для компіляції необхідного аспекту початкового завантаження і впровадження його в код.


Початкова завантаження АОП


Після завантаження AspectJ необхідно створити аспект, що розширює GuardAspect середовища OVal. Сам він нічого не повинен робити, як показано в лістингу 5. Простежте, щоб розширення файлу закінчувалося на. Aj, і не намагайтеся компілювати файл зі звичайним розширенням javac.


Лістинг 5. Аспект початкового завантаження DefaultGuardAspect





import net.sf.oval.aspectj.GuardAspect;
public aspect DefaultGuardAspect extends GuardAspect{
public DefaultGuardAspect(){
super();
}
}

AspectJ включає завдання Ant під назвою iajc, Що діє подібно javac; Але в цьому процесі аспекти компілюються і вбудовуються в суб’єктний код. В даному випадку, скрізь, де я вкажу обмеження OVal, логіка, визначена в коді OVal, буде вбудована в код і буде діяти як передумови і післяумови.


Слід пам’ятати, що iajc замінює собою javac. Наприклад, в лістингу 6 наведено фрагмент коду Ant файлу build.xml, який компілює код і вбудовує в нього аспекти OVal, виявлені по анотаціям в коді, як ви незабаром побачите:


Лістинг 6. Фрагмент файлу збірки Ant з компіляцією АОП





<target name=”aspectjc” depends=”get-deps”>
<taskdef resource=”org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties”>
<classpath>
<path refid=”build.classpath” />
</classpath>
</taskdef>
<iajc destdir=”${classesdir}” debug=”on” source=”1.5″>
<classpath>
<path refid=”build.classpath” />
</classpath>
<sourceroots>
<pathelement location=”src/java” />
<pathelement location=”test/java” />
</sourceroots>
</iajc>
</target>

Тепер, після того як ми налаштували кошти OVal і здійснили початкове завантаження процесу АОП, можна приступити до завдання простих обмежень для коду з використанням анотування Java 5.


Багаторазово використовувані обмеження OVal


Щоб задати післяумови для методу за допомогою OVal, необхідно анотувати параметри методу. Відповідно, при виклику методу, анотованого з обмеженнями OVal, OVal перевіряє обмеження до фактичного виконання методу.


У нашому випадку хотілося б зробити так, щоб метод buildHierarchy не викликався при передачі значення null для параметра Class. У стандартній комплектації OVal підтримує це обмеження за допомогою анотації @NotNull, Яку можна ставити перед будь-яким потрібним параметром методу. Зверніть також увагу, що будь-який клас, де ми хочемо використовувати обмеження OVal, повинен також мати анотацію @Guarded на рівні класу, як це зроблено в лістингу 7:


Лістинг 7. Обмеження OVal в дії





import net.sf.oval.annotations.Guarded;
import net.sf.oval.constraints.NotNull;
@Guarded
public class HierarchyBuilder {
public static Hierarchy buildHierarchy(@NotNull Class clzz){
Hierarchy hier = new Hierarchy();
hier.setBaseClass(clzz);
Class superclass = clzz.getSuperclass();
if(superclass != null && superclass.getName().equals(“java.lang.Object”)){
return hier;
}else{
while((clzz.getSuperclass() != null) &&
(!clzz.getSuperclass().getName().equals(“java.lang.Object”))){
clzz = clzz.getSuperclass();
hier.addClass(clzz);
}
return hier;
}
}
}

Після того як ми задали це обмеження за допомогою анотацій, нам більше не потрібно щоразу додавати в код умови для перевірки на null і виклику виключення при виявленні цього значення. Тепер ця логіка обробляється OVal, який фактично діє таким же чином. Якщо обмеження порушується, OVal викликає метод ConstraintsViolatedException, Який є підкласом RuntimeException.


Зрозуміло, наступний крок полягає в компіляції класу а HierarchyBuilder та відповідного DefaultGuardAspect, Представленого в лістингу 5. Для цього використовується завдання iajc з лістингу 6, Яка дозволяє вбудувати функціональність OVal вбудувати в мій код.


Тепер модифікуємо тест, представлений в лістингу 4, Щоб переконатися, що виклик ConstraintsViolatedException , Дійсно відбувається; див. лістинг 8:


Лістинг 8. Перевірка виключення ConstraintsViolatedException





@Test(expectedExceptions={ConstraintsViolatedException.class})
public void verifyHierarchyNull() throws Exception{
Class clzz = null;
HierarchyBuilder.buildHierarchy(clzz);
}

Визначення постусловіем


Як видно, визначення передумов є досить простою операцією. Те ж можна сказати і про визначення постусловіем. Наприклад, якщо нам потрібно, щоб ніякий з операторів, що викликають buildHierarchy, Не повертав значення null (І, отже, нам не потрібно було перевіряти наявність цього значення), можна перед оголошенням методу помістити анотацію @NotNull, Як це показано в лістингу 9:


Лістинг 9. Післяумови в OVal





@NotNull
public static Hierarchy buildHierarchy(@NotNull Class clzz){
//method body
}

Зрозуміло,@NotNull – Не єдине обмеження, яке надається OVal, але я вважаю, що воно дуже корисно для зниження кількості дратівливих винятків NullPointerException або хоча б для швидкого їх виявлення.


Інші обмеження OVal


OVal також підтримує кошти попередньої оцінки членів класів до або після виклику методів. Перевага цього механізму полягає в тому, що він зменшує потребу в багаторазових перевірках обмежень, наприклад, при перевірці розмірів колекцій або обговорюваного раніше умови нерівності значенням null.


Наприклад, в лістингу 10 наведена задача Ant, що створює звіт для ієрархії класів за допомогою HierarchyBuilder. Зверніть увагу, як у методі execute() викликається validate, Який, у свою чергу, перевіряє наявність значень в члені класу fileSet; В іншому випадку було б викликано виняток, оскільки без оцінюваних класів створити звіт неможливо.


Лістинг 10. Завдання HierarchyBuilderTask з перевіркою умови





public class HierarchyBuilderTask extends Task {
private Report report;
private List fileSet;
private void validate() throws BuildException{
if(!(this.fileSet.size() > 0)){
throw new BuildException(“must supply classes to evaluate”);
}
if(this.report == null){
this.log(“no report defined, printing XML to System.out”);
}
}
public void execute() throws BuildException {
validate();
String[] classes = this.getQualifiedClassNames(this.fileSet);
Hierarchy[] hclz = new Hierarchy[classes.length];
try{
for(int x = 0; x < classes.length; x++){
hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]);
}
BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz);
this.handleReportCreation(xmler);
}catch(ClassNotFoundException e){
throw new BuildException(“Unable to load class check classpath! ” + e.getMessage());
}
}
//more methods below….
}

Оскільки я використовую OVal, я можу вчинити так:



Ці два кроки дозволяють ефективно усунути перевірку умов в методі validate() і перекласти цю роботу на OVal (див. лістинг 11):


Лістинг 11. Змінена завдання HierarchyBuilderTask без перевірки умов





@Guarded
public class HierarchyBuilderTask extends Task {
private Report report;
@Size(min = 1)
private List fileSet;
private void validate() throws BuildException {
if (this.report == null) {
this.log(“no report defined, printing XML to System.out”);
}
}
@PreValidateThis
public void execute() throws BuildException {
validate();
String[] classes = this.getQualifiedClassNames(this.fileSet);
Hierarchy[] hclz = new Hierarchy[classes.length];
try{
for(int x = 0; x < classes.length; x++){
hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]);
}
BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz);
this.handleReportCreation(xmler);
}catch(ClassNotFoundException e){
throw new BuildException(“Unable to load class check classpath! ” + e.getMessage());
}
}
//more methods below….
}

При кожному виклику execute() в лістингу 11 (яке виконується з Ant), член fileSet перевіряється OVal. Якщо він порожній, тобто хтось не задав класи для оцінки, викликається ConstraintsViolatedException. Виняток зупиняє процес, подібно до того як це робилося у вихідному коді викликом BuildException.


Прикінцеві міркування


Конструкції захисного програмування запобігають безліч дефектів, але самі перевантажують код повторюваної логікою. Поєднання методів захисного програмування з аспектно-орієнтованим програмуванням (За допомогою контрактних специфікацій) – це одна з можливостей зберегти сильні сторони захисного програмування без багаторазового повторення фрагментів коду.


OVal – не єдина наявна бібліотека DBC; насправді конструкції DBC цього середовища досить обмежені в порівнянні з іншими середовищами (наприклад, в OVal немає простого способу завдання інваріантів класу). З іншого боку, зручність використання OVal і широкий вибір обмежень роблять це середовище зручним інструментом для простого додавання перевірочних обмежень до коду. Крім того, в OVal дуже легко створювати власні обмеження, тому закликаю вас – вистачить використовувати перевірочні умови, користуйтеся можливостями АОП!

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*