Паттерн Прокси

Паттерн Прокси относится к структурным паттернам проектирования.
Паттерн описывает технику работы с классом через класс прослойку – прокси. Прокси позволяет изменять функционал оригинального класса, с возможностью сохранения оригинального поведения, при сохранении оригинального интерфейса класса.
Представим ситуацию – в 2015 году одна из стран западной Европы решает записывать все запросы к сайтам пользователей страны, для улучшения статистики и углубленного понимания политических настроений граждан.
Представим псевдокод наивной имплементации шлюза которым пользуются граждане для выхода в интернет:


class InternetRouter {

    private let internet: Internet

    init(internet: Internet) {
        self.internet = internet
    }

    func handle(request: Request, from client: Client) -> Data {
        return self.internet.handle(request)
    }

}

В приведенном выше коде, мы создаем класс интернет-роутера, с указателем на объект предоставляющий доступ в интернет. При обращении клиента с запросом сайта, мы возвращает ответ из интернета.

Используя паттерн Прокси и антипаттерн синглтон, добавим функционал логирования имени клиента и URL:


class InternetRouterProxy {

    private let internetRouter: InternetRouter

    init(internet: Internet) {
        self.internetRouter = InternetRouter(internet: internet)
    }

    func handle(request: Request, from client: Client) -> Data {

        Logger.shared.log(“Client name: \(client.name), requested URL: \(request.URL)”)

        return self.internetRouter.handle(request: request, from: client)
    }

}

Из-за сохранения оригинального интерфейса InternetRouter в классе прокси InternetRouterProxy, достаточно заменить класс инициализации с InternerRouter на его прокси, больше изменений в кодовой базе не потребуется.

Источники

https://refactoring.guru/ru/design-patterns/proxy

Паттерн Прототип

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


fun didPressOnCopyProfileButton() {
    let profileCopy = new Profile()
    profileCopy.name = generateRandomName()
    profileCopy.age = profile.age
    profileCopy.photos = profile.photos.randomize()
    storage.save(profileCopy)
}

Теперь представим что другие члены команды сделали copy-paste кода копирования или придумали его с нуля, и после этого добавилось новое поле – likes. В данном поле хранится количество лайков профиля, теперь нужно обновить *все* места где происходит копирование вручную, добавив новое поле. Это очень долго и сложно в поддержке кода, как и в тестировании.
Для решения этой проблемы придуман паттерн проектирования Прототип. Создадим общий протокол Copying, с методом copy() который возвращает копию объекта с необходимыми полями. После изменения полей сущности нужно будет обновить только один метод copy(), вместо того чтобы искать и обновлять вручную все места содержащие код копирования.

Источники

https://refactoring.guru/ru/design-patterns/prototype

Стейт машина и паттерн Состояние

В данной заметке я опишу использование стейт машины (Машину состояний), покажу простую реализацию, реализацию с использованием паттерна Состояние. Стоит оговориться, что использовать паттерн Состояние нежелательно при наличии меньше трех состояний, т.к. обычно это приводит к излишнему усложнению читаемости кода, сопутствующими проблемам в поддержке – все должно быть в меру.

MEAACT PHOTO / STUART PRICE.

Повелитель флажков

Допустим мы разрабатываем экран видеоплеер для медиасистемы гражданского самолета, плеер должен уметь загружать видео поток, проигрывать его, позволять пользователю останавливать процесс загрузки, перематывать и производить прочие операции обыкновенные для плеера.
Допустим плеер закэшировал очередной чанк видеопотока, проверил что чанков достаточно для воспроизведения, начал воспроизводить пользователю фрагмент и одновременно продолжает загрузку следующего.
В этот момент пользователь перематывает в середину ролика, тоесть теперь нужно остановить проигрывание текущего фрагмента, и начать загрузку с новой позиции. Однако существуют ситуации в которых такое делать нельзя – пользователь не может управлять проигрыванием видеопотока, пока ему показывают видеоролик о безопасности в воздухе. Заведем флажок isSafetyVideoPlaying для проверки данной ситуации.
Также система должна уметь ставить на паузу текущее видео и транслировать оповещение от капитана судна и экипажа через плеер. Заведем еще один флажок isAnnouncementPlaying. Плюс ко всему появилось требование не приостанавливать воспроизведение во время вывода справки по работе с плеером, еще один флажок isHelpPresenting.

