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

  • Исследование и анализ границ

  • Использование несуществующего кода

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


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница17 из 49
    1   ...   13   14   15   16   17   18   19   20   ...   49
    140
    Глава 7 . Обработка ошибок for(Employee e : employees) {
    totalPay += e.getPay();
    }
    }
    Сейчас метод getEmployees может возвращать null
    , но так ли это необходимо?
    Если изменить getEmployee так, чтобы метод возвращал пустой список, код станет чище:
    List employees = getEmployees();
    for(Employee e : employees) {
    totalPay += e.getPay();
    }
    К счастью, в Java существует метод
    Collections.emptyList()
    , который возвращает заранее определенный неизменяемый список, и мы можем воспользоваться им для своих целей:
    public List getEmployees() {
    if( .. there are no employees .. ) return Collections.emptyList();
    }
    Такое решение сводит к минимуму вероятность появления
    NullPointerException
    , а код становится намного чище .
    не передавайте null
    Возвращать null из методов плохо, но передавать null при вызове еще хуже . По возможности избегайте передачи null в своем коде (исключение составляют разве что методы сторонних API, при вызове которых без нее не обойтись) .
    Следующий пример поясняет, почему не следует передавать null
    . Возьмем про- стой метод для вычисления метрики по двум точкам:
    public class MetricsCalculator
    {
    public double xProjection(Point p1, Point p2) {
    return (p2.x — p1.x) * 1.5;
    }

    }
    Что произойдет, если при вызове будет передан аргумент null
    ?
    calculator.xProjection(null, new Point(12, 13));
    Конечно, возникнет исключение
    NullPointerException
    Как исправить его? Можно создать новый тип исключения и инициировать его в методе:
    public class MetricsCalculator
    {
    public double xProjection(Point p1, Point p2) {
    if (p1 == null || p2 == null) {
    140

    Литература
    141
    throw InvalidArgumentException(
    "Invalid argument for MetricsCalculator.xProjection");
    }
    return (p2.x — p1.x) * 1.5;
    }
    }
    Стало лучше? Пожалуй, лучше, чем
    NullPointerException
    , но вспомните: для
    In- validArgumentException приходится определять обработчик . Что должен делать этот обработчик? Возьметесь предложить хорошую идею?
    Существует другая альтернатива: можно воспользоваться набором проверочных директив assert
    :
    public class MetricsCalculator
    {
    public double xProjection(Point p1, Point p2) {
    assert p1 != null : "p1 should not be null";
    assert p2 != null : "p2 should not be null";
    return (p2.x — p1.x) * 1.5;
    }
    }
    Неплохо с точки зрения документирования, но проблема не решена . Если при вызове передать null
    , произойдет ошибка времени выполнения .
    В большинстве языков программирования не существует хорошего способа справиться со случайной передачей null с вызывающей стороны . А раз так, разумно запретить передачу null по умолчанию . В этом случае вы будете знать, что присутствие null в списке аргументов свидетельствует о возникшей про- блеме; это будет способствовать уменьшению количества ошибок, сделанных по неосторожности .
    Заключение
    Чистый код хорошо читается, но он также должен быть надежным . Эти цели не конфликтуют друг с другом . Чтобы написать надежный и чистый код, следует рассматривать обработку ошибок как отдельную задачу, решаемую независимо от основной логики программы . В зависимости от того, насколько нам это удастся, мы сможем прорабатывать ее реализацию независимо от основной логики про- граммы, а это окажет существенное положительное влияние на удобство сопро- вождения нашего кода .
    литература
    [Martin]: Agile Software Development: Principles, Patterns, and Practices, Robert
    C . Martin, Prentice Hall, 2002 .
    141

    Границы
    Джеймс Гренинг
    Редко когда весь программный код наших систем находится под нашим полным контролем . Иногда нам приходится покупать пакеты сторонних разработчиков или использовать открытый код . В других случаях мы зависим от других групп нашей компании, производящих компоненты или подсистемы для нашего про- екта . И этот внешний код мы должны каким-то образом четко интегрировать со своим кодом . В этой главе рассматриваются приемы и методы «сохранения чистоты» границ нашего программного кода .
    8
    142

    Использование стороннего кода
    143
    Использование стороннего кода
    Между поставщиком и пользователем интерфейса существует естественная напряженность . Поставщики сторонних пакетов и инфраструктур стремятся к универсальности, чтобы их продукты работали в разных средах и были обра- щены к широкой аудитории . С другой стороны, пользователи желают получить интерфейс, специализирующийся на их конкретных потребностях . Эта напря- женность приводит к появлению проблем на границах наших систем .
    Для примера возьмем класс java.util.Map
    . Как видно из рис . 8 .1,
    Map имеет очень широкий интерфейс с многочисленными возможностями . Конечно, мощь и гиб- кость контейнера полезны, но они также создают некоторые неудобства . Допу- стим, наше приложение строит объект
    Map и передает его другим сторонам . При этом мы не хотим, чтобы получатели
    Map удаляли данные из полученного кон- тейнера . Но в самом начале списка стоит метод clear()
    , и любой пользователь
    Map может стереть текущее содержимое контейнера . А может быть, наша архитекту- ра подразумевает, что в контейнере должны храниться объекты только опреде- ленного типа, но
    Map не обладает надежными средствами ограничения типов со- храняемых объектов . Любой настойчивый пользователь сможет разместить в
    Map элементы любого типа .
    y clear() void – Map y
    containsKey(Object key) boolean – Map y
    containsValue(Object value) boolean – Map y
    entrySet() Set – Map y
    equals(Object o) boolean – Map y
    get(Object key) Object – Map y
    getClass() Class – Object y
    hashCode() int – Map y
    isEmpty() boolean – Map y
    keySet() Set – Map y
    notify() void – Object y
    notifyAll() void – Object y
    put(Object key, Object value) Object – Map y
    putAll(Map t) void – Map y
    remove(Object key) Object – Map y
    size() int – Map y
    toString() String – Object y
    values() Collection – Map y
    wait() void – Object y
    wait(long timeout) void – Object y
    wait(long timeout, int nanos) void – Object
    Рис . 8 .1 . Методы Map
    Если в приложении требуется контейнер
    Map с элементами
    Sensor
    , его можно со- здать следующим образом:
    Map sensors = new HashMap();
    143

    144
    Глава 8 . Границы
    Когда другой части кода понадобится обратиться к элементу, мы видим код сле- дующего вида:
    Sensor s = (Sensor)sensors.get(sensorId );
    Причем видим его не только в этом месте, но снова и снова по всему коду . Клиент кода несет ответственность за получение
    Object из
    Map и его приведение к правиль- ному типу . Такое решение работает, но «чистым» его не назовешь . Кроме того, этот код не излагает свою историю, как ему положено . Удобочитаемость кода можно было бы заметно улучшить при помощи шаблонов (параметризованных контейнеров):
    Map sensors = new HashMap();
    Sensor s = sensors.get(sensorId );
    Но и такая реализация не решает проблемы:
    Map
    предоставляет намного больше возможностей, чем нам хотелось бы .
    Свободная передача
    Map
    по системе означает, что в случае изменения интерфейса
    Map исправления придется вносить во множестве мест . Казалось бы, такие изменения маловероятны, но вспомните, что интерфейс изменился при добавлении поддержки шаблонов в Java 5 . В самом деле, мы видели системы, разработчики которых воздерживались от использования шаблонов из-за боль- шого количества потенциальных изменений, связанных с частым использова- нием
    Map
    Ниже представлен другой, более чистый вариант использования
    Map
    . С точки зре- ния пользователя
    Sensors совершенно не важно, используются шаблоны или нет .
    Это решение стало (и всегда должно быть) подробностью реализации .
    public class Sensors {
    private Map sensors = new HashMap();
    public Sensor getById(String id) {
    return (Sensor) sensors.get(id);
    }
    //...
    }
    Граничный интерфейс (
    Map
    ) скрыт от пользователя . Он может развиваться не- зависимо, практически не оказывая никакого влияния на остальные части при- ложения . Применение шаблонов уже не создает проблем, потому что все преоб- разования типов выполняются в классе
    Sensors
    Этот интерфейс также приспособлен и ограничен в соответствии с потребностя- ми приложения . Код становится более понятным, а возможности злоупотребле- ний со стороны пользователя сокращаются . Класс
    Sensors может обеспечивать выполнение архитектурных требований и требований бизнес-логики .
    Поймите правильно: мы не предлагаем инкапсулировать каждое применение
    Map в этой форме . Скорее, мы рекомендуем ограничить передачу
    Map
    (или любого
    144

    Исследование и анализ границ
    145
    другого граничного интерфейса) по системе . Если вы используете граничный интерфейс вроде
    Map
    , держите его внутри класса (или тесно связанного семейства классов), в которых он используется . Избегайте его возвращения или передачи в аргументах при вызовах методов общедоступных API .
    Исследование и анализ границ
    Сторонний код помогает нам реализовать больше функциональности за меньшее время . С чего начинать, если мы хотим использовать сторонний пакет? Тести- рование чужого кода не входит в наши обязанности, но, возможно, написание тестов для стороннего кода, используемого в наших продуктах, в наших же интересах .
    Допустим, вам не ясно, как использовать стороннюю библиотеку . Можно потра- тить день-два (или более) на чтение документации и принятие решений о том, как работать с библиотекой . Затем вы пишете код, использующий стороннюю библиотеку, и смотрите, делает ли он то, что ожидалось . Далее вы, скорее всего, погрязнете в долгих сеансах отладки, пытаясь разобраться, в чьем коде возникают ошибки – в стороннем или в вашем собственном .
    Изучение чужого кода – непростая задача . Интеграция чужого кода тоже сложна .
    Одновременное решение обоих задач создает двойные сложности . А что, если пойти по другому пути? Вместо того чтобы экспериментировать и опробовать новую библиотеку в коде продукта, можно написать тесты, проверяющие наше понимание стороннего кода . Джим Ньюкирк ( Jim Newkirk) называет такие тесты
    «учебными тестами» [BeckTDD, pp . 136–137] .
    В учебных тестах мы вызываем методы стороннего API в том виде, в котором намереваемся использовать их в своем приложении . Фактически выполняется контролируемый эксперимент, проверяющий наше понимание стороннего API .
    Основное внимание в тестах направлено на то, чего мы хотим добиться при по- мощи API .
    Изучение log4j
    Допустим, вместо того чтобы писать специализированный журнальный модуль, мы хотим использовать пакет apache log4j
    . Мы загружаем пакет и открываем страницу вводной документации . Не особенно вчитываясь в нее, мы пишем свой первый тестовый сценарий, который, как предполагается, будет выводить на консоль строку «hello» .
    @Test public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
    }
    145

    146
    Глава 8 . Границы
    При запуске журнальный модуль выдает ошибку . В описании ошибки говорится, что нам понадобится нечто под названием
    Appender
    . После непродолжительных поисков в документации обнаруживается класс
    ConsoleAppender
    . Соответственно, мы создаем объект
    ConsoleAppender и проверяем, удалось ли нам раскрыть секреты вывода журнала на консоль:
    @Test public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConsoleAppender();
    logger.addAppender(appender);
    logger.info("hello");
    }
    На этот раз выясняется, что у объекта
    Appender нет выходного потока . Странно – логика подсказывает, что он должен быть . После небольшой помощи от Google опробуется следующее решение:
    @Test public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.removeAllAppenders();
    logger.addAppender(new ConsoleAppender(
    new PatternLayout("%p %t %m%n"),
    ConsoleAppender.SYSTEM_OUT));
    logger.info("hello");
    }
    Заработало; на консоли выводится сообщение со словом «hello»! На первый взгляд происходящее выглядит немного странно: мы должны указывать
    Console-
    Appender
    , что данные выводятся на консоль .
    Еще интереснее, что при удалении аргумента
    ConsoleAppender.SystemOut сообщение
    «hello» все равно выводится . Но если убрать аргумент
    PatternLayout
    , снова начи- наются жалобы на отсутствие выходного потока . Все это выглядит очень странно .
    После более внимательного чтения документации мы видим, что конструктор
    ConsoleAppender по умолчанию «не имеет конфигурации» – весьма неочевидное и бесполезное решение . Похоже, это ошибка (или по крайней мере нелогичность) в log4j
    После некоторых поисков, чтения документации и тестирования мы приходим к листингу 8 .1 . Попутно мы получили много полезной информации о том, как работает log4j
    , и закодировали ее в наборе простых модульных тестов .
    листинг 8 .1 . LogTest.java public class LogTest {
    private Logger logger;
    @Before public void initialize() {
    logger = Logger.getLogger("logger");
    logger.removeAllAppenders();
    146

    Исследование и анализ границ
    147
    Logger.getRootLogger().removeAllAppenders();
    }
    @Test public void basicLogger() {
    BasicConfigurator.configure();
    logger.info("basicLogger");
    }
    @Test public void addAppenderWithStream() {
    logger.addAppender(new ConsoleAppender(
    new PatternLayout("%p %t %m%n"),
    ConsoleAppender.SYSTEM_OUT));
    logger.info(“addAppenderWithStream”);
    }
    @Test public void addAppenderWithoutStream() {
    logger.addAppender(new ConsoleAppender(
    new PatternLayout("%p %t %m%n")));
    logger.info("addAppenderWithoutStream");
    }
    }
    Теперь мы знаем, как инициализировать простейший консольный вывод и можем воплотить эти знания в специализированном журнальном классе, чтобы изоли- ровать остальной код приложения от граничного интерфейса log4j
    учебные тесты: выгоднее, чем бесплатно
    Учебные тесты не стоят ничего . API все равно приходится изучать, а написание тестов является простым способом получения необходимой информации, в изо- ляции от рабочего кода . Учебные тесты были точно поставленными эксперимен- тами, которые помогли нам расширить границы своего понимания .
    Учебные тесты не просто бесплатны – они приносят дополнительную прибыль .
    При выходе новых версий сторонних пакетов вы сможете провести учебные тесты и выяснить, не изменилось ли поведение пакета .
    Учебные тесты позволяют убедиться в том, что сторонние пакеты, используе- мые в коде, работают именно так, как мы ожидаем . Нет никаких гарантий, что сторонний код, интегрированный в наши приложения, всегда будет сохранять совместимость . Например, авторы могут изменить код в соответствии с какими- то новыми потребностями . Изменения также могут происходить из-за исправ- ления ошибок и добавления новых возможностей . Выход каждой новой версии сопряжен с новым риском . Если в стороннем пакете появятся изменения, несо- вместимые с нашими тестами, мы сразу узнаем об этом .
    Впрочем, независимо от того, нужна ли вам учебная информация, получаемая в ходе тестирования, в системе должна существовать четкая граница, которая под-
    147

    148
    Глава 8 . Границы держивается группой исходящих тестов, использующих интерфейс по аналогии с кодом продукта . Без граничных тестов, упрощающих процесс миграции, у нас появляются причины задержаться на старой версии дольше необходимого .
    Использование несуществующего кода
    Также существует еще одна разновидность границ, отделяющая известное от не- известного . В коде часто встречаются места, в которых мы не располагаем полной информацией . Иногда то, что находится на другой стороне границы, остается не- известным (по крайней мере в данный момент) . Иногда мы намеренно не желаем заглядывать дальше границы .
    Несколько лет назад я работал в группе, занимавшейся разработкой программ- ного обеспечения для системы радиосвязи . В нашем продукте была подсистема
    «Передатчик», о которой мы почти ничего не знали, а люди, ответственные за разработку этой подсистемы, еще не дошли до определения своего интерфейса .
    Мы не хотели простаивать и поэтому начали работать подальше от неизвестной части кода .
    Мы неплохо представляли себе, где заканчивалась наша зона ответственности и начиналась чужая территория . В ходе работы мы иногда наталкивались на границу . Хотя туманы и облака незнания скрывали пейзаж за границей, в ходе работы мы начали понимать, каким должен быть граничный интерфейс . Пере- датчику должны были отдаваться распоряжения следующего вида:
    Настроить передатчик на заданную частоту и отправить аналоговое представ-
    ление данных, поступающих из следующего потока.
    Мы тогда понятия не имели, как это будет делаться, потому что API еще не был спроектирован . Поэтому подробности было решено отложить на будущее .
    Чтобы не останавливать работу, мы определили собственный интерфейс с бро- ским именем
    Transmitter
    . Интерфейс содержал метод transmit
    , которому при вызове передавались частота и поток данных . Это был тот интерфейс, который нам хотелось бы иметь .
    У этого интерфейса было одно важное достоинство: он находился под нашим контролем . В результате клиентский код лучше читался, а мы в своей работе могли сосредоточиться на том, чего стремились добиться .
    На рис . 8 .2 мы видим, что классы
    CommunicationsController отделены от API пере- датчика (который находился вне нашего контроля и оставался неопределенным) .
    Использование конкретного интерфейса нашего приложения позволило сохра- нить чистоту и выразительность кода
    CommunicationsController
    . После того как другая группа определила API передатчика, мы написали класс
    TransmitterAdapter для «наведения мостов» . АДАПТЕР
    1
    инкапсулировал взаимодействие с API и создавал единое место для внесения изменений в случае развития API .
    Такая архитектура также создает в коде очень удобный «стык

    » для тестирования .
    Используя подходящий
    FakeTransmitter
    , мы можем тестировать классы
    Communi-
    1
    См . описание паттерна АДАПТЕР в [GOF] .
    148

    Литература
    1   ...   13   14   15   16   17   18   19   20   ...   49


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