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

  • T2: Используйте средства анализа покрытия кода

  • T3: не пропускайте тривиальные тесты Тривиальные тесты пишутся легко, а их информативная ценность превышает затраты времени на их создание .354 Тесты 355

  • T4: Отключенный тест как вопрос

  • T5: тестируйте граничные условия

  • T6: тщательно тестируйте код рядом с ошибками

  • T7: Закономерности сбоев часто несут полезную информацию

  • T8: Закономерности покрытия кода часто несут полезную информацию

  • Пример приложения «клиент/сервер»

  • ВыЧИСленИе ВОЗМОжных ВАРИАнтОВ уПОРяДОЧенИя

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


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница39 из 49
    1   ...   35   36   37   38   39   40   41   42   ...   49
    354
    Глава 17 . Запахи и эвристические правила
    n7: Имена должны описывать
    побочные эффекты
    Имена должны описывать все, что делает функция, переменная или класс . Не скрывайте побочные эффекты за именами . Не используйте простые глаголы для описания функции, которая делает что-то помимо этой простой операции . Для примера возьмем следующий код из TestNG:
    public ObjectOutputStream getOos() throws IOException {
    if (m_oos == null) {
    m_oos = new ObjectOutputStream(m_socket.getOutputStream());
    }
    return m_oos;
    }
    Функция не ограничивается простым получением m_oos
    ; она создает объект m_oos
    , если он не был создан ранее . Таким образом, эту функцию было бы правильнее назвать createOrReturnOos
    тесты
    T1: нехватка тестов
    Сколько тестов должен включать тестовый пакет? К сожалению, многие про- граммисты руководствуются принципом «Пожалуй, этого хватит» . Тестовый пакет должен тестировать все, что может сломаться . Если в системе остались условия, не проверенные тестами, или вычисления, правильность которых не подтверждена, значит, количество тестов недостаточно .
    T2: Используйте средства анализа
    покрытия кода
    Средства анализа покрытия сообщают о пробелах в вашей стратегии тестирова- ния . Они упрощают поиск модулей, классов и функций с недостаточно полным тестированием . Во многих IDE используются визуальные обозначения: строки, покрытые тестами, выделяются зеленым цветом, а непокрытые — красным . Это позволяет легко и быстро обнаружить команды if или catch
    , тело которых не проверяется тестами .
    T3: не пропускайте тривиальные тесты
    Тривиальные тесты пишутся легко, а их информативная ценность превышает затраты времени на их создание .
    354

    Тесты
    355
    T4: Отключенный тест как вопрос
    Иногда мы не уверены в подробностях поведения системы, потому что неясны сами требования к программе . Вопрос о требованиях можно выразить в виде теста — закомментированного или помеченного аннотацией
    @Ignore
    . Выбор за- висит от того, компилируется или нет код, к которому относится неопределен- ность .
    T5: тестируйте граничные условия
    Особенно тщательно тестируйте граничные условия . Программисты часто правильно реализуют основную часть алгоритма, забывая о граничных ситуа- циях .
    T6: тщательно тестируйте код
    рядом с ошибками
    Ошибки часто собираются группами . Если вы обнаружили ошибку в функции, особенно тщательно протестируйте эту функцию . Может оказаться, что ошибка была не одна .
    T7: Закономерности сбоев часто несут
    полезную информацию
    Иногда анализ закономерностей в сбоях тестовых сценариев помогает выявить причины возникших проблем . Это еще один аргумент в пользу максимальной полноты тестовых сценариев . Всесторонние наборы тестовых сценариев, упо- рядоченные логичным образом, выявляют закономерности .
    Простой пример: вы заметили, что все тесты с входными данными, длина которых превышает пять символов, завершаются неудачей? Или что любой тест, который передает во втором аргументе функции отрицательное число, не проходит? Ино- гда простая закономерность чередования красного и зеленого в тестовом отчете заставляет нас воскликнуть «Ага!» на пути к правильному решению . Интересный пример такого рода приведен на с . 303 .
    T8: Закономерности покрытия кода часто несут
    полезную информацию
    Анализ того, какой код выполняется или не выполняется в ходе тестирования, иногда подсказывает причины возможных сбоев в ходе тестирования .
    355

    356
    Глава 17 . Запахи и эвристические правила
    T9: тесты должны работать быстро
    Медленный тест не выполняется за разумное время . Если время поджимает, из тестового пакета первыми будут удалены медленные тесты . Сделайте все необ- ходимое для того, чтобы ваши тесты работали быстро .
    Заключение
    Приведенный список эвристических правил и «запахов» не претендует на пол- ноту . Я вообще не уверен в том, можно ли составить полный список такого рода .
    Но вероятно, стопроцентная полнота здесь и не нужна, поскольку список всего лишь дает косвенное представление о системе ценностей .
    Именно эта система ценностей является нашей целью — и основной темой кни- ги . Невозможно написать чистый код, действуя по списку правил . Нельзя стать мастером, изучив набор эвристик . Профессионализм и мастерство формируются на основе ценностей, которыми вы руководствуетесь в обучении .
    литература
    [Refactoring]: Refactoring: Improving the Design of Existing Code, Martin Fowler et al ., Addison-Wesley, 1999 .
    [PRAG]: The Pragmatic Programmer, Andrew Hunt, Dave Thomas, Addison-Wesley,
    2000 .
    [GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al ., Addison-Wesley, 1996 .
    [Beck97]: Smalltalk Best Practice Patterns, Kent Beck, Prentice Hall, 1997 .
    [Beck07]: Implementation Patterns, Kent Beck, Addison-Wesley, 2008 .
    [PPP]: Agile Software Development: Principles, Patterns, and Practices, Robert
    C . Martin, Prentice Hall, 2002 .
    [DDD]: Domain Driven Design, Eric Evans, Addison-Wesley, 2003 .
    356

    Многопоточность II
    Бретт Л. Шухерт
    Данное приложение дополняет и углубляет материал главы «Многопоточность», с . 207 . Оно написано в виде набора независимых разделов, которые можно читать в произвольном порядке . Чтобы такое чтение было возможно, материал разделов частично перекрывается .
    Пример приложения «клиент/сервер»
    Представьте простое приложение «клиент/сервер» . Сервер работает в режиме ожидания, прослушивая сокет на предмет клиентских подключений . Клиент подключается и отправляет запросы .
    Сервер
    Ниже приведена упрощенная версия серверного приложения . Полный исходный код примера приводится, начиная со с . 385 .
    ServerSocket serverSocket = new ServerSocket(8009);
    while (keepProcessing) {
    try {
    Socket socket = serverSocket.accept();
    process(socket);
    } catch (Exception e) {
    handle(e);
    }
    }
    Приложение ожидает подключения, обрабатывает входящее сообщение, а затем снова ожидает следующего клиентского запроса . Код клиента для подключения к серверу выглядит так:
    private void connectSendReceive(int i) {
    try {
    Socket socket = new Socket("localhost", PORT);
    MessageUtils.sendMessage(socket, Integer.toString(i));
    MessageUtils.getMessage(socket);
    socket.close();
    А
    357

    358
    Приложение А . Многопоточность II
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    Как работает пара «клиент/сервер»? Как формально описать ее производитель- ность? Следующий тест проверяет, что производительность является «прием- лемой»:
    @Test(timeout = 10000)
    public void shouldRunInUnder10Seconds() throws Exception {
    Thread[] threads = createThreads();
    startAllThreadsw(threads);
    waitForAllThreadsToFinish(threads);
    }
    Чтобы по возможности упростить пример, я исключил из него подготовительный код (см . «ClientTest .java», с . 387) . Тест предполагает, что обработка должна быть завершена за 10 000 миллисекунд .
    Перед нами классический пример оценки производительности системы . Систе- ма должна завершить обработку серии клиентских запросов за 10 секунд . Если сервер сможет обработать все клиентские запросы за положенное время, то тест пройдет .
    А что делать, если тест завершится неудачей? В однопоточной модели практиче- ски невозможно как-то ускорить обработку запросов (если не считать реализации цикла опроса событий) . Сможет ли многопоточная модель решить проблему?
    Может, но нам необходимо знать, в какой области расходуется основное время выполнения . Возможны два варианта:
    
    Ввод/вывод – использование сокета, подключение к базе данных, ожидание подгрузки из виртуальной памяти и т . д .
    
    Процессор – числовые вычисления, обработка регулярных выражений, уборка мусора и т . д .
    В системах время обычно расходуется в обеих областях, но для конкретной операции одна из областей является доминирующей . Если код в основном ориентирован на обработку процессором, то повышение вычислительной мощ- ности способно улучшить производительность и обеспечить прохождение теста .
    Однако количество процессорных тактов все же ограничено, так что реализация многопоточной модели в процессорно-ориентированных задачах не ускорит их выполнения .
    С другой стороны, если значительное время в выполняемом процессе расходу- ется на операции ввода/вывода, то многопоточная модель способна повысить эффективность работы . Пока одна часть системы ожидает ввода/вывода, другая часть использует время ожидания для выполнения других действий, обеспечивая более эффективное использование процессорного времени .
    358

    Пример приложения «клиент/сервер»
    359
    Реализация многопоточности
    Допустим, тест производительности не прошел . Как повысить производитель- ность и обеспечить прохождение теста? Если серверный метод process ориен- тирован на ввод/вывод, одна из возможных реализаций многопоточной модели выглядит так:
    void process(final Socket socket) {
    if (socket == null)
    return;
    Runnable clientHandler = new Runnable() {
    public void run() {
    try {
    String message = MessageUtils.getMessage(socket);
    MessageUtils.sendMessage(socket, "Processed: " + message);
    closeIgnoringException(socket);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    };
    Thread clientConnection = new Thread(clientHandler);
    clientConnection.start();
    }
    Допустим, в результате внесенных изменений тест проходит
    1
    ; задача решена, верно?
    Анализ серверного кода
    На обновленном сервере тест успешно завершается всего за одну с небольшим секунду . К сожалению, приведенное решение наивно, и оно создает ряд новых проблем .
    Сколько потоков может создать наш сервер? Код не устанавливает ограничений, поэтому сервер вполне может столкнуться с ограничениями, установленными виртуальной машиной Java ( JVM) . Для многих простых систем этого достаточно .
    А если система обслуживает огромное количество пользователей в общедоступ- ной сети? При одновременном подключении слишком многих пользователей система «заглохнет» .
    Но давайте ненадолго отложим проблемы с поведением . Чистота и структура представленного кода тоже оставляют желать лучшего . Какие ответственности возложены на серверный код?
    
    Управление подключением к сокетам .
    
    Обработка клиентских запросов .
    1
    Вы можете убедиться в этом сами, тестируя код до и после внесения изменений . Однопо- точный код приведен на с . 385, а многопоточный – на с . 389 .
    359

    360
    Приложение А . Многопоточность II
    
    Политика многопоточности .
    
    Политика завершения работы сервера .
    К сожалению, все эти ответственности реализуются кодом функции process
    Кроме того, код распространяется на несколько разных уровней абстракции .
    Следовательно, какой бы компактной ни была функция process
    , ее все равно не- обходимо разбить на несколько меньших функций .
    У серверного кода несколько причин для изменения; следовательно, он нарушает принцип единой ответственности . Чтобы код многопоточной системы оставался чистым, управление потоками должно быть сосредоточено в нескольких хорошо контролируемых местах . Более того, код управления потоками не должен делать ничего другого . Почему? Да хотя бы потому, что отслеживать проблемы многопо- точности достаточно сложно и без параллельного отслеживания других проблем, не имеющих ничего общего с многопоточностью .
    Если создать отдельный класс для каждой из ответственностей, перечисленных выше (включая управление потоками), то последствия любых последующих изменений стратегии управления потоками затронут меньший объем кода и не будут загрязнять реализацию других обязанностей . Кроме того, такое разбиение упростит тестирование других модулей, так как вам не придется отвлекаться на многопоточные аспекты . Обновленная версия кода:
    public void run() {
    while (keepProcessing) {
    try {
    ClientConnection clientConnection = connectionManager.awaitClient();
    ClientRequestProcessor requestProcessor
    = new ClientRequestProcessor(clientConnection);
    clientScheduler.schedule(requestProcessor);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    connectionManager.shutdown();
    }
    Все аспекты, относящиеся к многопоточности, теперь собраны в объекте clientScheduler
    . Если в приложении возникнут многопоточные проблемы, ис- кать придется только в одном месте:
    public interface ClientScheduler {
    void schedule(ClientRequestProcessor requestProcessor);
    }
    Текущая политика реализуется легко:
    public class ThreadPerRequestScheduler implements ClientScheduler {
    public void schedule(final ClientRequestProcessor requestProcessor) {
    360

    Пример приложения «клиент/сервер»
    361
    Runnable runnable = new Runnable() {
    public void run() {
    requestProcessor.process();
    }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    }
    }
    Изоляция всего многопоточного кода существенно упрощает внесение любых изменений в политику управления потоками . Например, чтобы перейти на ин- фраструктуру Java 5 Executor, достаточно написать новый класс и подключить его к существующему коду (листинг А .1) .
    листинг А .1 . ExecutorClientScheduler.java import java.util.concurrent.Executor;
    import java.util.concurrent.Executors;
    public class ExecutorClientScheduler implements ClientScheduler {
    Executor executor;
    public ExecutorClientScheduler(int availableThreads) {
    executor = Executors.newFixedThreadPool(availableThreads);
    }
    public void schedule(final ClientRequestProcessor requestProcessor) {
    Runnable runnable = new Runnable() {
    public void run() {
    requestProcessor.process();
    }
    };
    executor.execute(runnable);
    }
    }
    Заключение
    Применение многопоточной модели в этом конкретном примере показывает, как повысить производительность системы, а также демонстрирует методологию проверки изменившейся производительности посредством тестовой инфра- структуры . Сосредоточение всего многопоточного кода в небольшом количестве классов – пример практического применения принципа единой ответственности .
    В случае многопоточного программирования это особенно важно из-за его не- тривиальности .
    361

    362
    Приложение А . Многопоточность II
    Возможные пути выполнения
    Рассмотрим код incrementValue однострочного метода Java, не содержащего ци- клов или ветвления:
    public class IdGenerator {
    int lastIdUsed;
    public int incrementValue() {
    return ++lastIdUsed;
    }
    }
    Забудем о возможности целочисленного переполнения . Будем считать, что толь- ко один программный поток имеет доступ к единственному экземпляру
    IdGenera- tor
    . В этом случае существует единственный путь выполнения с единственным гарантированным результатом:
    
    Возвращаемое значение равно значению lastIdUsed
    , и оба значения на одну единицу больше значения lastIdUsed непосредственно перед вызовом метода .
    Что произойдет, если мы используем два программных потока, а метод оста- нется неизменным? Какие возможны результаты, если каждый поток вызовет incrementValue по одному разу? Сколько существует возможных путей вы- полнения? Начнем с результатов (допустим, lastIdUsed начинается со значе- ния 93):
    
    Поток 1 получает значение 94, поток 2 получает значение 95, значение last-
    IdUsed равно 95 .
    
    Поток 1 получает значение 95, поток 2 получает значение 94, значение lastIdUsed равно 95 .
    
    Поток 1 получает значение 94, поток 2 получает значение 94, значение last-
    IdUsed равно 94 .
    Последний результат выглядит удивительно, но он возможен . Чтобы понять, как образуются эти разные результаты, необходимо разобраться в количестве возможных путей выполнения и в том, как они исполняются виртуальной ма- шиной Java .
    Количество путей
    Чтобы вычислить количество возможных путей выполнения, начнем со сгенери- рованного байт-кода . Одна строка кода Java (
    return ++lastIdUsed;
    ) преобразуется в восемь инструкций байт-кода . Выполнение этих восьми инструкций двумя по- токами может перемежаться подобно тому, как перемежаются карты в тасуемой колоде
    1
    . Хотя в каждой руке вы держите всего восемь карт, количество всевоз- можных перетасованных комбинаций чрезвычайно велико .
    1
    Это несколько упрощенное объяснение . Впрочем, для целей нашего обсуждения мы вос- пользуемся этой упрощенной моделью .
    362

    Пример приложения «клиент/сервер»
    363
    В простом случае последовательности из N команд, без циклов и условных пере- ходов, и T потоков, общее количество возможных путей выполнения будет равно
    (
    )!
    !
    T
    NT
    N
    ВыЧИСленИе ВОЗМОжных ВАРИАнтОВ уПОРяДОЧенИя
    Из сообщения электронной почты, отправленного Дядюшкой Бобом Бретту:
    При N шагов и T потоков общее количество в итоговой последовательности шагов равно T * N. Перед каждым шагом происходит переключение контекста, в ходе которого производится выбор между T потоками. Каждый путь может быть пред- ставлен в виде последовательности цифр, обозначающей переключение контек- стов. Так, для шагов A и B с потоками 1 и 2 возможны шесть путей: 1122, 1212,
    1221, 2112, 2121 и 2211. Если использовать в записи обозначения шагов, мы получаем A1B1A2B2, A1A2B1B2, A1A2B2B1, A2A1B1B2, A2A1B2B1 и A2B2A1B1. Для трех потоков последовательность вариантов имеет вид 112233, 112323, 113223,
    113232, 112233, 121233, 121323, 121332, 123132, 123123, . . . .
    Одно из свойств этих строк заключается в том, что каждый поток должен присут- ствовать в строке в N экземплярах. Таким образом, строка 111111 невозможна, потому что она содержит шесть экземпляров 1 и нуль экземпляров 2 и 3.
    Итак, нам нужно сгенерировать перестановки из N цифр 1, N цифр 2… и N цифр T.
    Искомое число равно числу перестановок из N * T объектов, то есть (N * T)!, но с удалением всех дубликатов. Таким образом, задача заключается в том, чтобы подсчитать дубликаты и вычесть их количество из (N * T )!.
    Сколько дубликатов содержит серия для двух шагов и двух потоков? Каждая строка из четырех цифр состоит из двух 1 и двух 2. Цифры каждой пары мож- но поменять местами без изменения смысла строки. Мы можем переставить две цифры 1 и/или две цифры 2. Таким образом, каждая строка существует в четырех изоморфных версиях; это означает, что у каждой строки имеются три дубликата.
    Три варианта из четырех повторяются, то есть только одна перестановка из че- тырех НЕ ЯВЛЯЕТСЯ дубликатом. 4! * 0.25 = 6. Похоже, наша схема рассуждений работает.
    Как вычислить количество дубликатов в общем случае? Для N = 2 и T = 2 можно переставить 1 и/или 2. Для N = 2 и T = 3 можно переставить 1, 2, 3, 1 и 2, 1 и 3 или 2 и 3. Количество вариантов равно количеству перестановок N. Допустим, существует P разных перестановок N. Количество разных вариантов размещения этих перестановок равно P**T.
    Таким образом, количество возможных изоморфных версий равно N!**T. Соот- ветственно, количество путей равно (T*N)!/(N!**T). Для исходного случая T = 2,
    N = 2 мы получаем 6 (24/4).
    Для N = 2 и T = 3 количество путей равно 720/8 = 90.
    Для N = 3 и T = 3 получается 9!/6^3 = 1680.
    В простейшем случае с одной строкой кода Java, эквивалентной восьми инструк- циям байт-кода, и двумя программными потоками общее количество возможных путей выполнения равно 12 870 . Если переменная lastIdUsed будет относиться
    363

    1   ...   35   36   37   38   39   40   41   42   ...   49


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