Псевдокод примера медиаплеера:


class MediaPlayer {

    public var isHelpPresenting = false
    public var isCaching = false
    public var isMediaPlaying: Bool = false
    public var isAnnouncementPlaying = false
    public var isSafetyVideoPlaying = false

    public var currentMedia: Media = null

    fun play(media: Media) {

        if isMediaPlaying == false, isAnnouncementPlaying == false, isSafetyVideoPlaying == false {

            if isCaching == false {
                if isHelpPresenting == false {
                    media.playAfterHelpClosed()
                }
                else {
                    media.playAfterCaching()
                }
            }
    }

    fun pause() {
        if isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
            currentMedia.pause()
        }
    }
}

Приведенный выше пример плохо читаем, такой код сложно поддерживать из-за большой вариативности (энтропии) Данный пример основывается на моем опыте работы с кодовой базой *многих* проектов, где не использовалась стейт-машина.
Каждый флажок должен по особенному “управлять” элементами интерфейса, бизнес-логики приложения, разработчик, добавляя очередной флажок, должен уметь жонглировать ими, проверяя и перепроверяя все по нескольку раз со всеми возможными вариантами.
Подставляем в формулу “2 ^ количество флажков” можно получить 2 ^ 6 = 64 варианта поведения приложения для всего 6 флажков, все эти сочетания флажков нужно будет проверять и поддерживать вручную.
Со стороны разработчика добавление нового функционала при такой системе выглядит так:
– Нужно добавить возможность показывать страницу браузера авиакомпании, при этом она должна сворачиваться как с фильмами, если члены экипажа что-то объявляют.
– Ок, сделаю. (Ох чёрт придется добавить еще один флаг, и перепроверить все места где пересекаются флажки, это же сколько всего надо поменять!)

Также слабое место флажковой системы – внесение изменений в поведение приложения. Очень сложно представить как быстро/гибко менять поведение основанное на флажках, если после изменения одного только флага приходится все перепроверять. Такой подход к разработке приводит к большому количеству проблем, потере времени и денег.

Enter The Machine

Если хорошо присмотреться к флажкам, то можно понять что на самом деле мы пытаемся обработать конкретные процессы происходящие в реальном мире. Перечислим их: обычный режим, показ видео безопасности, трансляция сообщения от капитана или членов экипажа. Для каждого процесса известен свод правил, которые меняют поведение приложения.
По правилам паттерна стейт машины (машины состояний), перечислим все процессы как состояния в enum, добавим такое понятие как стейт в код плеера, реализуем поведение на основе стейта, удалив комбинации на флажках. Таким образом мы снизим варианты для тестирования ровно до количества состояний.

Псевдокод:


enum MediaPlayerState {
	mediaPlaying,
	mediaCaching,
	crewSpeaking,
	safetyVideoPlaying,
	presentingHelp
}

class MediaPlayer {
	fun play(media: Media) {
		media.play()
	}

	func pause() {
		media.pause()
	}
}

class MediaPlayerStateMachine {
	public state: MediaPlayerState
	public mediaPlayer: MediaPlayer
	public currentMedia: Media

	//.. init (mediaPlayer) etc

	public fun set(state: MediaPlayerState) {
		switch state {
			case mediaPlaying:
				mediaPlayer.play(currentMedia)
			case mediaCaching, crewSpeaking,
			safetyVideoPlaying, presentingHelp:
				mediaPlayer.pause()
		}
	}
}

Огромная разница между флажковой системой и стейт-машиной заключается в воронке переключения логического состояния в методе set(state: ..), оно позволяет транслировать человеческое понимание состояния в программный код, без необходимости играть в логические игры по конвертации флажков в состояния при дальнейшей поддержке кода.

Паттерн Состояние

Далее я покажу разницу между наивной имплементацией стейт машины и паттерном состояние. Представим что понадобилось добавить 10 стейтов, в итоге класс стейт-машины разрастется до размеров godobject, его будет сложно и затратно поддерживать. Конечно данная реализация лучше флажковой, (при флажковой системе застрелится сначала разработчик, а если и нет, то увидев 2 ^ 10 = 1024 вариаций повесится QA, однако если они оба *не заметят* сложности задачи, то ее заметит пользователь, у которого приложение просто откажется работать при определенном сочетании флажков)
При большом количестве состояний необходимо использовать паттерн Состояния.
Вынесем свод правил в протокол Состояния:


