Декоратор (Перевод с английского главы «Decorator» из книги «Pro Objective-C Design Patterns for iOS» Carlo Chung)

в 23:43, , рубрики: decorator, design patterns, ipad, iphone, objective-c, разработка под iOS, метки: , , , ,

Обычно, делая фотографии, вы не задумываетесь, как оформите их потом. Вы фотографируете просто потому, что хотите поймать момент. Скажем, одну из фотографий вы затем распечатали, потом решили поместить в рамку с необычным стеклом. Но позже вы могли бы поместить ту же фотографию в другую рамку, если бы захотели. Даже несмотря на то, что вы изменили рамку, картинка осталась той же, потому что вы просто что-то добавляли к ней, но не изменяли ее при этом.


В объектно-ориентированном программировании заимствовали похожую идею добавления поведения к другим объектам без потери их исходных особенностей, т. е. расширенный объект будет улучшенной версией того же самого класса (фото в рамке). Любое «улучшение» (рамка) может быть наложено и снято динамически. Мы называем этот паттерн проектирования Декоратором, так как декоратор может добавляться к другому декоратору или исходному объекту для расширения его свойств, оставив исходное поведение нетронутым.

В этой главе мы сначала обсудим концепции паттерна и когда его использовать. Затем мы обсудим, как воспользоваться преимуществами этого паттерна для проектирования серии фильтрующих изображения классов для объектов UIImage.

Что такое паттерн Декоратор?

Классический паттерн Декоратор содержит родительский абстрактный класс Component, который объявляет некоторые общие операции для других конкретных компонентов. Абстрактный класс Component может быть обернут в другой класс Decorator. Decorator содержит ссылку на другой Component. ConcreteDecorator определяет некоторое расширенное поведение для других похожих Component-ов и Decorator-ов и выполняет операцию включенного в него объекта Component. Их отношения проиллюстрированы на рисунке 16-1.

image

Рисунок 16–1. Диаграмма классов паттерна Декоратор

Класс Component определяет некий абстрактный метод operation, который его конкретные классы переопределяют, чтобы иметь свои собственные специфические операции. Decorator – это базовый класс, который определяет «декорирующее» поведение для расширения других экземпляров Component (или Decorator) за счет включения в объект Decorator. Его метод operation по умолчанию просто пересылает сообщение включенному component. ConcreteDecoratorA и ConcreteDecoratorB переопределяют родительский operation, чтобы добавить их собственное поведение к вызову метода operation component-а через использование super. Если вам нужно расширить component только один раз, тогда вы можете пренебречь базовым классом Decorator и позволить ConcreteDecorator-ам перенаправлять любые запросы напрямую к component. Это похоже на формирование цепочки операций с добавлением одного поведения поверх другого, как проиллюстрировано на диаграмме объектов на рисунке 16–2.

image

Рисунок 16–2. Реализация паттерна Декоратор и его функциональность

Примечание. Паттерн Декоратор: Добавляет дополнительные возможности к объекту динамически. Декораторы предоставляют гибкую альтернативу наследованию для расширения функциональности.*

*Исходное определение, данное в книге «Паттерны проектирования» GoF (Addison Wesley, 1994).

Когда бы вы могли использовать паттерн Декоратор?

Есть три распространенные ситуации, в которых вы могли бы рассмотреть возможность использования этого паттерна:

  • Вы хотите добавлять функциональность к отдельным объектам динамически и прозрачно без влияния на другие объекты.
  • Вы хотите расширить поведение класса, с которым это делать было бы непрактично. Определение класса может быть скрыто и недоступно для наследования от него, или расширение поведения класса потребовало бы огромного количества подклассов для поддержания каждой комбинации возможностей.
  • Расширенные возможности класса могут быть опциональными.

Изменение «шкуры» объекта в сравнении с изменением «внутренностей»

В предыдущем разделе мы обсуждали, как различные декораторы могут быть соединены во время выполнения за счет использования внутреннего встроенного компонента в каждом узле декоратора, как показано на рисунке 16–2. Здесь также проиллюстрировано, что каждый декоратор изменяет свой встроенный компонент извне, то есть просто изменяет оболочку объекта. Идея в том, что узел не знает, что его изменяет.

Однако когда каждый узел знает о других узлах, цепочка будет строиться в другом направлении, то есть изнутри. Такой паттерн называется Стратегия (смотри главу 19). Каждому узлу нужно содержать в себе набор различных API, чтобы подключиться к другому узлу стратегии. Визуальное представление этой концепции показано на рисунке 16–3.

image

Рисунок 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.

image

Рисунок 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.

image

Рисунок 16–5. Диаграмма объектов, показывающая, что на каждый ImageComponent есть ссылка в другом экземпляре ImageComponent на каждом уровне.

Правый конец этой цепочки – это исходное изображение, которое показано слева на рисунке 16–6. После того, как мы добавим его к anImageTransformFilter, а затем добавим anImageTransformFilter к anImageShadowFilter, клиент получит что-то вроде правой картинки на рисунке 16–6. Каждый узел инкапсулирован в виде component_ другого экземпляра ImageComponent. Можно провести аналогию с тем, как более крупная рыба проглатывает более мелкую. Очевидно, что клиент не знает никаких деталей декораторов, а просто получает ссылку на экземпляр того же старого типа UIImage (в форме ImageComponent, потому что UIImage реализует ImageComponent через категорию).

image

Рисунок 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.

image

Рисунок 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.

image

Рисунок 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, изображение отображается на экране, объект UIImage извлекается и возвращается.

К настоящему моменту у нас есть все необходимое для фильтрации изображений UIImage. Как мы будем это использовать? В том же самом методе 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 поддерживает динамическое связывание (какая реализация метода должна быть использована) в силу характера самого языка. Также категории Декоратора на самом деле не инкапсулируют экземпляр расширяемого класса.

Несмотря на тот факт, что использование категорий для реализации паттерна немного отклоняется от его исходного замысла, они легковесны и проще в реализации для маленького количества декораторов, чем подход с подклассами. Хотя категории UIImage в предыдущем примере не строго инкапсулируют другой компонент, расширяемый экземпляр неявно представлен с помощью self в UIImage.

Заключение

Мы представили паттерн Декоратор с его концепциями и различными подходами в реализации на Objective-C. Реализация с использованием подклассов использует более структурированный подход для соединения различных декораторов. Подход, основанный на категориях, проще и более легок, чем его конкурент. Он подходит для приложений, в которых требуется небольшое количество декораторов для существующих классов. Хотя категории отличаются от подклассов и не могут точно адаптировать исходный паттерн, они решают те же проблемы. Паттерн Декоратор – это естественный выбор для проектирования приложений вроде примера фильтрации изображений. Любая комбинация фильтров изображений может быть применена или удалена динамически без влияния на целостность исходного поведения UIImage.

В следующей главе мы увидим паттерн, похожий на Декоратор, но служащий другим целям. Он называется Цепочка Обязанностей.

Автор: WildCat2013

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js