Главная страница
Навигация по странице:

  • Классы должны быть компактными!

  • Создание, анализ ирефакторинг


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница19 из 49
    1   ...   15   16   17   18   19   20   21   22   ...   49
    Двойной стандарт
    Группа, о которой я упоминал в начале этой главы, в определенном смысле была права . Код тестового API подчиняется несколько иным техническим стандартам, чем код продукта . Он также должен быть простым, лаконичным и выразитель- ным, но от него не требуется такая эффективность . В конце концов, тестовый код работает в тестовой среде, а не в среде реальной эксплуатации продукта, а эти среды весьма заметно различаются по своим потребностям .
    Рассмотрим тест из листинга 9 .3 . Я написал его в ходе работы над прототипом си- стемы контроля окружающей среды . Не вдаваясь в подробности, скажу, что тест этот проверяет, что при слишком низкой температуре включается механизм опо- вещения о низкой температуре, обогреватель и система подачи нагретого воздуха .
    листинг 9 .3 . EnvironmentControllerTest.java
    @Test public void turnOnLoTempAlarmAtThreashold() throws Exception {
    hw.setTemp(WAY_TOO_COLD);
    controller.tic();
    assertTrue(hw.heaterState());
    assertTrue(hw.blowerState());
    assertFalse(hw.coolerState());
    продолжение
    157

    158
    Глава 9 . Модульные тесты
    листинг 9 .3 (продолжение)
    assertFalse(hw.hiTempAlarm());
    assertTrue(hw.loTempAlarm());
    }
    Конечно, этот листинг содержит множество ненужных подробностей . Например, что делает функция tic
    ? Я бы предпочел, чтобы читатель не задумывался об этом в ходе чтения теста . Читатель должен думать о другом: соответствует ли конечное состояние системы его представлениям о «слишком низкой» температуре .
    Обратите внимание: в ходе чтения теста вам постоянно приходится переключать- ся между названием проверяемого состояния и условием проверки . Вы смотри- те на heaterState
    (состояние обогревателя), а затем ваш взгляд скользит налево к assertTrue
    . Вы смотрите на coolerState
    (состояние охладителя), а ваш взгляд отступает к assertFalse
    . Все эти перемещения утомительны и ненадежны . Они усложняют чтение теста .
    В листинге 9 .4 представлена новая форма теста, которая читается гораздо проще .
    листинг 9 .4 . EnvironmentControllerTest.java (переработанная версия)
    @Test public void turnOnLoTempAlarmAtThreshold() throws Exception {
    wayTooCold();
    assertEquals("HBchL", hw.getState());
    }
    Конечно, я скрыл функцию tic
    , создав более понятную функцию wayTooCold
    . Но особого внимания заслуживает странная строка в вызове assertEquals
    . Верхний регистр означает включенное состояние, нижний регистр — выключенное состоя- ние, а буквы всегда следуют в определенном порядке: {обогреватель, подача воз-
    духа, охладитель, сигнал о высокой температуре, сигнал о низкой температуре} .
    Хотя такая форма близка к нарушению правила о мысленных преобразовани- ях
    1
    , в данном случае она выглядит уместной . Если вам известен смысл этих обозначений, ваш взгляд скользит по строке в одном направлении и вы можете быстро интерпретировать результаты . Чтение таких тестов почти что доставляет удовольствие . Взгляните на листинг 9 .5 и убедитесь, как легко понять их смысл .
    листинг 9 .5 . EnvironmentControllerTest.java (расширенный набор)
    @Test public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
    tooHot();
    assertEquals("hBChl", hw.getState());
    }
    @Test public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
    tooCold();
    1
    См . «Избегайте мысленных преобразований», с . 47 .
    158

    Одна проверка на тест
    159
    assertEquals("HBchl", hw.getState());
    }
    @Test public void turnOnHiTempAlarmAtThreshold() throws Exception {
    wayTooHot();
    assertEquals("hBCHl", hw.getState());
    }
    @Test public void turnOnLoTempAlarmAtThreshold() throws Exception {
    wayTooCold();
    assertEquals("HBchL", hw.getState());
    }
    Функция getState приведена в листинге 9 .6 . Обратите внимание: эффективность этого кода оставляет желать лучшего . Чтобы сделать его более эффективным, вероятно, мне стоило использовать класс
    StringBuffer
    листинг 9 .6 . MockControlHardware.java public String getState() {
    String state = "";
    state += heater ? "H" : "h";
    state += blower ? "B" : "b";
    state += cooler ? "C" : "c";
    state += hiTempAlarm ? "H" : "h";
    state += loTempAlarm ? "L" : "l";
    return state;
    }
    Класс
    StringBuffer некрасив и неудобен . Даже в коде продукта я стараюсь избе- гать его, если это не приводит к большим потерям; конечно, в коде из листинга 9 .6 потери невелики . Однако следует учитывать, что приложение пишется для встро- енной системы реального времени, в которой вычислительные ресурсы и память сильно ограничены . С другой стороны, в среде тестирования такие ограничения отсутствуют .
    В этом проявляется природа двойного стандарта . Многое из того, что вы никогда не станете делать в среде эксплуатации продукта, абсолютно нормально выглядит в среде тестирования . Обычно речь идет о затратах памяти или эффективности работы процессора — но никогда о проблемах чистоты кода .
    Одна проверка на тест
    Существует точка зрения
    1
    , согласно которой каждая тестовая функция в тесте
    JUnit должна содержать одну — и только одну — директиву assert
    . Такое правило
    1
    См . запись в блоге Дейва Астела (Dave Astel): http://www .artima .com/weblogs/viewpost .
    jsp?thread=35578 .
    159

    160
    Глава 9 . Модульные тесты может показаться излишне жестким, но его преимущества наглядно видны в листинге 9 .5 . Тесты приводят к одному выводу, который можно быстро и легко понять при чтении .
    Но что вы скажете о листинге 9 .2? В нем объединена проверка двух условий: что выходные данные представлены в формате XML и они содержат некоторые под- строки . На первый взгляд такое решение выглядит сомнительно . Впрочем, тест можно разбить на два отдельных теста, каждый из которых имеет собственную директиву assert
    , как показано в листинге 9 .7 .
    листинг 9 .7 . SerializedPageResponderTest.java (одна директива assert)
    public void testGetPageHierarchyAsXml() throws Exception {
    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
    whenRequestIsIssued("root", "type:pages");
    thenResponseShouldBeXML();
    }
    public void testGetPageHierarchyHasRightTags() throws Exception {
    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
    whenRequestIsIssued("root", "type:pages");
    thenResponseShouldContain(
    "PageOne", "PageTwo", "ChildOne"
    );
    }
    Обратите внимание: я переименовал функции в соответствии со стандартной схемой given
    - when
    - then
    [RSpec] . Это еще сильнее упрощает чтение тестов . К со- жалению, такое разбиение приводит к появлению большого количества дубли- рующегося кода .
    Чтобы избежать дублирования, можно воспользоваться паттерном ШАБЛОН-
    НЫЙ МЕТОД [GOF], включить части given
    /
    when в базовый класс, а части then
    — в различные производные классы . А можно создать отдельный тестовый класс, поместить части given и when в функцию
    @Before
    , а части then
    — в каждую функ- цию
    @Test
    . Но похоже, такой механизм слишком сложен для столь незначитель- ной проблемы . В конечном итоге я предпочел решение с множественными ди- рективами assert из листинга 9 .2 .
    Я думаю, что правило «одного assert
    » является хорошей рекомендацией . Обычно я стараюсь создать предметно-ориентированный язык тестирования, который это правило поддерживает, как в листинге 9 .5 . Но при этом я не боюсь включать в свои тесты более одной директивы assert
    . Вероятно, лучше всего сказать, что количество директив assert в тесте должно быть сведено к минимуму .
    160

    Одна проверка на тест
    161
    Одна концепция на тест
    Пожалуй, более полезное правило гласит, что в каждой тестовой функции должна тестироваться одна концепция . Мы не хотим, чтобы длинные тестовые функции выполняли несколько разнородных проверок одну за другой . Листинг 9 .8 со- держит типичный пример такого рода . Этот тест следовало бы разбить на три независимых теста, потому что в нем выполняются три независимых проверки .
    Объединение их в одной функции заставляет читателя гадать, почему в функцию включается каждая секция, и какое условие проверяется в этой секции .
    листинг 9 .8
    /**
    * Тесты для метода addMonths().
    */
    public void testAddMonths() {
    SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
    SerialDate d2 = SerialDate.addMonths(1, d1);
    assertEquals(30, d2.getDayOfMonth());
    assertEquals(6, d2.getMonth());
    assertEquals(2004, d2.getYYYY());
    SerialDate d3 = SerialDate.addMonths(2, d1);
    assertEquals(31, d3.getDayOfMonth());
    assertEquals(7, d3.getMonth());
    assertEquals(2004, d3.getYYYY());
    SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
    assertEquals(30, d4.getDayOfMonth());
    assertEquals(7, d4.getMonth());
    assertEquals(2004, d4.getYYYY());
    }
    Вероятно, три тестовые функции должны выглядеть так:
    
    Given
    : последний день месяца, состоящего из 31 дня (например, май) .
    1)
    When
    : при добавлении одного месяца, последним днем которого является
    30-е число (например, июнь), датой должно быть 30-е число этого месяца, а не 31-е .
    2)
    When
    : при добавлении двух месяцев, когда последним днем второго месяца является 31-е число, датой должно быть 31-е число .
    
    Given
    : последний день месяца, состоящего из 30 дней (например, июнь) .
    1)
    When
    : при добавлении одного месяца, последним днем которого является
    31-е число, датой должно быть 30-е число этого месяца, а не 31-е .
    В такой формулировке видно, что среди разнородных тестов скрывается одно общее правило . При увеличении месяца дата не может превысить последнее чис- ло нового месяца . Следовательно, в результате увеличение месяца для 28 февраля
    161

    162
    Глава 9 . Модульные тесты должна быть получена дата 28 марта . В текущей версии это условие не проверя- ется, хотя такой тест был бы полезен .
    Таким образом, проблема не в множественных директивах assert в разных секци- ях листинга 9 .8 . Проблема в том, что тест проверяет более одной концепции . Так что, вероятно, лучше всего сформулировать это правило так: количество директив assert на концепцию должно быть минимальным, и в тестовой функции должна проверяться только одна концепция .
    f .I .r .S .T .
    1
    Чистые тесты должны обладать еще пятью характеристиками, названия которых образуют приведенное сокращение .
    Быстрота (Fast) . Тесты должны выполняться быстро . Если тесты выполняются медленно, вам не захочется часто запускать их . Без частого запуска тестов про- блемы не будут выявляться на достаточно ранней стадии, когда они особенно легко исправляются . В итоге вы уже не так спокойно относитесь к чистке своего кода, и со временем код начинает загнивать .
    Независимость (Independent) . Тесты не должны зависеть друг от друга . Один тест не должен создавать условия для выполнения следующего теста . Все тесты должны выполняться независимо и в любом порядке на ваше усмотрение . Если тесты зависят друг от друга, то при первом отказе возникает целый каскад сбоев, который усложняет диагностику и скрывает дефекты в зависимых тестах .
    Повторяемость (Repeatable) . Тесты должны давать повторяемые результаты в любой среде . Вы должны иметь возможность выполнить тесты в среде реальной эксплуатации, в среде тестирования или на вашем ноутбуке во время возвраще- ния домой с работы . Если ваши тесты не будут давать однозначных результатов в любых условиях, вы всегда сможете найти отговорку для объяснения неудач .
    Также вы лишитесь возможности проводить тестирование, если нужная среда недоступна .
    Очевидность (Self-Validating) . Результатом выполнения теста должен быть ло- гический признак . Тест либо прошел, либо не прошел . Чтобы узнать результат, пользователь не должен читать журнальный файл . Не заставляйте его вручную сравнивать два разных текстовых файла . Если результат теста не очевиден, то отказы приобретают субъективный характер, а выполнение тестов может потре- бовать долгой ручной обработки данных .
    Своевременность (Timely) . Тесты должны создаваться своевременно . Модуль- ные тесты пишутся непосредственно перед кодом продукта, обеспечивающим их прохождение . Если вы пишете тесты после кода продукта, вы можете решить, что тестирование кода продукта создает слишком много трудностей, а все из-за того, что удобство тестирования не учитывалось при проектировании кода продукта .
    1
    Учебные материалы Object Mentor .
    162

    Литература
    163
    Заключение
    В этой главе мы едва затронули тему тестирования . Я думаю, что на тему чистых
    тестов можно было бы написать целую книгу
    . Для «здоровья» проекта тесты не менее важны, чем код продукта . А может быть, они еще важнее, потому что те- сты сохраняют и улучшают гибкость, удобство сопровождения и возможности повторного использования кода продукта . Постоянно следите за чистотой своих тестов . Постарайтесь сделать их выразительными и лаконичными . Изобретайте тестовые API, которым отводится роль предметно-ориентированного языка те- стирования, упрощающего написание тестов .
    Если вы будете пренебрежительно относиться к тестам, то и ваш код начнет за- гнивать . Поддерживайте чистоту в своих тестах .
    литература
    [RSpec]: RSpec: Behavior Driven Development for Ruby Programmers, Aslak Hellesy,
    David Chelimsky, Pragmatic Bookshelf, 2008 .
    [GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al ., Addison-Wesley, 1996 .
    163

    Классы
    Совместно с Джеффом Лангром
    До настоящего момента наше внимание было сосредоточено исключительно на том, как качественно написать строки и блоки кода . Мы разобрались с правиль- ной композицией функций и их взаимосвязями . Но какими бы выразительными ни были функции и содержащиеся в них команды, мы не добьемся чистоты кода до тех пор, пока не обратим внимание на более высокие уровни организации кода .
    В этой главе речь пойдет о чистоте классов .
    Строение класса
    По стандартным правилам Java класс должен начинаться со списка переменных .
    Сначала перечисляются открытые статические константы . Далее следуют при- ватные статические переменные, а за ними идут приватные переменные экзем-
    10
    164

    Строение класса
    165
    пляров . Открытых переменных обычно нет, трудно найти веские причины для их использования .
    За списком переменных обычно следуют открытые функции . Мы предпочита- ем размещать приватные вспомогательные функции, вызываемые открытыми функциями, непосредственно за самой открытой функцией . Такое размещение соответствует правилу понижения, в результате чего программа читается как газетная статья .
    Инкапсуляция
    Мы предпочитаем объявлять переменные и вспомогательные функции приват- ными, но относимся к ним без фанатизма . Иногда переменную или вспомогатель- ную функцию приходится объявлять защищенной, чтобы иметь возможность обратиться к ней из теста . С нашей точки зрения тесты исключительно важны .
    Если тест из того же пакета должен вызвать функцию или обратиться к перемен- ной, мы используем защищенный или пакетный уровень доступа . Тем не менее начинать следует с поиска способа, сохраняющего приватность . Ослабление инкапсуляции всегда должно быть последней мерой .
    Классы должны быть компактными!
    Первое правило: классы должны быть компактными . Второе правило: классы должны быть еще компактнее . Нет, мы не собираемся повторять текст из главы 3 .
    Но как и в случае с функциями, компактность должна стать основным правилом проектирования классов . И для классов начинать следует с вопроса: «А насколько компактными?»
    Размер функций определяется количеством физических строк . В классах исполь- зуется другая метрика; мы подсчитываем ответственности [RDD] .
    В листинге 10 .1 представлен класс
    SuperDashboard
    , предоставляющий около 70 от- крытых методов . Большинство разработчиков согласится с тем, что это перебор .
    листинг 10 .1 . Слишком много ответственностей public class SuperDashboard extends JFrame implements MetaDataUser public String getCustomizerLanguagePath()
    public void setSystemConfigPath(String systemConfigPath)
    public String getSystemConfigDocument()
    public void setSystemConfigDocument(String systemConfigDocument)
    public boolean getGuruState()
    public boolean getNoviceState()
    public boolean getOpenSourceState()
    public void showObject(MetaObject object)
    public void showProgress(String s)
    public boolean isMetadataDirty()
    public void setIsMetadataDirty(boolean isMetadataDirty)
    public Component getLastFocusedComponent()
    public void setLastFocused(Component lastFocused)
    продолжение
    165

    166
    Глава 10 . Классы
    листинг 10 .1 (продолжение)
    public void setMouseSelectState(boolean isMouseSelected)
    public boolean isMouseSelected()
    public LanguageManager getLanguageManager()
    public Project getProject()
    public Project getFirstProject()
    public Project getLastProject()
    public String getNewProjectName()
    public void setComponentSizes(Dimension dim)
    public String getCurrentDir()
    public void setCurrentDir(String newDir)
    public void updateStatus(int dotPos, int markPos)
    public Class[] getDataBaseClasses()
    public MetadataFeeder getMetadataFeeder()
    public void addProject(Project project)
    public boolean setCurrentProject(Project project)
    public boolean removeProject(Project project)
    public MetaProjectHeader getProgramMetadata()
    public void resetDashboard()
    public Project loadProject(String fileName, String projectName)
    public void setCanSaveMetadata(boolean canSave)
    public MetaObject getSelectedObject()
    public void deselectObjects()
    public void setProject(Project project)
    public void editorAction(String actionName, ActionEvent event)
    public void setMode(int mode)
    public FileManager getFileManager()
    public void setFileManager(FileManager fileManager)
    public ConfigManager getConfigManager()
    public void setConfigManager(ConfigManager configManager)
    public ClassLoader getClassLoader()
    public void setClassLoader(ClassLoader classLoader)
    public Properties getProps()
    public String getUserHome()
    public String getBaseDir()
    public int getMajorVersionNumber()
    public int getMinorVersionNumber()
    public int getBuildNumber()
    public MetaObject pasting(
    MetaObject target, MetaObject pasted, MetaProject project)
    public void processMenuItems(MetaObject metaObject)
    public void processMenuSeparators(MetaObject metaObject)
    public void processTabPages(MetaObject metaObject)
    public void processPlacement(MetaObject object)
    public void processCreateLayout(MetaObject object)
    public void updateDisplayLayer(MetaObject object, int layerIndex)
    public void propertyEditedRepaint(MetaObject object)
    public void processDeleteObject(MetaObject object)
    public boolean getAttachedToDesigner()
    public void processProjectChangedState(boolean hasProjectChanged)
    public void processObjectNameChanged(MetaObject object)
    public void runProject()
    166

    Строение класса
    1   ...   15   16   17   18   19   20   21   22   ...   49


    написать администратору сайта