protocol State {
    func playMedia(media: Media, context: MediaPlayerContext)
    func shouldCacheMedia(context: MediaPlayerContext)
    func crewSpeaking(context: MediaPlayerContext)
    func safetyVideoPlaying(context:MediaPlayerContext)
    func presentHelp(context: MediaPlayerContext)
}

Вынесем реализацию свода правил в отдельные состояния, для примера код одного состояния:


class CrewSpeakingState: State {
	func playMedia(context: MediaPlayerContext) {
		showWarning(“Can’ t play media - listen to announce!”)
	}

	func mediaCaching(context: MediaPlayerContext) {
		showActivityIndicator()
	}

	func crewSpeaking(context: MediaPlayerContext) {
		set(volume: 100)
	}

	func safetyVideoPlaying(context: MediaPlayerContext) {
		set(volume: 100)
	}

	func presentHelp(context: MediaPlayerContext) {
		showWarning(“Can’ t present help - listen to announce!”)
	}
}

Далее создадим контекст с которым будет работать каждый стейт, интегрируем стейт-машину:


final class MediaPlayerContext {
	private
	var state: State

	public fun set(state: State) {
		self.state = state
	}

	public fun play(media: Media) {
		state.play(media: media, context: this)
	}

	…
	остальные возможные события
}

Компоненты приложения работают с контекстом через публичные методы, объекты стейты сами решают из какого стейта в какой осуществлять переход с помощью стейт-машины внутри контекста.
Таким образом мы реализовали декомпозицию God Object, поддерживать изменяющееся состояние будет гораздо проще, благодаря отслеживанию изменений в протоколе компилятором, уменьшения сложности понимания состояний из-за уменьшения количества строк кода и фокусе на решении конкретной задачи состояния. Также теперь можно разделять работу в команде, отдавая реализацию конкретного состояния членам команды, не беспокоясь о необходимости “разруливать” конфликты, что происходит при работе с одним большим классом стейт-машины.

Источники

https://refactoring.guru/ru/design-patterns/state

Скелетная анимация (Часть 1 – шейдер)

В данной статье я опишу свое понимание скелетной анимации, которая используется во всех современных 3D движках для анимирования персонажей, игрового окружения и т.п.
Начну описание с наиболее осязаемой части – вертексного шейдера, ведь весь путь расчетов, сколь сложным он не был, заканчивается передачей подготовленных данных на отображение в вертексный шейдер.

Скелетная анимация после обсчета на CPU попадает в вертексный шейдер.
Напомню формулу вертекса без скелетной анимации:
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vertex;
Для тех кто не понимает как возникла эта формула, может почитать мою статью описывающую принцип работы с матрицами для отображения 3D контента в контексте OpenGL.
Для остальных – формула реализации скелетной анимации:
” vec4 animatedVertex = bone0matrix * vertex * bone0weight +”
“bone1matrix * vertex * bone1weight +”
“bone2matrix * vertex * bone2weight +”
“bone3matrix * vertex * bone3weight;\n”
” gl_Position = projectionMatrix * viewMatrix * modelMatrix * animatedVertex;\n”

Тоесть конечную матрицу трансформации кости умножаем на вертекс и на вес этой матрицы относительно вертекса. Каждый вертекс может быть анимирован 4-мя костями, сила воздействия регулируется параметром веса кости, сумма воздействий должна равняться единице.
Что делать если на вертекс воздействуют меньше 4-х костей? Нужно поделить вес между ними, воздействие остальных сделать равным нулю.
Математически умножение веса на матрицу называется “Умножение матрицы на скаляр”. Умножение на скаляр позволяет суммировать воздействие матриц на итоговый вертекс.

Сами матрицы трансформации костей передаются массивом. Причем массив содержит матрицы для всей модели в целом, а не для каждого меша по отдельности.

