Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
203 image.dispose(); System.gc(); image = newImage; } Чтобы обеспечить чистоту системы, следует устранить незначительное дублиро- вание между методами scaleToOneDimension и rotate : public void scaleToOneDimension( float desiredDimension, float imageDimension) { if (Math.abs(desiredDimension - imageDimension) < errorThreshold) return; float scalingFactor = desiredDimension / imageDimension; scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f); replaceImage(ImageUtilities.getScaledImage( image, scalingFactor, scalingFactor)); } public synchronized void rotate(int degrees) { replaceImage(ImageUtilities.getRotatedImage(image, degrees)); } private void replaceImage(RenderedOp newImage) { image.dispose(); System.gc(); image = newImage; } В ходе выделения общности конструкций на этом микроскопическом уровне начинают проявляться нарушения принципа SRP . Таким образом, только что сформированный метод можно переместить в другой класс . Это расширяет видимость метода . Другой участник группы может найти возможность дальней- шего абстрагирования нового метода и его использования в другом контексте . Таким образом, принцип «повторного использования даже в мелочах» может привести к значительному сокращению сложности системы . Понимание того, как обеспечить повторное использование в мелочах, абсолютно необходимо для его обеспечения в большом масштабе . Паттерн ШАБЛОННЫЙ МЕТОД [GOF] относится к числу стандартных при- емов устранения высокоуровневого дублирования . Пример: public class VacationPolicy { public void accrueUSDivisionVacation() { // Код вычисления продолжительности отпуска // по количеству отработанных часов // ... // Код проверки минимальной продолжительности отпуска // по стандартам США // ... // Код внесения отпуска в платежную ведомость // ... } 203 204 Глава 12 . Формирование архитектуры public void accrueEUDivisionVacation() { // Код вычисления продолжительности отпуска // по количеству отработанных часов // ... // Код проверки минимальной продолжительности отпуска // по европейским стандартам // ... // Код внесения отпуска в платежную ведомость // ... } } Код accrueUSDivisionVacation и accrueEuropeanDivisionVacation в основном совпада- ет, если не считать проверки минимальной продолжительности . Этот фрагмент алгоритма изменяется в зависимости от типа работника . Для устранения этого очевидного дублирования можно воспользоваться паттер- ном ШАБЛОННЫЙ МЕТОД: abstract public class VacationPolicy { public void accrueVacation() { calculateBaseVacationHours(); alterForLegalMinimums(); applyToPayroll(); } private void calculateBaseVacationHours() { /* ... */ }; abstract protected void alterForLegalMinimums(); private void applyToPayroll() { /* ... */ }; } public class USVacationPolicy extends VacationPolicy { @Override protected void alterForLegalMinimums() { // Логика для США } } public class EUVacationPolicy extends VacationPolicy { @Override protected void alterForLegalMinimums() { // Логика для Европы } } Субклассы «заполняют пробел» в обобщенном алгоритме accrueVacation ; они предоставляют только ту информацию, которая различается в специализиро- ванных версиях алгоритма . Выразительность Большинству читателей доводилось работать с запутанным кодом . Многие из них создавали запутанный код сами . Легко написать код, понятный для нас самих, 204 Выразительность 205 потому что в момент его написания мы глубоко понимаем решаемую проблему . У других программистов, которые будут заниматься сопровождением этого кода, такого понимания не будет . Основные затраты программного проекта связаны с его долгосрочным сопрово- ждением . Чтобы свести к минимуму риск появления дефектов в ходе внесения изменений, очень важно понимать, как работает система . С ростом сложности системы разработчику приходится разбираться все дольше и дольше, а вероят- ность того, что он поймет что-то неправильно, только возрастает . Следовательно, код должен четко выражать намерения своего автора . Чем понятнее будет код, тем меньше времени понадобится другим программистам, чтобы разобраться в нем . Это способствует уменьшению количества дефектов и снижению затрат на сопровождение . Хороший выбор имен помогает выразить ваши намерения . Имя класса или функ- ции должно восприниматься «на слух», а когда читатель разбирается в том, что делает класс, это не должно вызывать у него удивления . Относительно небольшой размер функций и классов также помогает выразить ваши намерения . Компактным классам и функциям проще присваивать имена; они легко пишутся и в них легко разобраться . Стандартная номенклатура также способствует выражению намерений автора . В частности, передача информация и выразительность являются важнейшими целями для применения паттернов проектирования . Включение стандартных названий паттернов (например, КОМАНДА или ПОСЕТИТЕЛЬ) в имена клас- сов, реализующих эти паттерны, помогает кратко описать вашу архитектуру для других разработчиков . Хорошо написанные модульные тесты тоже выразительны . Они могут рассматри- ваться как разновидность документации, построенная на конкретных примерах . Читая код тестов, разработчик должен составить хотя бы общее представление о том, что делает класс . И все же самое важное, что можно сделать для создания выразительного кода — это постараться сделать его выразительным . Как только наш код заработает, мы обычно переходим к следующей задаче, не прикладывая особых усилий к тому, чтобы код легко читался другими людьми . Но помните: следующим человеком, которому придется разбираться в вашем коде, с большой вероятностью окажетесь вы сами . Так что уделите немного внимания качеству исполнения своего продукта . Не- много поразмыслите над каждой функцией и классом . Попробуйте улучшить имена, разбейте большие функции на меньшие и вообще проявите заботу о том, что вы создали . Неравнодушие — воистину драгоценный ресурс . 205 206 Глава 12 . Формирование архитектуры Минимум классов и методов Даже такие фундаментальные концепции, как устранение дубликатов, вырази- тельность кода и принцип единой ответственности, могут зайти слишком дале- ко . Стремясь уменьшить объем кода наших классов и методов, мы можем напло- дить слишком много крошечных классов и методов . Это правило рекомендует ограничиться небольшим количеством функций и классов . Многочисленность классов и методов иногда является результатом бессмыслен- ного догматизма . В качестве примера можно привести стандарт кодирования, который требует создания интерфейса для каждого без исключения класса . Или разработчиков, настаивающих, что поля данных и поведение всегда должны быть разделены на классы данных и классы поведения . Избегайте подобных догм, а в своей работе руководствуйтесь более прагматичным подходом . Наша цель — сделать так, чтобы система была компактной, но при этом одно- временно сохранить компактность функций и классов . Однако следует помнить, что из четырех правил простой архитектуры это правило обладает наименьшим приоритетом . Свести к минимуму количество функций и классов важно, одна- ко прохождение тестов, устранение дубликатов и выразительность кода все же важнее . Заключение Может ли набор простых правил заменить практический опыт? Нет, конечно . С другой стороны, правила, описанные в этой главе и в книге, представляют со- бой кристаллизованную форму многих десятилетий практического опыта ав- торов . Принципы простой архитектуры помогают разработчикам следовать по тому пути, который им пришлось бы самостоятельно прокладывать в течение многих лет . литература [XPE]: Extreme Programming Explained: Embrace Change, Kent Beck, Addison- Wesley, 1999 . [GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al ., Addison-Wesley, 1996 . 206 Многопоточность Бретт Л. Шухерт Объекты — абстракции для обработки данных . Программные потоки — абстракции для плани- рования . Джеймс О. Коплиен � Написать чистую многопоточную программу трудно — очень трудно . Гораздо проще писать код, выполняемый в одном программном потоке . Многопоточный 13 207 208 Глава 13 . Многопоточность код часто выглядит нормально на первый взгляд, но содержит дефекты на более глубоком уровне . Такой код работает нормально до тех пор, пока система не за- работает с повышенной нагрузкой . В этой главе мы поговорим о том, почему необходимо многопоточное программи- рование и какие трудности оно создает . Далее будут представлены рекомендации относительно того, как справиться с этими трудностями и как написать чистый многопоточный код . В завершение главы рассматриваются проблемы тестиро- вания многопоточного кода . Чистый многопоточный код — сложная тема, по которой вполне можно было бы написать отдельную книгу . В этой главе приводится обзор, а более подробный учебный материал содержится в приложении «Многопоточность II» на с . 357 . Если вы хотите получить общее представление о многопоточности, этой главы будет достаточно . Чтобы разобраться в теме на более глубоком уровне, читайте вторую главу . Зачем нужна многопоточность? Многопоточное программирование может рассматриваться как стратегия устра- нения привязок . Оно помогает отделить выполняемую операцию от момента ее выполнения . В однопоточных приложениях «что» и «когда» связаны так сильно, что просмотр содержимого стека часто позволяет определить состояние всего приложения . Программист, отлаживающий такую систему, устанавливает точку прерывания (или серию точек прерывания) и узнает состояние системы на мо- мент остановки . Отделение «что» от «когда» способно кардинально улучшить как производитель- ность, так и структуру приложения . Со структурной точки зрения многопоточное приложение выглядит как взаимодействие нескольких компьютеров, а не как один большой управляющий цикл . Такая архитектура упрощает понимание си- стемы и предоставляет мощные средства для разделения ответственности . Для примера возьмем «сервлет», одну из стандартных моделей веб-приложений . Такие системы работают под управлением веб-контейнера или контейнера EJB, который частично управляет многопоточностью за разработчика . Сервлеты вы- полняются асинхронно при поступлении веб-запросов . Разработчику сервера не нужно управлять входящими запросами . В принципе каждый выполняемый экземпляр сервлета существует в своем замкнутом мире, отделенном от всех остальных экземпляров сервлетов . Конечно, если бы все было так просто, эта глава стала бы ненужной . Изоляция, обеспечиваемая веб-контейнерами, далеко не идеальна . Чтобы многопоточный код работал корректно, разработчики сервлетов должны действовать очень вни- мательно и осторожно . И все же структурные преимущества модели сервлетов весьма значительны . 208 Зачем нужна многопоточность? 209 Но структура — не единственный аргумент для многопоточного программиро- вания . В некоторых системах действуют ограничения по времени отклика и про- пускной способности, требующие ручного кодирования многопоточных решений . Для примера возьмем однопоточный агрегатор, который получает информацию с многих сайтов и объединяет ее в ежедневную сводку . Так как система работает в однопоточном режиме, она последовательно обращается к каждому сайту, всегда завершая получение информации до перехода к следующему сайту . Ежедневный сбор информации должен занимать менее 24 часов . Но по мере добавления новых сайтов время непрерывно растет, пока в какой-то момент на сбор всех данных не потребуется более 24 часов . Однопоточной реализации приходится подолгу ожидать завершения операций ввода/вывода в сокетах . Для повышения произ- водительности такого приложения можно было бы воспользоваться многопо- точным алгоритмом, параллельно работающим с несколькими сайтами . Или другой пример: допустим, система в любой момент времени работает только с одним пользователем, обслуживание которого у нее занимает всего одну секун- ду . При малом количестве пользователей система оперативно реагирует на все запросы, но с увеличением количества пользователей растет и время отклика . Ни- кто не захочет стоять в очереди после 150 других пользователей! Время отклика такой системы можно было бы улучшить за счет параллельного обслуживания многих пользователей . Или возьмем систему, которая анализирует большие объемы данных, но выдает окончательный результат только после их полной обработки . Наборы данных могут обрабатываться параллельно на разных компьютерах . Мифы и неверные представления Итак, существуют весьма веские причины для использования многопоточности . Но как говорилось ранее, написать многопоточную программу трудно . Необхо- димо действовать очень осторожно, иначе в программе могут возникнуть крайне неприятные ситуации . С многопоточностью связан целый ряд распространенных мифов и неверных представлений . Многопоточность всегда повышает быстродействие . Действительно, многопоточность иногда повышает быстродействие, но только при относительно большом времени ожидания, которое могло бы эффективно использоваться другими потоками или процессорами . Написание многопоточного кода не изменяет архитектуру программы. На самом деле архитектура многопоточного алгоритма может заметно отличать- ся от архитектуры однопоточной системы . Отделение «что» от «когда» обычно оказывает огромное влияние на структуру системы . При работе с контейнером (например, веб-контейнером или EJB-контейнером) разбираться в проблемах многопоточного программирования не обязательно. 209 210 Глава 13 . Многопоточность В действительности желательно знать, как работает контейнер и как защититься от проблем одновременного обновления и взаимных блокировок, описанных позднее в этой главе . Несколько более объективных утверждений, относящихся к написанию много- поточного кода: Многопоточность сопряжена с определенными дополнительными затрата- ми — в отношении как производительности, так и написания дополнительного кода . Правильная реализация многопоточности сложна даже для простых задач . Ошибки в многопоточном коде обычно не воспроизводятся, поэтому они часто игнорируются как случайные отклонения 1 (а не как систематические дефекты, которыми они на самом деле являются) . Многопоточность часто требует фундаментальных изменений в стратегии проектирования . трудности Что же делает многопоточное программирование таким сложным? Рассмотрим тривиальный класс: public class X { private int lastIdUsed; public int getNextId() { return ++lastIdUsed; } } Допустим, мы создаем экземпляр X , присваиваем полю lastIdUsed значение 42, а затем используем созданный экземпляр в двух программных потоках . В обоих потоках вызывается метод getNextId() ; возможны три исхода: Первый поток получает значение 43, второй получает значение 44, в поле lastIdUsed сохраняется 44 . Первый поток получает значение 44, второй получает значение 43, в поле lastIdUsed сохраняется 44 . Первый поток получает значение 43, второй получает значение 43, поле las- tIdUsed содержит 43 . Удивительный третий результат 2 встречается тогда, когда два потока «перебива- ют» друг друга . Это происходит из-за того, что выполнение одной строки кода Java в двух потоках может пойти по разным путям, и некоторые из этих путей порождают неверные результаты . Сколько существует разных путей? Чтобы от- ветить на этот вопрос, необходимо понимать, как JIT-компилятор обрабатывает 1 Фазы Луны, космические лучи и т . д . 2 См . раздел «Копаем глубже» на с . 364 . 210 Защита от ошибок многопоточности 211 сгенерированный байт-код, и разбираться в том, какие операции рассматривают- ся моделью памяти Java как атомарные . В двух словах скажу, что в сгенерированном байт-коде приведенного фрагмента существует 12 870 разных путей выполнения 1 метода getNextId в двух программ- ных потоках . Если изменить тип lastIdUsed c int на long , то количество возмож- ных путей возрастет до 2 704 156 . Конечно, на большинстве путей выполнения вычисляются правильные результаты . Проблема в том, что на некоторых путях результаты будут неправильными . Защита от ошибок многопоточности Далее перечислены некоторые принципы и приемы, которые помогают защитить вашу систему от проблем многопоточности . Принцип единой ответственности Принцип единой ответственности (SRP) [PPP] гласит, что метод/класс/компо- нент должен иметь только одну причину для изменения . Многопоточные архи- тектуры достаточно сложны, чтобы их можно было рассматривать как причину изменения сами по себе, а следовательно, они должны отделяться от основного кода . К сожалению, подробности многопоточной реализации нередко встраива- ются в другой код . Однако разработчик должен учитывать ряд факторов: Код реализации многопоточности имеет собственный цикл разработки, мо- дификации и настройки . При написании кода реализации многопоточности возникают специфические сложности, принципиально отличающиеся от сложностей однопоточного кода (и часто превосходящие их) . Количество потенциальных сбоев в неверно написанном многопоточном коде достаточно велико и без дополнительного бремени в виде окружающего кода приложения . Рекомендация: отделяйте код, относящийся к реализации многопоточности, от остального кода 2 Следствие: ограничивайте область видимости данных Как было показано ранее, два программных потока, изменяющих одно поле обще- го объекта, могут мешать друг другу, что приводит к непредвиденному поведению . Одно из возможных решений — защита критической секции кода, в которой про- 1 См . раздел «Пути выполнения» на с . 262 . 2 См . раздел «Пример архитектуры «клиент/сервер»» на с . 357 . 211 |