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

  • Определяйте классы исключений в контексте потребностей вызывающей стороны

  • Определите нормальный путь выполнения

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


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница16 из 49
    1   ...   12   13   14   15   16   17   18   19   ...   49
    130
    Глава 6 . Объекты и структуры данных
    Заключение
    Объекты предоставляют поведение и скрывают данные . Это позволяет програм- мисту легко добавлять новые виды объектов, не изменяя существующего поведе- ния . С другой стороны, объекты усложняют добавление нового поведения к су- ществующим объектам . Структуры данных предоставляют данные, но не обла- дают сколько-нибудь значительным поведением . Они упрощают добавление но- вого поведения в существующие структуры данных, но затрудняют добавление новых структур данных в существующие функции .
    Если в некоторой системе нас прежде всего интересует гибкость в добавлении новых типов данных, то в этой части системы предпочтение отдается объектной реализации . В других случаях нам нужна гибкость расширения поведения, и тог- да в этой части используются типы данных и процедуры . Хороший программист относится к этой проблеме без предубеждения и выбирает то решение, которое лучше всего подходит для конкретной ситуации .
    литература
    [Refactoring]: Refactoring: Improving the Design of Existing Code, Martin Fowler et al ., Addison-Wesley, 1999 .
    130

    Обработка ошибок
    Майкл Физерс
    На первый взгляд глава, посвященная обработке ошибок, в книге о чистом коде выглядит немного странно . Обработка ошибок — одна из тех рутинных вещей, которыми нам всем приходится заниматься при программировании . Программа может получить аномальные входные данные, на устройстве могут произойти сбои . Короче говоря, выполнение программы может пойти по неверному пути, и если это случается, мы, программисты, должны позаботиться, чтобы наш код сделал то, что ему положено сделать .
    Однако связь этих двух тем — обработки ошибок и чистого кода — очевидна .
    Во многих кодовых базах обработка ошибок выходит на первый план . Я вовсе не хочу сказать, что код не делает ничего полезного, кроме обработки ошибок; я имею в виду, что из-за разбросанной повсюду обработки ошибок практически невозможно понять, что же делает код . Обработка ошибок важна, но если они заслоняют собой логику программы — значит, она реализована неверно .
    В этой главе представлены некоторые соображения и приемы, которые помогают писать чистый и надежный код, то есть код, в котором ошибки обрабатываются стильно и элегантно .
    7
    131

    132
    Глава 7 . Обработка ошибок
    Используйте исключения
    вместо кодов ошибок
    В далеком прошлом многие языки программирования не поддерживали механиз- ма обработки исключений . В таких языках возможности обработки и получения информации об ошибках были ограничены . Программа либо устанавливала флаг ошибки, либо возвращала код, который проверялся вызывающей стороной . Оба способа продемонстрированы в листинге 7 .1 .
    листинг 7 .1 . DeviceController.java public class DeviceController {
    public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
    // Проверить состояние устройства if (handle != DeviceHandle.INVALID) {
    // Сохранить состояние устройства в поле записи retrieveDeviceRecord(handle);
    // Если устройство не приостановлено, отключить его if (record.getStatus() != DEVICE_SUSPENDED) {
    pauseDevice(handle);
    clearDeviceWorkQueue(handle);
    closeDevice(handle);
    } else {
    logger.log("Device suspended. Unable to shut down");
    }
    } else {
    logger.log("Invalid handle for: " + DEV1.toString());
    }
    }
    }
    У обоих решений имеется общий недостаток: они загромождают код на стороне вызова . Вызывающая сторона должна проверять ошибки немедленно после вы- зова . К сожалению, об этом легко забыть . По этой причине при обнаружении ошибки лучше инициировать исключение . Код вызова становится более понят- ным, а его логика не скрывается за кодом обработки ошибок .
    В листинге 7 .2 представлен тот же код с выдачей исключений в методах, способ- ных обнаруживать ошибки .
    Обратите внимание, насколько чище стал код . Причем дело даже не в эстетике .
    Качество кода возросло, потому что два аспекта, которые прежде были тесно переплетены — алгоритм отключения устройства и обработка ошибок, — теперь изолированы друг от друга . Вы можете рассмотреть их по отдельности и разо- браться в каждом из них независимо .
    132

    Начните с написания команды try-catch-finally
    133
    листинг 7 .2 . DeviceController.java (с исключениями)
    public class DeviceController {
    public void sendShutDown() {
    try {
    tryToShutDown();
    } catch (DeviceShutDownError e) {
    logger.log(e);
    }
    }
    private void tryToShutDown() throws DeviceShutDownError {
    DeviceHandle handle = getHandle(DEV1);
    DeviceRecord record = retrieveDeviceRecord(handle);
    pauseDevice(handle);
    clearDeviceWorkQueue(handle);
    closeDevice(handle);
    }
    private DeviceHandle getHandle(DeviceID id) {
    throw new DeviceShutDownError("Invalid handle for: " + id.toString());
    }
    }
    начните с написания команды
    try-catch-finally
    У исключений есть одна интересная особенность: они определяют область види- мости в вашей программе . Размещая код в секции try команды try
    - catch
    - finally
    , вы утверждаете, что выполнение программы может прерваться в любой точке, а затем продолжиться в секции catch
    Блоки try в каком-то отношении напоминают транзакции . Секция catch должна оставить программу в целостном состоянии, что бы и произошло в секции try
    По этой причине написание кода, который может инициировать исключения, ре- комендуется начинать с конструкции try
    - catch
    - finally
    . Это поможет вам опреде- лить, чего должен ожидать пользователь кода, что бы ни произошло в коде try
    Допустим, требуется написать код, который открывает файл и читает из него сериализованные объекты .
    133

    134
    Глава 7 . Обработка ошибок
    Начнем с модульного теста, который проверяет, что при неудачном обращении к файлу будет выдано исключение:
    @Test(expected = StorageException.class)
    public void retrieveSectionShouldThrowOnInvalidFileName() {
    sectionStore.retrieveSection("invalid - file");
    }
    Для теста необходимо создать следующую программную заглушку:
    public List retrieveSection(String sectionName) {
    // Пусто, пока не появится реальная реализация return new ArrayList();
    }
    Тест завершается неудачей, потому что код не инициирует исключения . Затем мы изменяем свою реализацию так, чтобы она попыталась обратиться к несуще- ствующему файлу . При попытке выполнения происходит исключение:
    public List retrieveSection(String sectionName) {
    try {
    FileInputStream stream = new FileInputStream(sectionName)
    } catch (Exception e) {
    throw new StorageException("retrieval error", e);
    }
    return new ArrayList();
    }
    Теперь тест проходит успешно, потому что мы перехватили исключение . На этой стадии можно переработать код . Тип перехватываемого исключения сужается до типа, реально инициируемого конструктором
    FileInputStream
    , то есть
    FileNot-
    FoundException
    :
    public List retrieveSection(String sectionName) {
    try {
    FileInputStream stream = new FileInputStream(sectionName);
    stream.close();
    } catch (FileNotFoundException e) {
    throw new StorageException("retrieval error", e);
    }
    return new ArrayList();
    }
    Определив область видимости при помощи структуры try
    - catch
    , мы можем ис- пользовать методологию TDD

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

    Используйте непроверяемые исключения
    135
    Используйте непроверяемые исключения
    Время споров прошло . Java-программисты годами обсуждали преимущества и недостатки проверяемых исключений (checked exceptions) . Когда проверяемые исключения появились в первой версии Java, всем казалось, что это отличная идея . В сигнатуре каждого метода должны быть перечислены все исключения, которые могут передаваться вызывающей стороне . Фактически исключения становились частью типа метода . Если сигнатура не соответствовала тому, что происходит в коде, то программа просто не компилировалась .
    В то время мы с энтузиазмом относились к проверяемым исключениям; в самом деле, они бывают полезными . Но сейчас стало ясно, что они не являются необхо- димыми для создания надежных программ . В C# нет проверяемых исключений, и несмотря на все доблестные попытки, в C++ они так и не появились . Их также нет в Python и Ruby . Тем не менее на всех этих языках можно писать надежные программы . А раз так, нам приходится решать, оправдывают ли проверяемые исключения ту цену, которую за них приходится платить .
    Какую цену, спросите вы? Цена проверяемых исключений — нарушение прин- ципа открытости/закрытости [Martin] . Если вы инициируете проверяемое ис- ключение из метода своего кода, а catch находится тремя уровнями выше, то это исключение должно быть объявлено в сигнатурах всех методов между вашим методом и catch
    . Следовательно, изменение на низком уровне программного продукта приводит к изменениям сигнатур на многих более высоких уровнях .
    Измененные модули приходится строить и развертывать заново, притом что в программе не изменилось ничего, что было бы существенно для них .
    Представьте иерархию вызовов большой системы . Функции верхнего уровня вызывают функции нижележащего уровня, которые, в свою очередь, вызывают функции низких уровней и т . д . Теперь допустим, что одна из низкоуровневых функций изменилась таким образом, что она должна инициировать исключение .
    Если это исключение является проверяемым, то в сигнатуру функции должна быть добавлена секция throws
    . Но тогда каждая функция, вызывающая нашу измененную функцию, тоже должна быть изменена с перехватом нового ис- ключения или присоединением соответствующей секции throws к ее сигнатуре .
    И так до бесконечности . В итоге мы имеем каскад изменений, пробивающихся с нижних уровней программного продукта на верхние уровни! При этом нару- шается инкапсуляция, потому что все функции на пути инициирования должны располагать подробной информацией об этом низкоуровневом исключении . Учи- тывая, что главной целью исключений является возможность обработки ошибок
    «на расстоянии», такое нарушение инкапсуляции проверяемыми исключениями выглядит особенно постыдно .
    Проверяемые исключения иногда могут пригодиться при написании особо важных библиотек: программист обязан перехватить их . Но в общем случае разработки приложений проблемы, создаваемые зависимостями, перевешивают преимущества .
    135

    136
    Глава 7 . Обработка ошибок
    Передавайте контекст с исключениями
    Каждое исключение, инициируемое в программе, должно содержать достаточно контекстной информации для определения источника и местонахождения ошиб- ки . В Java из любого исключения можно получить данные трассировки стека; однако по трассировке невозможно узнать, с какой целью выполнялась операция, завершившаяся неудачей .
    Создавайте содержательные сообщения об ошибках и передавайте их со своими исключениями . Включайте в них сведения о сбойной операции и типе сбоя .
    Если в приложении ведется журнал, передайте информацию, достаточную для регистрации ошибки из секции catch
    Определяйте классы исключений
    в контексте потребностей
    вызывающей стороны
    Существует много способов классификации ошибок . Например, ошибки можно классифицировать по источнику, то есть по компоненту, в котором они прои- зошли . Также возможна классификация по типу: сбои устройств, сетевые сбои, ошибки программирования и т . д . Однако при определении классов исключений в при ложениях думать необходимо прежде всего о том, как они будут перехва-
    тываться .
    Рассмотрим пример неудачной классификации исключений . Далее приводится конструкция try
    - catch
    - finally для сторонней библиотечной функции . Она учи- тывает все исключения, которые могут быть инициированы при вызовах:
    ACMEPort port = new ACMEPort(12);
    try {
    port.open();
    } catch (DeviceResponseException e) {
    reportPortError(e);
    logger.log("Device response exception", e);
    } catch (ATM1212UnlockedException e) {
    reportPortError(e);
    logger.log("Unlock exception", e);
    } catch (GMXError e) {
    reportPortError(e);
    logger.log("Device response exception");
    } finally {

    }
    Конструкция содержит множество повторений, и это неудивительно . В боль- шинстве ситуаций при обработке исключений выполняются относительно стан-
    136

    Определяйте классы исключений в контексте потребностей
    137
    дартные действия, не зависящие от их реальной причины . Мы должны сохранить ошибку и убедиться в том, что работа программы может быть продолжена . В этом случае, поскольку выполняемая работа остается более или менее постоянной независимо от исключения, код можно существенно упростить — для этого мы создаем «обертку» для вызываемой функции API и обеспечиваем возвращение стандартного типа исключения:
    LocalPort port = new LocalPort(12);
    try {
    port.open();
    } catch (PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(), e);
    } finally {

    }
    Класс
    LocalPort представляет собой простую обертку, которая перехватывает и преобразует исключения, инициированные классом
    ACMEPort
    :
    public class LocalPort {
    private ACMEPort innerPort;
    public LocalPort(int portNumber) {
    innerPort = new ACMEPort(portNumber);
    }
    public void open() {
    try {
    innerPort.open();
    } catch (DeviceResponseException e) {
    throw new PortDeviceFailure(e);
    } catch (ATM1212UnlockedException e) {
    throw new PortDeviceFailure(e);
    } catch (GMXError e) {
    throw new PortDeviceFailure(e);
    }
    }

    }
    Обертки — вроде той, которую мы определили для
    ACMEPort
    , — бывают очень по- лезными . Более того, инкапсуляция вызовов сторонних API принадлежит к числу стандартных приемов . Создавая обертку для стороннего вызова, вы сокращаете до минимума зависимость от него в своем коде: в будущем вы можете переклю- читься на другую библиотеку без сколько-нибудь заметных проблем . Обертки также упрощают имитацию сторонних вызовов в ходе тестирования кода .
    Последнее преимущество оберток заключается в том, что вы не ограничиваетесь архитектурными решениями разработчика API . Вы можете определить тот API, который вам удобен . В предыдущем примере мы определили для всех сбоев порта один тип исключения, и код от этого стал намного чище .
    137

    138
    Глава 7 . Обработка ошибок
    Часто в определенной области кода бывает достаточно одного класса исключения .
    Информация, передаваемая с исключением, позволяет различать разные виды ошибок . Используйте разные классы исключений только в том случае, если вы намерены перехватывать одни исключения, разрешая прохождение других типов .
    Определите нормальный путь выполнения
    Выполнение рекомендаций из преды- дущих разделов обеспечивает хорошее разделение бизнес-логики и кода обра- ботки ошибок . Основной код програм- мы начинает выглядеть как простой ал- горитм, не отягощенный посторонними вставками . Однако в результате код об- наружения ошибок смещается на пери- ферию вашей программы . Вы создаете обертки для внешних API, чтобы иметь возможность инициировать собственные исключения, и определяете обработчик, который находится над основным кодом и позволяет справиться с любым пре- рыванием вычислений . Обычно такое решение отлично работает, но в некоторых ситуациях прерывание нежелательно .
    Рассмотрим конкретный пример . В следующем, довольно неуклюжем фрагменте суммируются командировочные расходы на питание:
    try {
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
    } catch(MealExpensesNotFound e) {
    m_total += getMealPerDiem();
    }
    Если работник предъявил счет по затратам на питание, то сумма включается в общий итог . Если счет отсутствует, то работнику за этот день начисляется определенная сумма . Исключение загромождает логику программы . А если бы удалось обойтись без обработки особого случая? Это позволило бы заметно упростить код:
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
    Можно ли упростить код до такой формы? Оказывается, можно . Мы можем из- менить класс
    ExpenseReportDAO
    , чтобы он всегда возвращал объект
    MealExpense
    . При отсутствии предъявленного счета возвращается объект
    MealExpense
    , у которого в качестве затрат указана стандартная сумма, начисляемая за день:
    public class PerDiemMealExpenses implements MealExpenses {
    public int getTotal() {
    // Вернуть стандартные ежедневные затраты на питание
    }
    }
    138

    Не возвращайте null
    139
    Такое решение представляет собой реализацию паттерна ОСОБЫЙ СЛУЧАЙ
    [Fowler] . Программист создает класс или настраивает объект так, чтобы он обра- батывал особый случай за него . Это позволяет избежать обработки исключитель- ного поведения в клиентском коде . Все необходимое поведение инкапсулируется в объекте особого случая .
    не возвращайте null
    На мой взгляд, при любых обсуждениях обработки ошибок необходимо упо- мянуть о неправильных действиях программистов, провоцирующих ошибки .
    На первом месте в этом списке стоит возвращение null
    . Я видел бесчисленное множество приложений, в которых едва ли не каждая строка начиналась с про- верки null
    . Характерный пример:
    public void registerItem(Item item) {
    if (item != null) {
    ItemRegistry registry = persistentStore.getItemRegistry();
    if (registry != null) {
    Item existing = registry.getItem(item.getID());
    if (existing.getBillingPeriod().hasRetailOwner()) {
    existing.register(item);
    }
    }
    }
    }
    Если ваша кодовая база содержит подобный код, возможно, вы не видите в нем ничего плохого, но это не так! Возвращая null
    , мы фактически создаем для себя лишнюю работу, а для вызывающей стороны — лишние проблемы . Стоит про- пустить всего одну проверку null
    , и приложение «уходит в штопор» .
    А вы заметили, что во второй строке вложенной команды if проверка null от- сутствует? Что произойдет во время выполнения, если значение persistentStore окажется равным null
    ? Произойдет исключение
    NullPointerException
    ; либо кто-то перехватит его на верхнем уровне, либо не перехватит . В обоих случаях все будет плохо . Как реагировать на исключение
    NullPointerException
    , возникшее где-то в глубинах вашего приложения?
    Легко сказать, что проблемы в приведенном коде возникли из-за пропущенной проверки null
    . В действительности причина в другом: этих проверок слишком
    много . Если у вас возникает желание вернуть null из метода, рассмотрите воз- можность выдачи исключения или возвращения объекта «особого случая» . Если ваш код вызывает метод стороннего API, способный вернуть null
    , создайте для него обертку в виде метода, который инициирует исключение или возвращает объект особого случая .
    Довольно часто объекты особых случаев легко решают проблему . Допустим, у вас имеется код следующего вида:
    List employees = getEmployees();
    if (employees != null) {
    139

    1   ...   12   13   14   15   16   17   18   19   ...   49


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