Аспектно-ориентированное программирование Аспектно-ориентированное программирование (АОП) — парадигма программирования, основанная на идее разделения функциональности для улучшения разбиения программы на модули. Методология аспектно-ориентированного программирования была предложена группой инженеров исследовательского центра Xerox PARC под руководством Грегора Кичалеса (Gregor Kiczales). Ими же был разработан аспектно-ориентированное расширение для языка Java, получившее название AspectJ — (2001 год).
Существующие парадигмы программирования, такие как процедурное, модульное и объектно-ориентированное программирование, предоставляют определённые способы для разделения и выделения функциональности (функции, классы, модули), но некоторую функциональность с помощью предложенных методов невозможно выделить в отдельные сущности. Такую функциональность называют сквозной ( scattered, разбросанная или tangled, переплетённая), так как её реализация рассыпана по различным модулям программы. Сквозная функциональность приводит к рассредоточенному и запутанному коду, сложному для понимания и сопровождения.
Все языки АОП предоставляют средства для выделения сквозной функциональности в отдельную сущность. Так как AspectJ является родоначальником этого направления, используемые в этом расширении концепции распространились на большинство языков АОП. Основные понятия АОП:
• Аспект ( aspect) — модуль или класс, реализующий сквозную функциональность. Аспект изменяет поведение остального кода, применяя совет в точках соединения, определённых некоторым срезом.
• Совет ( advice) — средство оформления кода, который должен быть вызван из точки соединения. Совет может быть выполнен до, после или вместо точки соединения.
• Точка соединения ( join point) — точка в выполняемой программе, где следует применить совет. Многие реализации АОП позволяют использовать вызовы методов и обращения к полям объекта в качестве точек соединения.
• Срез ( pointcut) — набор точек соединения. Срез определяет, подходит ли данная точка соединения к данному совету. Самые удобные реализации АОП используют для определения срезов синтаксис основного языка (например, в AspectJ применяются Java-cигнатуры) и позволяют их повторное использование с помощью переименования и комбинирования.
• Внедрение ( introduction, введение) — изменение структуры класса и/или изменение иерархии наследования для добавления функциональности аспекта в инородный код. Обычно реализуется с помощью некоторого метаобъектного протокола ( metaobject protocol, MOP).
Ведение лога и обработка ошибок — типичные примеры сквозной функциональности. Другие примеры: трассировка; авторизация и проверка прав доступа; контрактное программирование (в частности, проверка пред- и постусловий). Для программы, написанной в парадигме ООП, любая функциональность, по которой не была проведена декомпозиция, является сквозной.
Однако как утверждают некоторые авторы, АОП может успешно применяться и для решения задач защиты, многопоточности, управления транзакциями и многих других.
Контрольные вопросы
1. Дайте понятие парадигмы программирования.
2. Может ли язык программирования поддерживать сразу несколько парадигм?
3. Охарактеризуйте процедурное программирование?
4. Охарактеризуйте функциональное программирование?
5. В чем особенность логического программирования?
6. Что такое автоматное программирование?
7. В чем особенность объектно-ориентированного программирования (ООП)?
8. Какой язык был первым в ООП и когда он появился?
9. Какое понятие является важнейшим в ООП?
10. Поясните основные принципы ООП?
11. Перечислите родственные ООП методологии.
12. От какого принципа отказались в прототипном программировании?
13. Какие особенности ООП приводят к снижению производительности программных систем?
14. Охарактеризуйте обязательный набор синтаксических средств объектно-ориентированного языка программирования.
15. В чем заключается аспектно-ориентированное программирование?
Глава 12. Эффективность и оптимизация программ
Общие понятия эффективности
Основной задачей программирования является создание правильных, а не эффективных программ. Эффективная программа не нужна, если она не обеспечивает правильных результатов. Это правило Ван Тассела. Эффективная, но неправильная программа редко может быть сделана правильной, в то время как правильную, хотя и неэффективную программу можно оптимизировать и сделать эффективной. Поэтому оптимизация является вторым этапом программирования. Первый этап — получение правильной программы.
Наиболее разумный подход к программированию заключается в создании программы наилучшим возможным способом, не уделяя особого внимания эффективности. Затем, если
- программа в таком виде пригодна,
- если она нужна для работы,
- если ее будут выполнять многократно и
- если статус проекта и фирмы позволяет,
тогда и только тогда следует рассмотреть возможность ее оптимизации.
Обычно большая часть времени расходуется на выполнение очень небольшой части программы (<~5% ее объема), называемой критической областью. Как правило, только критическая область объектной программы оптимизируется программистом вручную. Погоня за эффективностью часто ведет к злоупотреблению. Замечено, что программисты тратят огромное количество времени, думая и беспокоясь о работе некритических областей программы. Современные компьютеры отличаются высоким быстродействием и очень мала разница во времени выполнения программы, если некоторые, редко выполняемые операторы удается сделать эффективными. Экономию можно получить только за счет многократно выполняемых циклов.
Некоторые программисты считают архаичной задачу написания эффективной программы. Это справедливо только в отношении небольших программ, для выполнения которых используются машины с высоким быстродействием и большим объемом памяти. Что же касается больших программ, то еще на стадии проектирования определяются требуемые параметры, включающие время и емкость памяти. Для экономических задач емкость памяти часто более критичный параметр, чем время. Создатель системного программного обеспечения должен установить необходимые объем памяти и производительность каждого модуля, особенно при создании больших программных проектов. Требования к эффективности программы определяются на стадии проектирования.
Существуют три типа программ, и для каждого из них эффективность должна быть различной.
К первому типу относятся часто используемые программы. Это операционные системы, компиляторы, прикладные подпрограммы и системы резервирования авиабилетов. Для этих программ эффективность является первостепенной задачей вследствие их частого использования и специфического выполнения.
Второй тип составляют производственные программы, используемые длительное время. Этот тип программ пишут профессиональные программисты. Хотя эффективность таких программ существенна, обычно еще больше внимания уделяют их эксплуатационным характеристикам.
Третий тип программ—программы, созданные не программистами, а научными работниками или администраторами. Время для этих людей важнее всего. Здесь эффективность имеет значение только для программ, которые должны уместиться в заданном объеме памяти и выполняться за приемлемое время.
Следовательно, еще до написания программы необходимо установить, насколько эффективной она должна быть. Очевидно, что следует модифицировать только те программы, которые выполняются многократно. Программисты, «экономящие на спичках», сокращают на 10 мкс время выполнения редко используемой программы, затрачивая при этом 2 ч на программирование и много минут на компилирование и тестирование. Очевидно, что в этом случае вы ничего не сэкономите. Зато, как и при любом изменении программы, можете добавить в нее ошибки. Однако человеческая натура такова, что эффективность программ всегда будет вызывать интерес.
Многие методы, делающие программу эффективной, не наносят ущерба ее удобочитаемости. Эти методы следует использовать всегда. Но некоторые меры по повышению эффективности могут быть просто вредными для получения удобочитаемой программы. Удобочитаемость программы более существенна, чем ее эффективность. Дело в том, что удобочитаемую программу легче отлаживать, модифицировать и использовать. А всякую большую программу обычно изменяет, модифицирует и применяет совсем не тот человек, который ее писал. Лишь в особых случаях программу следует делать более эффективной: программа: либо не помещается в памяти, либо слишком долго выполняется. Или же программа должна быть включена в библиотеку и часто использоваться. В этом случае эффективность становится очень важным фактором и ей отдают предпочтение в ущерб удобочитаемости.
Оптимизирующие компиляторы
Эффективность важна на двух стадиях разработки программы: компилирования и выполнения. Если компилятор работает быстро, то он обычно составляет программу, которая выполняется медленно. Компиляторы, создающие эффективную объектную программу, обычно бывают большими и работают медленно, так как оптимизируют объектную программу.
Некоторые современные компиляторы позволяют пользователю выбрать ресурс, который нужно оптимизировать. Пользователь может потребовать, чтобы был минимизирован размер памяти, необходимый для выполнения программы, либо время выполнения. Оптимизация одного ресурса выполняется за счет другого.
К сожалению, об увеличении скорости компилирования можно сказать немного. Некоторые программные ухищрения могут сократить время компилирования, но они либо тривиальны, либо сильно зависят от компилятора. Методы, дающие положительный результат при использовании в одном компиляторе, не дают тех же улучшений в другом компиляторе. Некоторые компиляторы работают более эффективно, если длины имен переменных распределены равномерно. Другие компиляторы более эффективны, если метки операторов равномерно распределены по последнему символу. Естественно, что исключение неиспользуемых меток и выражений также уменьшает время компилирования любого компилятора. Наличие меток препятствует некоторым типам оптимизации, поэтому неиспользуемые метки ухудшают эффективность объектной программы. При повторном прогоне программы следует исправить все ошибки в исходной программе, а число предупреждающих диагностических сообщений должно минимизироваться каждый раз, когда это возможно.
Описанные методы оптимизации не зависят от машины и применяемого языка и пригодны для оптимизации времени выполнения и минимизации объема памяти компилируемых программ. Эти методы машинно-независимы, так как улучшения, сделанные в программе с их помощью, приведут к ускорению работы программы на разных машинах. Методы не зависят от языка (за исключением методов, специфичных для определенного языка) в том смысле, что они применимы в общем случае для языков высокого уровня. Некоторые из этих методов при их реализации на одних машинах будут давать более заметные результаты, чем на других. Даже различные модели одной и той же машины могут иметь разные наборы команд ассемблера, которые обусловят заметную разницу в оптимизации.
Некоторые компиляторы оптимизируют выполнение программы. Имеются два типа такой оптимизации: машинно-зависимая и машинно-независимая.
К первому типу относятся способы, результат применения которых зависит от используемой машины. Как правило, эти способы оптимизации обычно не известны или не понятны программистам на уровне входного языка. Они состоят из способов обработки индексов, назначения регистров и анализа машинных команд.
Второй тип оптимизации — машинно-независимая оптимизация,— которая выполняется на уровне входного языка. Хотя компилятор может оптимизировать программу, но обычно у программиста большие возможности для этого. Многие способы оптимизации может сделать только программист, так как они требуют знания логики программы. Некоторые способы оптимизации, выполняемые компилятором, могли бы быть применены, но не реализуются просто потому, что требуют слишком много машинного времени. Таким образом, программисты, создающие программы, могут сделать очень много для оптимизации своих программ.
Использование обсуждавшихся здесь способов оптимизации не исключает необходимости в оптимизирующем компиляторе, так как машинно-зависимая оптимизация редко предусматривается на уровне исходной программы. Кроме того, даже наилучшим образом оптимизированная человеком исходная программа будет улучшена оптимизирующим компилятором.
Оптимизация программ
Часто возникает необходимость в оптимизации некоторой рабочей программы, потому что, либо она выполняется слишком долго, либо для нее требуется слишком большой объем памяти. Вполне возможно, что при создании программы не думали о том, часто ли ее будут использовать, и не позаботились о ее эффективности. Или в результате значительной модификации программа стала неэффективной, а теперь она используется довольно часто и занимает слишком много машинного времени или, возможно, близка к превышению объема памяти, имеющегося в ее распоряжении. Поэтому нужно попытаться сделать программу более эффективной.
Если программу следует оптимизировать, необходимо прежде всего тщательно проверить алгоритм. Основными критериями при этом должны являться время и объем памяти, используемые программой. Если оптимизация старого алгоритма не дает желаемого результата, тогда, возможно, следует выбрать другой алгоритм. В дальнейшем предполагается, что первоначальный алгоритм обоснован и целесообразен, однако программисты, которым нужно оптимизировать свою программу, никогда не должны делать такого предположения.
Сегментация программ
Программу, подлежащую оптимизации, следует разделить на подпрограммы. Оптимизация значительно облегчается, если программа уже разделена на подпрограммы в соответствии с принципами структурного программирования. Как только подпрограммы выделены, следует ответить на три вопроса:
- Какой процент общего времени использует каждая подпрограмма?
- Насколько (в процентном выражении) оптимизируется каждая подпрограмма?
- Сколько человеко-часов необходимо для достижения этой цели?
Каждый из этих вопросов подробно обсуждается далее.
Время работы подпрограмм
После деления программы на подпрограммы следует определить процент времени, используемый каждой подпрограммой. Это необходимо сделать для того, чтобы узнать, какие части программы расходуют больше всего времени.
Если для определения времени работы каждой подпрограммы используются оценки и предположения, то часто возникают ошибки и оптимизировать программу не удается. Поверхностные исследования приведут к тому, что для оптимизации будут выбраны не те подпрограммы. После того как установлено фактическое время работы, подпрограмма, которая используется больше других, должна оптимизироваться в первую очередь. Предположим, что программа разделена на четыре подпрограммы и время их выполнения составляет для
подпрограммы А — 5%, подпрограммы С — 15%,
подпрограммы В — 60%, подпрограммы В — 20%.
Очевидно, что, даже если подпрограмму А исключить совсем (что, вообще говоря, невозможно), мы смогли бы сэкономить только 5% общего времени работы программы. Таким образом, попытку оптимизации, вероятно, следует предпринять в первую очередь в отношении подпрограммы В, где возможна наибольшая экономия.
Если нельзя получить фактическое время выполнения каждой подпрограммы, применяется другой подход, заключающийся в подсчете количества операторов в подпрограмме, используя листинг программы на входном языке высокого уровня. Операторы, включенные в тело цикла, следует учитывать многократно. Подсчет количества операторов — достаточно надежный показатель времени, требуемого для каждой подпрограммы, но определение фактического времени работы подпрограммы гораздо лучше.
Большинство программ имеет одну критическую точку, которая использует большую часть времени выполнения. Нередко какая-либо малая часть программы расходует более 50% времени выполнения. Очевидно, что эту часть программы следует оптимизировать в первую очередь.
После оптимизации первой критической точки хорошо проанализировать программу еще раз, чтобы найти теперь уже другую критическую точку, которую также следует оптимизировать. Этот процесс можно повторять до тех пор, пока будут получены значительные результаты.
|