А вот для каждого вертекса отдельно передается следующая информация:
– Индекс матрицы которая воздействует на вертекс
– Вес матрицы которая воздействует на вертекс
Передается не одна кость, обычно используется воздействие 4 костей на вертекс.
Также сумма весов 4-х костей должна всегда быть равна единице.
Далее рассмотрим как это выглядит в шейдере.
Массив матриц:
“uniform mat4 bonesMatrices[kMaxBones];”

Информация о воздействии 4 костей на каждый вертекс:
“attribute vec2 bone0info;”
“attribute vec2 bone1info;”
“attribute vec2 bone2info;”
“attribute vec2 bone3info;”

vec2 – в координате X храним индекс кости (и переводим в int в шейдере), в координате Y вес воздействия кости на вертекс. Почему приходится передавать эти данные в двухмерном векторе? Потому что GLSL не поддерживает передачу читаемых C структур с корректными полями в шейдер.

Ниже приведу пример получения необходимой информации из вектора, для дальнейшей подстановки в формулу animatedVertex:

“int bone0Index = int(bone0info.x);”
“float bone0weight = bone0info.y;”
“mat4 bone0matrix = bonesMatrices[bone0Index];”

“int bone1Index = int(bone1info.x);”
“float bone1weight = bone1info.y;”
“mat4 bone1matrix = bonesMatrices[bone1Index];”

“int bone2Index = int(bone2info.x);”
“float bone2weight = bone2info.y;”
“mat4 bone2matrix = bonesMatrices[bone2Index];”

“int bone3Index = int(bone3info.x);”
“float bone3weight = bone3info.y;”
“mat4 bone3matrix = bonesMatrices[bone3Index];”

Теперь структура вертекса, заполняющаяся на CPU, должна выглядеть так:
x, y, z, u, v, bone0index, bone0weight, bone1index, bone1weight, bone2index, bone2weight, bone3index, bone3weight

Структура вертексного буфера заполняется один раз во время загрузки модели, а вот матрицы трансформации передаются из CPU в шейдер при каждом кадре рендеринга.

В остальных частях я опишу принцип обсчета анимации на CPU, перед передачей в вертексный шейдер, опишу дерево нод костей, проход по иерархии анимация-модель-ноды-мэш, интерполяции матриц.

Источники

http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html

Исходный код

https://gitlab.com/demensdeum/skeletal-animation

Шаблонный метод

Шаблонный метод относится к поведенческим шаблонам проектирования. Паттерн описывает способ замены части логики класса по требованию, оставляя общую часть неизменяемой для потомков.

Cuban Cars

Допустим мы разрабатываем банк-клиент, рассмотрим задачу разработки модуля авторизации – пользователь должен иметь возможность авторизоваться в приложении используя абстрактные данные для входа.
Модуль авторизации должен быть кроссплатформенным, поддерживать разные технологии авторизации и хранения зашифрованных данных разных платформ. Для реализации модуля мы выбираем кроссплатформенный язык Kotlin, используя абстрактный класс (протокол) модуля авторизации, напишем реализацию для телефона MyPhone:


class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
    fun loginAndPassword() : Pair {
        return Pair("admin", "qwerty65435")
    }
}

class ServerApiClient {
    fun authorize(authorizationData: AuthorizationData) : Unit {
        println(authorizationData.login)
        println(authorizationData.password)
        println("Authorized")
    }
}

class AuthorizationData {
    var login: String? = null
    var password: String? = null
}

interface AuthorizationModule {
    abstract fun fetchAuthorizationData() : AuthorizationData
    abstract fun authorize(authorizationData: AuthorizationData)
}

class MyPhoneAuthorizationModule: AuthorizationModule {
    
    override fun fetchAuthorizationData() : AuthorizationData {
        val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
        val authorizationData = AuthorizationData()
        authorizationData.login = loginAndPassword.first
        authorizationData.password = loginAndPassword.second
        
        return authorizationData
    }
    
    override fun authorize(authorizationData: AuthorizationData) {
        ServerApiClient().authorize(authorizationData)
    }
    
}

fun main() {
    val authorizationModule = MyPhoneAuthorizationModule()
    val authorizationData = authorizationModule.fetchAuthorizationData()
    authorizationModule.authorize(authorizationData)
}

