Обычно, делая фотографии, вы не задумываетесь, как оформите их потом. Вы фотографируете просто потому, что хотите поймать момент. Скажем, одну из фотографий вы затем распечатали, потом решили поместить в рамку с необычным стеклом. Но позже вы могли бы поместить ту же фотографию в другую рамку, если бы захотели. Даже несмотря на то, что вы изменили рамку, картинка осталась той же, потому что вы просто что-то добавляли к ней, но не изменяли ее при этом.
В объектно-ориентированном программировании заимствовали похожую идею добавления поведения к другим объектам без потери их исходных особенностей, т. е. расширенный объект будет улучшенной версией того же самого класса (фото в рамке). Любое «улучшение» (рамка) может быть наложено и снято динамически. Мы называем этот паттерн проектирования Декоратором, так как декоратор может добавляться к другому декоратору или исходному объекту для расширения его свойств, оставив исходное поведение нетронутым.
В этой главе мы сначала обсудим концепции паттерна и когда его использовать. Затем мы обсудим, как воспользоваться преимуществами этого паттерна для проектирования серии фильтрующих изображения классов для объектов UIImage.
Что такое паттерн Декоратор?
Классический паттерн Декоратор содержит родительский абстрактный класс Component
, который объявляет некоторые общие операции для других конкретных компонентов. Абстрактный класс Component
может быть обернут в другой класс Decorator
. Decorator
содержит ссылку на другой Component
. ConcreteDecorator
определяет некоторое расширенное поведение для других похожих Component
-ов и Decorator
-ов и выполняет операцию включенного в него объекта Component
. Их отношения проиллюстрированы на рисунке 16-1.
Рисунок 16–1. Диаграмма классов паттерна Декоратор
Класс Component
определяет некий абстрактный метод operation
, который его конкретные классы переопределяют, чтобы иметь свои собственные специфические операции. Decorator
– это базовый класс, который определяет «декорирующее» поведение для расширения других экземпляров Component
(или Decorator
) за счет включения в объект Decorator
. Его метод operation
по умолчанию просто пересылает сообщение включенному component
. ConcreteDecoratorA
и ConcreteDecoratorB
переопределяют родительский operation
, чтобы добавить их собственное поведение к вызову метода operation
component
-а через использование super
. Если вам нужно расширить component
только один раз, тогда вы можете пренебречь базовым классом Decorator
и позволить ConcreteDecorator
-ам перенаправлять любые запросы напрямую к component
. Это похоже на формирование цепочки операций с добавлением одного поведения поверх другого, как проиллюстрировано на диаграмме объектов на рисунке 16–2.
Рисунок 16–2. Реализация паттерна Декоратор и его функциональность
Примечание. Паттерн Декоратор: Добавляет дополнительные возможности к объекту динамически. Декораторы предоставляют гибкую альтернативу наследованию для расширения функциональности.*
*Исходное определение, данное в книге «Паттерны проектирования» GoF (Addison Wesley, 1994).
Когда бы вы могли использовать паттерн Декоратор?
Есть три распространенные ситуации, в которых вы могли бы рассмотреть возможность использования этого паттерна:
- Вы хотите добавлять функциональность к отдельным объектам динамически и прозрачно без влияния на другие объекты.
- Вы хотите расширить поведение класса, с которым это делать было бы непрактично. Определение класса может быть скрыто и недоступно для наследования от него, или расширение поведения класса потребовало бы огромного количества подклассов для поддержания каждой комбинации возможностей.
- Расширенные возможности класса могут быть опциональными.
Изменение «шкуры» объекта в сравнении с изменением «внутренностей»
В предыдущем разделе мы обсуждали, как различные декораторы могут быть соединены во время выполнения за счет использования внутреннего встроенного компонента в каждом узле декоратора, как показано на рисунке 16–2. Здесь также проиллюстрировано, что каждый декоратор изменяет свой встроенный компонент извне, то есть просто изменяет оболочку объекта. Идея в том, что узел не знает, что его изменяет.
Однако когда каждый узел знает о других узлах, цепочка будет строиться в другом направлении, то есть изнутри. Такой паттерн называется Стратегия (смотри главу 19). Каждому узлу нужно содержать в себе набор различных API, чтобы подключиться к другому узлу стратегии. Визуальное представление этой концепции показано на рисунке 16–3.
Рисунок 16–3. Изменение «внутренностей» объектов с помощью паттерна Стратегия
Итоговые различия между изменением «шкуры» (декораторы) и «внутренностей» (стратегии) объекта показаны в таблице 16–1.
Таблица 16–1. Итоговая таблица различий между декораторами и стратегиями
Изменение «шкуры» (Декораторы) | Изменение «внутренностей» (Стратегии) |
Изменения снаружи | Изменения изнутри |
Отдельный узел не знает об изменениях | Отдельный узел знает предопределенные возможности изменений |
Создание фильтров для изображений UIImage
Фильтрация изображений – это процесс, с помощью которого мы можем изменять атрибуты изображений, такие, как цвета и геометрия. У нас может быть фильтр, который может изменить оттенок изображения, или Гауссовский фильтр для его затемнения, чтобы оно выглядело так, как будто оно не в фокусе. Мы можем даже применить некое 2D преобразование к нему, чтобы оно не выглядело плоским на поверхности. Есть много всевозможных фильтров, которые мы можем использовать, чтобы наложить некие «особые эффекты» на изображение. Множество фоторедакторов, таких, как Photoshop и GIMP, поставляются с большим количеством фильтров. Что в первую очередь делает фильтр изображений с помощью паттерна Декоратор?
Паттерн Декоратор – этот метод добавления нового поведения к объекту без изменения существующего поведения и интерфейса. Скажем, объект изображения содержит только интерфейс для управления атрибутами и ничего более. Мы хотим добавить что-то необычное к нему, типа трансформирующего фильтра, но не хотим модифицировать существующие интерфейсы, которые уже есть у объекта изображения. Что мы можем сделать, так это определить другой класс, который является таким же, как и объект изображения, но содержит ссылку на другой объект изображения, поведение которого нужно расширить. Новый класс содержит метод для рисования себя в графическом контексте. В его методе рисования он применяет алгоритм преобразования к включенной ссылке на изображение, рисует полностью картинку и возвращает результирующее изображение. Можно проиллюстрировать этот процесс как наложение еще одного слоя стекла поверх картинки. Изображению не нужно ничего знать о стекле, и когда мы смотрим на него, мы все еще считаем это картинкой. Само стекло может иметь какую-то окраску, волнистую структуру на поверхности или что-то другое, чтобы изображение выглядело по-другому. Если позже мы захотим применить другой слой или фильтр к изображению, тогда можем определить другой класс-фильтр, как в случае трансформации, с помощью которого мы можем применить тот же механизм для изменения изображения. Другие фильтры после трансформации могут подцепить результирующее изображение и продолжить процесс. Однако одну вещь важно отметить – изображение, передаваемое по цепочке фильтров, не обязательно должно быть всегда исходным, но оно обязано быть того же типа. Изображение, возвращаемое после фильтра трансформации, — это трансформированное изображение. Затем, когда оно передается в цветовой фильтр, возвращенное изображение будет с измененным цветом и трансформированным и т.д.
UIImage
в UIKit из фреймворка Cocoa Touch представляет объекты изображения. Сам класс UIImage
имеет довольно ограниченный интерфейс для манипулирования изображением. Нет ничего, кроме нескольких свойств изображения, таких, как размер, цвет и т. д. Мы расширим обычный объект изображения некоторыми возможностями, доступными во фреймворке Quartz 2D. Есть два подхода для реализации паттерна — подклассы и категории.
Реализация Декораторов через подклассы
В случае подхода, основанного на подклассах, структура будет похожа на исходный стиль паттерна, показанный на рисунке 16–1. Единственное различие в том, что конечным типом элемента будет категория UIImage
, а не его подкласс. Есть одна структурная проблема, которая не дает нам разделять тот же интерфейс, который реализует UIImage
. UIImage
является прямым потомком NSObject
и не более. Это своего рода конечный класс. Чтобы использовать какое-то подобие интерфейса “Component
” (как родительский интерфейс в диаграмме классов на рисунке 16–1), чтобы объединить и UIImage
, и классы фильтров вместе, нам нужно обходное решение. Сейчас у нас есть две проблемы:
- Нам нужно сделать наши классы фильтров такими же, как
UImage
, но уUIImage
нет высокоуровневого интерфейса для совместного разделения (наследование отUIImage
здесь не предполагается). - У
UIImage
есть несколько методов, связанных с рисованием контента в текущем контексте, таких, какdrawAsPatternInRect:
,drawAtPoint:
,drawAtPoint:blendMode:alpha:
,drawInRect:
иdrawInRect:blendMode:alpha:
. Если мы позволим классам фильтров реализовывать те же методы, то усложним все и можем не получить того результата, который хотим в соответствии с тем, как работает Quartz 2D. Мы вернемся к этому позже.
Что мы собираемся сделать? Прежде всего, конечно, нам нужен интерфейс для разделения UIImage
с группой классов фильтров, чтобы этот паттерн работал и оба типа классов могли разделять один и тот же базовый тип. Идея использования UIImage
в качестве высокоуровневого типа для этой цели (т. е. наследование от него) плоха, потому что тогда фильтры будет сложно использовать. Мы создаем интерфейс ImageComponent
в виде протокола как оптимальный базовый тип для всех. Но подождите минутку; разве мы не упомянули, что UIImage
не наследует ни от какого интерфейса, а просто прямо наследует NSObject
? Да, это так – вот где нам нужно творческое решение. Мы создадим категорию UIImage
, которая реализует ImageComponent
. Тогда компилятор будет знать, что UIImage
и ImageComponent
— родственники, и не будет ругаться. UIImage
даже не осведомлен, что у него теперь есть новый базовый тип. Только тем, кто будет использовать фильтры, нужно знать об этом.
Также мы не собираемся возиться с исходными draw*
методами, определенными в UIImage
, но как же мы тогда расширим возможности рисования в другом ImageComponent
? Мы скоро до этого дойдем.
Диаграмма классов, которая показывает их взаимосвязи, показана на рисунке 16–4.
Рисунок 16–4. Диаграмма классов различных фильтров изображений, которые реализуют паттерн Декоратор
Протокол ImageComponent
задает абстрактный интерфейс со всеми методами draw*
из класса UIImage
. Любой конкретный ImageComponent
и похожие декораторы должны уметь обрабатывать эти вызовы. Сообщение draw*
для экземпляра UIImage
позволит ему рисовать его содержимое в текущем графическом контексте. Каждый из методов может создавать также трансформации и другие эффекты в этом контексте. Таким образом, мы можем внедрить нашу собственную фильтрацию перед любой операцией draw*
.
Наш конкретный компонент здесь – это тип UIImage
, но мы не хотим создавать его подкласс просто, чтобы сделать его частью игры, поэтому мы определяем категорию для него. Категория UIImage (ImageComponent)
реализует протокол ImageComponent
. Поскольку все методы, объявленные в протоколе, уже есть в классе UIImage
, нам не нужно их реализовывать в категории. Категория в принципе ничего не делает, а только лишь сообщает компилятору, что это тоже разновидность ImageComponent
.
ImageFilter
– это как класс Decorator
на рисунке 16–1. Метод apply
класса ImageFilter
позволяет конкретным классам фильтров добавлять дополнительное поведение к базовому component_
. Вместо переопределения всех методов draw*
, чтобы внедрить действия фильтра, мы используем единственный метод (id) forwardingTargetForSelector:(SEL)aSelector
для обработки их всех. forwardingTargetForSelector:
определен в классе NSObject
, что позволяет подклассам возвращать альтернативного получателя для вызова селектора aSelector
. Экземпляр ImageFilter
сначала будет проверять, является ли aSelector
каким-то из сообщений draw*
. Если это так, то он пошлет себе сообщение apply
для внедрения дополнительного поведения перед возвратом component_
для вызова действий по умолчанию. Реализация метода apply
по умолчанию ничего не делает. Отсутствующая информация должна быть предоставлена подклассами. Этот подход гораздо проще, чем, если бы каждый класс фильтра реализовывал бы одинаковый механизм для расширения поведения.
И ImageTransformFilter
, и ImageShadowFilter
фокусируются на предоставлении собственных алгоритмов фильтрации путем переопределения метода apply
. Они наследуют от базового класса ImageFilter
, в котором есть ссылка на другой ImageComponent
в виде закрытой переменной component_
. Различные объекты ImageComponent
могут быть соединены во время выполнения, как продемонстрировано на рисунке 16–5.
Рисунок 16–5. Диаграмма объектов, показывающая, что на каждый ImageComponent есть ссылка в другом экземпляре ImageComponent на каждом уровне.
Правый конец этой цепочки – это исходное изображение, которое показано слева на рисунке 16–6. После того, как мы добавим его к anImageTransformFilter
, а затем добавим anImageTransformFilter
к anImageShadowFilter
, клиент получит что-то вроде правой картинки на рисунке 16–6. Каждый узел инкапсулирован в виде component_
другого экземпляра ImageComponent
. Можно провести аналогию с тем, как более крупная рыба проглатывает более мелкую. Очевидно, что клиент не знает никаких деталей декораторов, а просто получает ссылку на экземпляр того же старого типа UIImage
(в форме ImageComponent
, потому что UIImage
реализует ImageComponent
через категорию).
Рисунок 16–6. Исходное изображение и то же изображение после применения серии фильтров
Это захватывает. Давайте посмотрим, как можно набросать это в коде. Первое, на что мы взглянем, — это ImageComponent
, который объявлен как протокол в листинге 16–1.
Листинг 16–1. ImageComponent.h
@protocol ImageComponent <NSObject>
// Мы будем перехватывать эти
// методы UIImage и добавлять
// дополнительное поведение
@optional
- (void) drawAsPatternInRect:(CGRect)rect;
- (void) drawAtPoint:(CGPoint)point;
- (void) drawAtPoint:(CGPoint)point
blendMode:(CGBlendMode)blendMode
alpha:(CGFloat)alpha;
- (void) drawInRect:(CGRect)rect;
- (void) drawInRect:(CGRect)rect
blendMode:(CGBlendMode)blendMode
alpha:(CGFloat)alpha;
@end
Все методы draw*
объявлены как @optional
, так как мы хотим, чтобы каждый ImageComponent
мог поддерживать эти операции, но на самом деле не переопределяем их в реализующих классах. Ключевое слово @optional
уведомляет компилятор, что соответствующие реализации методов могут отсутствовать.
Листинг 16–2 содержит объявление категории для UIImage
, которое мы сможем использовать с другими декораторами позже.
Листинг 16–2. UIImage+ImageComponent.h
#import "ImageComponent.h"
@interface UIImage (ImageComponent) <ImageComponent>
@end
Она (категория) реализует протокол ImageComponent
без какой-либо реальной имплементации. Теперь мы обратимся к нашему главному классу декоратора — ImageFilter
. Его объявление показано в листинге 16–3.
Листинг 16–3. ImageFilter.h
#import "ImageComponent.h"
#import "UIImage+ImageComponent.h"
@interface ImageFilter : NSObject <ImageComponent>
{
@private
id <ImageComponent> component_;
}
@property (nonatomic, retain) id <ImageComponent> component;
- (void) apply;
- (id) initWithImageComponent:(id <ImageComponent>) component;
- (id) forwardingTargetForSelector:(SEL)aSelector;
@end
Он содержит ссылку на ImageComponent
в виде component_
, которая может быть отдекорирована с помощью любых других конкретных декораторов. ImageFilter
переопределяет forwardingTargetForSelector:
и объявляет apply
. Реализация класса показана в листинге 16–4.
Листинг 16–4. ImageFilter.m
#import "ImageFilter.h"
@implementation ImageFilter
@synthesize component=component_;
- (id) initWithImageComponent:(id <ImageComponent>) component
{
if (self = [super init])
{
// сохраняем ImageComponent
[self setComponent:component];
}
return self;
}
- (void) apply
{
// должен быть переопределен подклассами
// для применения настоящих фильтров
}
- (id) forwardingTargetForSelector:(SEL)aSelector
{
NSString *selectorName = NSStringFromSelector(aSelector);
if ([selectorName hasPrefix:@"draw"])
{
[self apply];
}
return component_;
}
@end
Не так уж много полезного делает метод initWithImageComponent:
. Он просто присваивает ссылку на ImageComponent
из параметра метода себе. Также его метод apply
не делает в данном случае ничего, пока мы не увидим его снова в конкретных классах фильтров.
Что здесь интересно, так это то, что мы используем forwardingTargetForSelector:
для перехвата вызовов сообщений, которые экземпляр ImageFilter
не знает, как обработать. Этот метод позволяет подклассам передать рантайму другого получателя сообщения, так что исходное сообщение будет переадресовано. Но мы заинтересованы только во всем, что имеет префикс @“draw”
, и затем мы переадресовываем все остальное прямо к component_
, возвращая его среде исполнения. Например, когда сообщение drawAtRect:
посылается экземпляру ImageFilter
, оно будет перехвачено в методе forwardingTargetForSelector:
, ожидая альтернативного получателя, потому что у ImageFilter
нет никакой реализации для этого. Так как сообщение содержит префикс “draw”, метод посылает сообщение apply
, чтобы сделать что-то перед тем, как component_
обработает сообщение позже.
Теперь мы уже можем получить несколько реальных фильтров. Первый, который мы собираемся создать, — ImageTransformFilter, как показано в листинге 16–5.
Листинг 16–5. ImageTransformFilter.h
#import "ImageFilter.h"
@interface ImageTransformFilter : ImageFilter
{
@private
CGAffineTransform transform_;
}
@property (nonatomic, assign) CGAffineTransform transform;
- (id) initWithImageComponent:(id <ImageComponent>)component
transform:(CGAffineTransform)transform;
- (void) apply;
@end
ImageTransformFilter
является подклассом ImageFilter
и переопределяет метод apply
. Он также объявляет закрытую переменную transform_
типа CGAffineTransform
с ассоциированным с ней свойством для доступа к ней. Так как CGAffineTransform
– это структура C, свойство должно быть присваемого типа, так как его значению нельзя вызвать retain
, как другим объектам Objective-C. У фильтра есть свой метод инициализации. Метод initWithImageComponent:(id <ImageComponent>)component transform: (CGAffineTransform)tranform
принимает экземпляр ImageComponent
и значение CGAffineTransform
во время инициализации. component
будет передан методу initWithComponent:
с помощью super
, а transform
будет присвоено закрытой переменной, как показано в листинге 16–6.
Листинг 16–6. ImageTransformFilter.m
@implementation ImageTransformFilter
@synthesize transform=transform_;
- (id) initWithImageComponent:(id <ImageComponent>)component
transform:(CGAffineTransform)transform
{
if (self = [super initWithImageComponent:component])
{
[self setTransform:transform];
}
return self;
}
- (void) apply
{
CGContextRef context = UIGraphicsGetCurrentContext();
// устанавливаем трансформацию
CGContextConcatCTM(context, transform_);
}
@end
В методе apply
мы получаем ссылку CGContextRef
от функции Quartz 2D UIGraphicsGetCurrentContext()
. Мы не будем вдаваться в детали Quartz 2D здесь. Так как ссылка на валидный текущий контекст получена, мы передаем значение transform_
в CGContextConcatCTM()
для добавления его в контекст. Что бы ни было потом нарисовано в контексте, оно будет оттрансформировано с помощью переданного значения CGAffineTransform
. Фильтр аффинных преобразований теперь закончен.
Как и ImageTransformFilter
, ImageShadowFilter
также является прямым подклассом класса ImageFilter
и переопределяет только метод apply
. В методе, показанном в листинге 16–7, мы получаем графический контекст, затем вызываем функцию Quartz 2D CGContextSetShadow()
, чтобы добавить тень. Остальное очень похоже на то, что делает метод ImageTransformFilter
. Все, что рисуется в контексте, теперь будет иметь эффект тени, как показано на правом изображении на рисунке 16–6.
Листинг 16–7. ImageShadowFilter.m
#import "ImageShadowFilter.h"
@implementation ImageShadowFilter
- (void) apply
{
CGContextRef context = UIGraphicsGetCurrentContext();
// создаем тень
CGSize offset = CGSizeMake (-25, 15);
CGContextSetShadow(context, offset, 20.0);
}
@end
Теперь мы наполнили содержанием все фильтры и готовы заняться клиентским кодом. В проекте-примере к этой главе есть класс DecoratorViewController
, который будет накладывать все эти фильтры в методе viewDidLoad
, как показано в листинге 16–8.
Листинг 16–8. Метод viewDidLoad в DecoratorViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// загружаем исходное изображение
UIImage *image = [UIImage imageNamed:@"Image.png"];
// создаем трансформацию
CGAffineTransform rotateTransform = CGAffineTransformMakeRotation(-M_PI / 4.0);
CGAffineTransform translateTransform = CGAffineTransformMakeTranslation(
-image.size.width / 2.0,
image.size.height / 8.0);
CGAffineTransform finalTransform = CGAffineTransformConcat(rotateTransform,
translateTransform);
// подход, основанный на подклассах
id <ImageComponent> transformedImage = [[[ImageTransformFilter alloc]
initWithImageComponent:image
transform:finalTransform]
autorelease];
id <ImageComponent> finalImage = [[[ImageShadowFilter alloc]
initWithImageComponent:transformedImage]
autorelease];
// создаем новый DecoratorView
// с отфильтрованным изображением
DecoratorView *decoratorView = [[[DecoratorView alloc]
initWithFrame:[self.view bounds]]
autorelease];
[decoratorView setImage:finalImage];
[self.view addSubview:decoratorView];
}
Сначала мы создаем ссылку на исходное изображение бабочки, показанное с левой стороны рисунка 16–6. Затем конструируем CGAffineTransform
для поворота и переноса изображения соответственно. И изображение, и трансформация используются для инициализации экземпляра ImageTransformFilter
как первого фильтра. Затем мы используем весь компонент, чтобы сконструировать экземпляр ImageShadowFilter
, чтобы наложить тень на объект, полученный от ImageTransformFilter
. На этом шаге finalImage
– это итоговый объект, который содержит ImageTransformFilter
, ImageShadowFilter
и исходное изображение. Затем мы присваиваем результирующий объект экземпляру DecoratorView
перед тем, как добавить его к контроллеру в качестве subview
. DecoratorView
занимается тем, что рисует изображение в своем методе drawRect:rect
, как показано в листинге 16–9.
Листинг 16–9. Метод drawRect:rect в DecoratorView.m
- (void)drawRect:(CGRect)rect
{
// Код рисования.
[image_ drawInRect:rect];
}
DecoratorView
хранит ссылку на итоговое изображение UIImage
в качестве image_
. Метод drawRect:rect
перенаправляет сообщение drawInRect:rect
объекту image_
с параметром rect
. Затем отсюда пойдет вся цепь операций по декорированию. ImageShadowFilter
перехватит сообщение первым. После того, как он добавит тень в текущий графический контекст и вернет component_
из метода forwardingTargetForSelector:
, то же самое сообщение будет направлено возвращенному component_
на следующем шаге. На этом шаге component_
— это фактически экземпляр ImageTransformFilter
, когда мы конструировали цепочку на предыдущих шагах. Он также перехватывает метод forwardingTargetForSelector:
и настраивает текущий контекст с помощью предопределенного значения CGAffineTransform
. Затем он снова возвращает component_
так же, как и ImageShadowFilter
. Но на этот раз это исходное изображение бабочки, поэтому когда оно возвращается из ImageTransformFilter
и получает сообщение на последнем шаге, его нарисуют в текущем контексте, учитывая уже все его тени и аффинные преобразования. Это вся последовательность операций, после которой мы получим преобразованное и затененное изображение, как показано на рисунке 16–6.
Примечание: фильтры могут быть соединены в разной последовательности.
Мы можем создать то же самое, используя категории, но с учетом их некоторых характерных особенностей. Следующий раздел покажет, как это сделать.
Реализация Декораторов с помощью категорий
В подходе, использующем категории, нам нужно только добавить фильтры к классу UIImage
в виде категорий, и они будут работать прямо как отдельные классы UIImage
, но при этом ими не являясь. В этом и заключается красота категорий в Objective-C. Мы собираемся добавить два фильтра, один — для применения 2D-трансформации к изображению, другой – для отбрасывания тени. Диаграмма классов, которая иллюстрирует эти отношения, показана на рисунке 16–7.
Рисунок 16–7. Диаграмма классов различных фильтров изображений, представленных категориями класса UIImage
Как и в предыдущем разделе, мы реализуем фильтры преобразования и тени. У нас есть три категории для этого подхода — UIImage (BaseFilter)
, UIImage (Transform)
и UIImage (Shadow)
. С этого момента будем их называть BaseFilter
, Transform
и Shadow
соответственно. BaseFilter
определяет некоторые рутинные 2D операции рисования, которые нужны для отрисовки в текущем графическом контексте, похожие на методы класса ImageFilter
в предыдущем разделе. Другие категории фильтров могут использовать тот же метод для рисования содержащегося в них изображения. И Transform
, и Shadow
не наследуют от BaseFilter
, но они того же происхождения, так как все эти классы – категории UIImage
. Методы, определенные в BaseFilter
, могут быть использованы и в категориях Transform
и Shadow
, как и в предыдущем подходе, но без наследования. Категория Transform
определяет метод imageWithTransform:transform
, который принимает ссылку на трансформацию (детали увидим немного позже), применяет ее к изображению, позволяет ему нарисовать себя, а затем возвращает оттрансформированное изображение. Категория Shadow
определяет метод imageWithDropShadow
, который отбрасывает тень на внутренний объект изображения и возвращает результирующее изображение с уже примененным эффектом. Возможно, вы уже заметили, что категории могут быть сцеплены вместе так же, как и подклассы, описанные в предыдущем разделе. Диаграмма классов, которая иллюстрирует это, показана на рисунке 16–8.
Рисунок 16–8. Диаграмма объектов показывает, как различные категории-фильтры ссылаются на другие экземпляры UIImage во время выполнения
Правый конец цепочки – это исходное изображение, как то, что слева на рисунке 16–6. После добавления исходного изображения к фильтру Shadow
и затем к фильтру Transform
, клиент, который ссылается на объект изображения, получит что-то вроде правой картинки 16–6. Структура цепочки очень похожа на версию с подклассами, за исключением того, что каждая категория использует self
в качестве ссылки на нижележащее изображение вместо отдельной ссылки наподобие component_
.
Давайте взглянем на код. Сначала мы определим категорию BaseFilter
, которая содержит методы, которые могут использоваться по умолчанию в других конкретных фильтрах, как проиллюстрировано в листинге 16–10.
Листинг 16–10. UIImage+BaseFilter.h
@interface UIImage (BaseFilter)
- (CGContextRef) beginContext;
- (UIImage *) getImageFromCurrentImageContext;
- (void) endContext;
@end
BaseFilter
содержит три метода, которые помогают рисовать себя в текущем контексте, листинг 16–11.
Листинг 16–11. UIImage+BaseFilter.m
#import "UIImage+BaseFilter.h"
@implementation UIImage (BaseFilter)
- (CGContextRef) beginContext
{
// Создаем графический контекст с целевым размером
// На iOS 4 и выше используется UIGraphicsBeginImageContextWithOptions
// для учета масштаба
// На более раннем iOS используйте UIGraphicsBeginImageContext
CGSize size = [self size];
if (NULL != UIGraphicsBeginImageContextWithOptions)
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
else
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
return context;
}
- (UIImage *) getImageFromCurrentImageContext
{
[self drawAtPoint:CGPointZero];
// Получаем UIImage из текущего контекста
UIImage *imageOut = UIGraphicsGetImageFromCurrentImageContext();
return imageOut;
}
- (void) endContext
{
UIGraphicsEndImageContext();
}
@end
beginContext
– это почти то же самое, что в версии с подклассами. Все необходимое для рисования происходит здесь. Когда контекст готов для рисования, метод вернет его по запросу.
getImageFromCurrentImageContext
рисует в текущем контексте и возвращает изображение из него с помощью вызова UIGraphicsGetImageFromCurrentImageContext()
.
Затем сообщение endContext
завершает процесс вызовом функции Quartz 2D UIGraphicsEndImageContext()
для очистки контекстно-зависимых ресурсов.
Теперь мы можем приступить к категориям наших фильтров. Первая, на которую мы посмотрим, — это категория Transform
. В Transform
есть только один метод, который принимает структуру CGAffineTransform
и применяет ее к изображению. Ее объявление представлено в листинге 16–12.
Листинг 16–12. UIImage+Transform.h
@interface UIImage (Transform)
- (UIImage *) imageWithTransform:(CGAffineTransform)transform;
@end
Ее реализация очень прямолинейна, как показано в листинге 16–13.
Листинг 16–13. UIImage+Transform.m
#import "UIImage+Transform.h"
#import "UIImage+BaseFilter.h"
@implementation UIImage (Transform)
- (UIImage *) imageWithTransform:(CGAffineTransform)transform
{
CGContextRef context = [self beginContext];
// установка трансформации
CGContextConcatCTM(context, transform);
// Рисуем исходное изображение в контексте
UIImage *imageOut = [self getImageFromCurrentImageContext];
[self endContext];
return imageOut;
}
@end
Метод принимает структуру CGAffineTransform
, которая содержит информацию об аффинной матрице преобразования. Метод передает transform
и контекст в функцию Quartz 2D, CGContextConcatCTM(context, transform)
. Затем transform
добавляется к текущему контексту рисования. Теперь метод посылает self
сообщение getImageFromCurrentImageContext
, которое определено в категории BaseFilter
для рисования на экране. После того, как экземпляр UIImage
возвращается из вызова, посылается сообщение endContext
для закрытия текущего контекста рисования и, в конце концов, возвращается изображение.
Мы разобрались с нашим фильтром Transform
. Это просто, не так ли? Теперь мы можем определить фильтр Shadow
так же просто, как и предыдущий, как показано в листинге 16–14.
Листинг 16–14. UIImage+Shadow.h
@interface UIImage (Shadow)
- (UIImage *) imageWithDropShadow;
@end
Так же, как и фильтр Transform
, Shadow
– это категория класса UIImage
, имеющая один метод. Метод не принимает параметров, но он содержит на пару шагов больше, чем реализация класса Transform
. Мы можем увидеть, как отбрасывать тень на изображение в листинге 16–15.
Листинг 16–15. UIImage+Shadow.m
#import "UIImage+Shadow.h"
#import "UIImage+BaseFilter.h"
@implementation UIImage (Shadow)
- (UIImage *) imageWithDropShadow
{
CGContextRef context = [self beginContext];
// установка тени
CGSize offset = CGSizeMake (-25, 15);
CGContextSetShadow(context, offset, 20.0);
// Вывод исходного изображения в контекст
UIImage * imageOut = [self getImageFromCurrentImageContext];
[self endContext];
return imageOut;
}
@end
Сначала мы создаем некоторые атрибуты тени, которые нам нужны, с помощью вызова функции Quartz 2D, CGSizeMake (-25, 15)
, где два параметра представляют смещения по направлениям X и Y. Затем мы передаем графический контекст в CGContextSetShadow(context, offset, 20.0)
, другую функцию Quartz 2D, с параметром 20.0, который указывает фактор размытия. В итоге, как и в методе addTranform:
категории Transform
, изображение отображается на экране, объект UIImag
e извлекается и возвращается.
К настоящему моменту у нас есть все необходимое для фильтрации изображений UIImag
e. Как мы будем это использовать? В том же самом методе viewDidLoad
класса DecoratorViewController
, как показано в листинге 16–16.
Листинг 16–16. Метод viewDidLoad в файле DecoratorViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// загружаем исходное изображение
UIImage *image = [UIImage imageNamed:@"Image.png"];
// создаем трансформацию
CGAffineTransform rotateTransform = CGAffineTransformMakeRotation(-M_PI / 4.0);
CGAffineTransform translateTransform = CGAffineTransformMakeTranslation(
-image.size.width / 2.0,
image.size.height / 8.0);
CGAffineTransform finalTransform = CGAffineTransformConcat(rotateTransform,
translateTransform);
// подход, использующий категории
// добавляем трансформацию
UIImage *transformedImage = [image imageWithTransform:finalTransform];
// добавляем тень
id <ImageComponent> finalImage = [transformedImage imageWithDropShadow];
// создаем новый image view
// с отфильтрованным изображением
DecoratorView *decoratorView = [[[DecoratorView alloc]
initWithFrame:[self.view bounds]]
autorelease];
[decoratorView setImage:finalImage];
[self.view addSubview:decoratorView];
}
Исходная ссылка на изображение, создание трансформации и прочее такое же, как и в листинге 16–8 версии с подклассами. Различия в том, что imageWithTransform:
, который применяет трансформацию к изображению, исполняется самим объектом изображения и возвращает преобразованное изображение (исходное остается нетронутым). Затем преобразованное изображение выполняет вызов метода imageWithDropShadow
, чтобы отбросить тень на себя, а затем возвращает свою затененную версию как новое изображение под именем finalImage
. finalImage
будет добавлено на imageView
и отображено на экране, как в подходе с подклассами. Однострочная версия вызовов может быть такой:
finalImage = [[image imageWithTransform:finalTransform] imageWithDropShadow];
Сейчас вы можете сказать, в чем разница между подходом, использующим категории, и использующим подклассы? Правильно – фильтры UIImage
в случае категорий – это методы экземпляра, а не реальные подклассы, как во втором случае. Нет никакого наследования в подходе, основанном на категориях, так как все фильтры – это часть UIImage
! Мы используем ImageComponent
в качестве абстрактного типа в подклассовом подходе в то время, как можем использовать UIImage
на всем пути во втором подходе. Однако в обоих случаях фильтры можно накладывать в разной последовательности.
Дизайн категорий кажется проще для реализации фильтрации. Он проще, так как мы не использовали наследование и инкапсуляцию другого объекта UIImage
для расширения цепочки декораторов. В следующем разделе мы поговорим о некоторых плюсах и минусах использования категорий для адаптации паттерна.
Категории Objective-C и паттерн Декоратор
Категория – это фича языка Objective-C, которая позволяет добавлять поведение (интерфейс метода и реализацию) к классу без наследования. Методы категории не оказывают никакого неблагоприятного влияния на исходные методы класса. Методы категории становятся частью класса и могут наследоваться подклассами.
Как мы видели в предыдущем примере, можно использовать категории для реализации паттерна Декоратор. Однако это не строгая его адаптация; это соответствует замыслу паттерна, но только как вариант. Поведение, добавляемое категориями Декоратора, — это артефакт времени компиляции, ведь Objective-C поддерживает динамическое связывание (какая реализация метода должна быть использована) в силу характера самого языка. Также категории Декоратора на самом деле не инкапсулируют экземпляр расширяемого класса.
Несмотря на тот факт, что использование категорий для реализации паттерна немного отклоняется от его исходного замысла, они легковесны и проще в реализации для маленького количества декораторов, чем подход с подклассами. Хотя категории UIImag
e в предыдущем примере не строго инкапсулируют другой компонент, расширяемый экземпляр неявно представлен с помощью self
в UIImage
.
Заключение
Мы представили паттерн Декоратор с его концепциями и различными подходами в реализации на Objective-C. Реализация с использованием подклассов использует более структурированный подход для соединения различных декораторов. Подход, основанный на категориях, проще и более легок, чем его конкурент. Он подходит для приложений, в которых требуется небольшое количество декораторов для существующих классов. Хотя категории отличаются от подклассов и не могут точно адаптировать исходный паттерн, они решают те же проблемы. Паттерн Декоратор – это естественный выбор для проектирования приложений вроде примера фильтрации изображений. Любая комбинация фильтров изображений может быть применена или удалена динамически без влияния на целостность исходного поведения UIImage
.
В следующей главе мы увидим паттерн, похожий на Декоратор, но служащий другим целям. Он называется Цепочка Обязанностей.
Автор: WildCat2013