Теперь для каждого телефона/платформы нам придется дублировать код отправки авторизации на сервер, налицо нарушение принципа DRY. Приведенный выше пример очень прост, в более комплексных классах дублирования будет еще больше. Для устранения дублирования кода следует использовать паттерн Шаблонный метод.
Вынесем общие части модуля в неизменяемые методы, функционал передачи зашифрованных данных переложим на конкретные классы платформ:


class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
    fun loginAndPassword() : Pair {
        return Pair("admin", "qwerty65435")
    }
}

class ServerApiClient {
    fun authorize(authorizationData: AuthorizationData) : Unit {
        println(authorizationData.login)
        println(authorizationData.password)
        println("Authorized")
    }
}

class AuthorizationData {
    var login: String? = null
    var password: String? = null
}

interface AuthorizationModule {
    abstract fun fetchAuthorizationData() : AuthorizationData
    
    fun authorize(authorizationData: AuthorizationData) {
        ServerApiClient().authorize(authorizationData)
    }
}

class MyPhoneAuthorizationModule: AuthorizationModule {
    
    override fun fetchAuthorizationData() : AuthorizationData {
        val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
        val authorizationData = AuthorizationData()
        authorizationData.login = loginAndPassword.first
        authorizationData.password = loginAndPassword.second
        
        return authorizationData
    }
    
}

fun main() {
    val authorizationModule = MyPhoneAuthorizationModule()
    val authorizationData = authorizationModule.fetchAuthorizationData()
    authorizationModule.authorize(authorizationData)
}

Источники

https://refactoring.guru/ru/design-patterns/template-method

Исходный код

https://gitlab.com/demensdeum/patterns/

Паттерн Мост

Паттерн Мост относится к структурным паттернам проектирования. Он позволяет абстрагировать реализацию логики класса, с помощью переноса логики в отдельный абстрактный класс. Звучит просто, да?

Допустим мы реализуем спам-бота который должен уметь отправлять сообщения в разные типы мессенджеров.
Реализуем с помощью общего протокола:


protocol User {
    let token: String
    let username: String
}

protocol Messenger {
    var authorize(login: String, password: String)
    var send(message: String, to user: User)
}

class iSeekUUser: User {
    let token: String
    let username: String
}

class iSeekU: Messenger {

    var authorizedUser: User?
    var requestSender: RequestSender?
    var requestFactory: RequestFactory?

    func authorize(login: String, password: String) {
        authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
    }
    
    func send(message: String, to user: User) {
        requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
    }
}

class SpamBot {
    func start(usersList: [User]) {
        let iSeekUMessenger = iSeekU()
        iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
        
        for user in usersList {
            iSeekUMessennger.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
        }
    }
}

Теперь представим ситуацию выхода нового более быстрого протокола отправки сообщений для мессенджера iSekU. Для добавления нового протокола нужно будет продублировать имплементацию бота iSekU, изменив лишь малую его часть. Непонятно зачем это делать если изменилась лишь малая часть логики класса. При таком подходе нарушается принцип DRY, при дальнейшем развитии продукта, отсутствие гибкости даст о себе знать ошибками и задержками в имплементации новых возможностей.
Вынесем логику работы протокола в абстрактный класс, таким образом реализовав паттерн Мост:


protocol User {
    let token: String
    let username: String
}

protocol Messenger {
    var authorize(login: String, password: String)
    var send(message: String, to user: User)
}

protocol MessagesSender {
    func send(message: String, to user: User)
}

class iSeekUUser: User {
    let token: String
    let username: String
}

class iSeekUFastMessengerSender: MessagesSender {
    func send(message: String, to user: User) {
        requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
    }
}

class iSeekU: Messenger {

    var authorizedUser: User?
    var requestSender: RequestSender?
    var requestFactory: RequestFactory?
    var messagesSender: MessengerMessagesSender?

    func authorize(login: String, password: String) {
        authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
    }
    
    func send(message: String, to user: User) {
        messagesSender?.send(message: message, to: user)
    }
}

class SpamBot {

    var messagesSender: MessagesSender?

    func start(usersList: [User]) {
        let iSeekUMessenger = iSeekU()
        iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
        
        for user in usersList {
            messagesSender.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
        }
    }
}

Одним из плюсов такого подхода несомненно является возможность расширять функционал приложения, с помощью написания плагинов/библиотек, реализующих абстрагированную логику, не изменяя при этом кода основного приложения.
В чем разница с паттерном Стратегия? Оба паттерны очень похожи, однако Стратегия описывает переключения *алгоритмов*, в то время как Мост позволяет переключать большие части *любой сколь сложной логики*.

Источники

https://refactoring.guru/ru/design-patterns/bridge

Исходный код

https://gitlab.com/demensdeum/patterns/

Паттерн Цепочка Обязанностей

Цепочка обязанностей относится к поведенческим шаблонам проектирования.


Ганна Долбієва

Кинокомпания Джа-пикчерс сняла фильм документальный фильм про коммунистов-растаманов из Либерии под названием “Красная заря Марли”. Фильм очень долгий (8 часов), интересный, однако перед отправкой в прокат оказалось что в некоторых странах кадры и фразы из фильмы могут счесть ересью и не дать прокатной лицензии. Продюсеры киноленты решают вырезать моменты содержащие сомнительные фразы из фильма, в ручном и в автоматическом режиме. Двойная проверка нужна для того чтобы представителей прокатчика банально не расстреляли в некоторых странах, в случае ошибки при ручном отсмотре и монтаже.
Страны делятся на четыре группы – страны без цензуры, с умеренной, средней и очень строгой цензурой. Принимается решение использовать нейросети для классификации уровня ереси в отсматриевом фрагменте фильма. Для проекта закупаются очень дорогие state-of-art нейронки обученные на разные уровни цензуры, задача разработчика – разбить фильм на фрагменты и передавать их по цепочке нейросетей, от вольной до строгой, пока одна из них не обнаружит ересь, дальше фрагмент передается на ручной отсмотр для дальнейшего монтажа. Делать проход по всем нейронкам нельзя, т.к. на их работу затрачивается слишком много вычислительных мощностей (нам ведь еще за свет платить) достаточно остановиться на первой сработавшей.
Наивная имлементация псевдокодом:


import StateOfArtCensorshipHLNNClassifiers

protocol MovieCensorshipClassifier {
    func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}

class CensorshipClassifier: MovieCensorshipClassifier {

    let hnnclassifier: StateOfArtCensorshipHLNNClassifier

    init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier) {
        self.hnnclassifier = hnnclassifier
    }
    
    func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
        return hnnclassifier.shouldBeCensored(movieChunk)
    }
}

let lightCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"))
let normalCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal"))
let hardCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard"))

let classifiers = [lightCensorshipClassifier, normalCensorshipClassifier, hardCensorshipClassifier]

let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
    for classifier in classifiers {
        if classifier.shouldBeCensored(chunk) == true {
            print("Should censor movie chunk: \(chunk), reported by \(classifier)")
        }
   }
}

Вообщем и целом решение с массивом классификаторов не такое уж и плохое, Однако! представим что мы не можем создавать массив, нам доступна возможность создавать только одну сущность классификатора, который уже определяет тип цензуры для фрагмента фильма. Такие ограничения возможны при разработки библиотеки расширяющей функционал приложения (плагин).
Воспользуемся паттерном декоратор – добавим в класс классификатора референс на следующий классификатор в цепочке, будем останавливать процесс проверки на первой успешной классификации.
Таким образом мы реализуем паттерн Цепочка Обязанности:


import StateOfArtCensorshipHLNNClassifiers

protocol MovieCensorshipClassifier {
    func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}

class CensorshipClassifier: MovieCensorshipClassifier {

    let nextClassifier: CensorshipClassifier?
    let hnnclassifier: StateOfArtCensorshipHLNNClassifier

    init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier, nextClassifier: CensorshipClassifiers?) {
            self.nextClassifier = nextClassifier
            self.hnnclassifier = hnnclassifier
    }
    
    func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
        let result = hnnclassifier.shouldBeCensored(movieChunk)
        
        print("Should censor movie chunk: \(movieChunk), reported by \(self)")
        
        if result == true {
                return true
        }
        else {
                return nextClassifier?.shouldBeCensored(movieChunk) ?? false
        }
    }
}

let censorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"), nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal", nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard")))))

let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
    censorshipClassifier.shouldBeCensored(chunk)
}

References

https://refactoring.guru/ru/design-patterns/chain-of-responsibility

Source Code

https://gitlab.com/demensdeum/patterns/

Паттерн Декоратор

Паттерн Декоратор относится к структурным паттернам проектирования.

Декоратор используется как альтернатива наследованию для расширения функционала классов.
Имеется задача расширения функционала приложения в зависимости от типа продукта. Заказчику необходимы три типа продукта – Basic, Professional, Ultimate.
Basic – считает количество символов, Professional – возможности Basic + печатает текст большими буквами, Ultimate – Basic + Professional + печатает текст с надписью ULTIMATE.
Реализуем с помощью наследования:


protocol Feature {
	func textOperation(text: String)
}

class BasicVersionFeature: Feature {
	func textOperation(text: String) {
		print("\(text.count)")
	}
}

class ProfessionalVersionFeature: BasicVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.uppercased())")
	}
}

class UltimateVersionFeature: ProfessionalVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")
	}
}

let textToFormat = "Hello Decorator"

let basicProduct = BasicVersionFeature()
basicProduct.textOperation(text: textToFormat)

let professionalProduct = ProfessionalVersionFeature()
professionalProduct.textOperation(text: textToFormat)

let ultimateProduct = UltimateVersionFeature()
ultimateProduct.textOperation(text: textToFormat)

Теперь появляется требование реализовать продукт “Ultimate Light” – Basic + Ultimate но без возможностей Professional версии. Случается первый ОЙ!, т.к. придется создавать отдельный класс для такой простой задачи, дублировать код.
Продолжим реализацию с помощью наследования:


protocol Feature {
	func textOperation(text: String)
}

class BasicVersionFeature: Feature {
	func textOperation(text: String) {
		print("\(text.count)")
	}
}

class ProfessionalVersionFeature: BasicVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.uppercased())")
	}
}

class UltimateVersionFeature: ProfessionalVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")
	}
}

class UltimateLightVersionFeature: BasicVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")	
	}
}

let textToFormat = "Hello Decorator"

let basicProduct = BasicVersionFeature()
basicProduct.textOperation(text: textToFormat)

let professionalProduct = ProfessionalVersionFeature()
professionalProduct.textOperation(text: textToFormat)

let ultimateProduct = UltimateVersionFeature()
ultimateProduct.textOperation(text: textToFormat)

let ultimateLightProduct = UltimateLightVersionFeature()
ultimateLightProduct.textOperation(text: textToFormat)

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


protocol Feature {
	func textOperation(text: String)
}

class FeatureDecorator: Feature {
	private var feature: Feature?
	
	init(feature: Feature? = nil) {
		self.feature = feature
	}
	
	func textOperation(text: String) {
		feature?.textOperation(text: text)
	}
}

class BasicVersionFeature: FeatureDecorator {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.count)")
	}
}

class ProfessionalVersionFeature: FeatureDecorator {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.uppercased())")
	}
}

class UltimateVersionFeature: FeatureDecorator {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")
	}
}

let textToFormat = "Hello Decorator"

let basicProduct = BasicVersionFeature(feature: UltimateVersionFeature())
basicProduct.textOperation(text: textToFormat)

let professionalProduct = ProfessionalVersionFeature(feature: UltimateVersionFeature())
professionalProduct.textOperation(text: textToFormat)

let ultimateProduct = BasicVersionFeature(feature: UltimateVersionFeature(feature: ProfessionalVersionFeature()))
ultimateProduct.textOperation(text: textToFormat)

let ultimateLightProduct = BasicVersionFeature(feature: UltimateVersionFeature())
ultimateLightProduct.textOperation(text: textToFormat)

Теперь мы можем создавать вариации продукта любого типа – достаточно инициализировать комбинированные типы на этапе запуска приложения, пример ниже представляет из себя создание Ultimate + Professional версии:

let ultimateProfessionalProduct = UltimateVersionFeature(feature: ProfessionalVersionFeature())
ultimateProfessionalProduct.textOperation(text: textToFormat)

Источники

https://refactoring.guru/ru/design-patterns/decorator

Исходный код

https://gitlab.com/demensdeum/patterns