
Всем привет! Зачастую чтобы в чем то разобраться полезнее один раз увидеть конкретный пример чем несколько раз прочитать заумное описание.
Решил написать ряд небольших статей для начинающих, в которых дать краткое описание основных паттернов проектирования и привести лаконичные примеры их использования.
Данная статья, как можно догадаться из названия =), посвящена структурным паттернам.
Приступим.
Адаптер / Adapter
Адаптер — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.
Представим у нас есть класс, единственная цель которого "говорить" Hello world!
// MARK: - Protocol
protocol HelloWorldSpeaker {
func sayHelloWorld()
}
// MARK: - Classes
final class EnglishSpeaker: HelloWorldSpeaker {
func sayHelloWorld() {
print("Hello World!")
}
}
Так же у нас есть классы, которые делают тоже самое на Русском и на Испанском.
final class RussianSpeaker {
func sayPrivetMir() {
print("Privet Mir!")
}
}
final class SpanishSpeaker {
func digaHolaElMundo() {
print("Hola el Mundo!")
}
}
Представим, что у нас нет доступа к классам RussianSpeaker и SpanishSpeaker, а в вызывающем коде нам подразумевается, что у класса должна быть функция sayHelloWorld(). Как нам адаптировать наши два класса для использования?
У нас есть два пути: через расширении и через создание класса адаптера.
Используя расширение, мы просто оборачиваем вызов digaHolaElMundo() в вызов sayHelloWorld().
extension SpanishSpeaker: HelloWorldSpeaker {
func sayHelloWorld() {
digaHolaElMundo()
}
}
А вот так мы создаем класс адаптер:
// MARK: - Adapter
final class RussianSpeakerAdapter: HelloWorldSpeaker {
let adapteeSpeaker: RussianSpeaker
init(adapteeSpeaker: RussianSpeaker) {
self.adapteeSpeaker = adapteeSpeaker
}
func sayHelloWorld() {
adapteeSpeaker.sayPrivetMir()
}
}
В вызывающем коде мы теперь можем спокойно использовать наши классы SpanishSpeaker и RussianSpeakerAdapter.
var speakers = [HelloWorldSpeaker]()
let englishSpeaker = EnglishSpeaker()
let spanishSpeaker = SpanishSpeaker()
let russianSpeaker = RussianSpeaker()
let russianSpeakerAdapter = RussianSpeakerAdapter(adapteeSpeaker: russianSpeaker)
speakers.append(englishSpeaker)
speakers.append(russianSpeakerAdapter)
speakers.append(spanishSpeaker)
for speaker in speakers {
speaker.sayHelloWorld()
}
Мост / Bridge
Мост — это структурный паттерн проектирования, который разделяет один или несколько классов на две отдельные иерархии — абстракцию и реализацию, позволяя изменять их независимо друг от друга.
Допустим у нас есть класс, представляющий девайс и и класс, описывающий пульт от этого девайса. Со временем количество девайсов и пультов может значительно увеличится. Как нам сохранить гибкость кода в таком случае? Создадим две иерархии классов девайсы и пульты и будем развивать их независимо.
Опиши протокол для классов каждой иерархии.
protocol Remote {
var device: Device { get set }
func on()
func off()
func volumeUp()
func volumeDown()
func set(channel: Int)
}
protocol Device {
var isOn: Bool { get set }
var volume: Int { get set }
var channel: Int { get set }
func on()
func off()
func set(volume: Int)
func set(channel: Int)
}
Наш класс пульта может выглядеть следующим образом:
final class DeviceRemote: Remote {
var device: Device
init(device: Device) {
self.device = device
}
func on() {
device.on()
}
func off() {
device.off()
}
func volumeUp() {
device.set(volume: device.volume + 1)
}
func volumeDown() {
device.set(volume: device.volume - 1)
}
func set(channel: Int) {
device.set(channel: channel)
}
}
Для примера можем создать два девайса радио и тв:
class Radio: Device {
var isOn = false
var volume = 0
var channel = 0
func on() {
isOn = true
print("Radio is on")
}
func off() {
isOn = false
print("Radio is off")
}
func set(volume: Int) {
self.volume = volume
print("Radio volume \(self.volume)")
}
func set(channel: Int) {
self.channel = channel
print("Radio channel \(self.channel)")
}
}
class TV: Device {
var isOn = false
var volume = 0
var channel = 0
func on() {
isOn = true
print("TV is on")
}
func off() {
isOn = false
print("TV is of")
}
func set(volume: Int) {
self.volume = volume
print("TV volume \(self.volume)")
}
func set(channel: Int) {
self.channel = channel
print("TV channel \(self.channel)")
}
}
Теперь мы можем использовать эти классы в нашем коде следующим образом:
let tv = TV()
let radio = Radio()
let remote = DeviceRemote(device: radio)
remote.on()
remote.set(channel: 4)
remote.volumeUp()
remote.volumeUp()
remote.volumeDown()
remote.off()
Компоновщик / Composit
Компоновщик — это структурный паттерн проектирования, который позволяет сгруппировать множество объектов в древовидную структуру, а затем работать с ней так, как будто это единичный объект.
Представим мы с вами пишем код для маркетплейса. У каждого товара есть цена. Когда мы помещаем несколько товаров в коробку, мы хотим иметь возможность узнать стоимость всех товаров, находящихся в коробке, не открывая ее и не пересчитывая содержимое. В решении этой задачи нам и поможет паттерн компоновщик.
Мы хотим, чтобы каждый товар или коробка с разными товарами могли сообщить нам свои стоимость. Создадим протокол:
protocol Composite {
func getPrice() -> Double
}
Создадим класс товара, удовлетворяющий данному протоколу:
class Item: Composite {
let price: Double
init(price: Double) {
self.price = price
}
func getPrice() -> Double {
return price
}
}
Создадим класс коробки, так же подписав его под протокол Composite.
class Box: Composite {
var items: [Composite] = []
func getPrice() -> Double {
var total = 0.0
for item in items {
let price = item.getPrice()
total += price
}
return total
}
func addItem(item: Composite) {
self.items.append(item)
}
Как видим, в методе getPrice мы пробегаемся по всему содержимому коробки и суммируем стоимость всех товаров.
Теперь в вызывающем коде мы можем использовать наши классы следующим образом:
// Создаем товары
let car = Item(price: 1000)
let bike = Item(price: 500)
let boat = Item(price: 300)
// Создаем коробки
let bigBox = Box()
let midBox = Box()
let smalBox = Box()
// Помещаем товары в коробки
smalBox.addItem(item: boat)
midBox.addItem(item: boat)
midBox.addItem(item: bike)
midBox.addItem(item: smalBox)
bigBox.addItem(item: car)
bigBox.addItem(item: bike)
bigBox.addItem(item: boat)
// Помещаем коробки в коробки =)
bigBox.addItem(item: smalBox)
bigBox.addItem(item: midBox)
// Печатаем стоимость коробок
print(smalBox.getPrice())
print(midBox.getPrice())
print(bigBox.getPrice())
Декоратор / Decorator
Декоратор — это структурный паттерн проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные «обёртки».
Теперь мы перешли на работу к крупному автопроизводителю (Toyota, Ford или кто-то другой, кому как больше нравиться =) )
У нас есть базовый автомобиль по базовой цене, а так же различные опции, которые добавляют к цене автомобиля свою стоимость. Как нам наиболее элегантно задать стоимость автомобиля черного цвета с кожаными сидениями? На помощь спешит паттерн декоратор.
Сам автомобиль, а так же каждая опция имеют свою цену. Чтобы обеспечить это, создадим протокол:
protocol Component {
func getPrice() -> Int
}
Создадим класс автомобиля.
class Car: Component {
let price: Int
init(price: Int) {
self.price = price
}
func getPrice() -> Int {
return price
}
}
Создадим базовый класс декоратор.
class Decorator: Component {
private var component: Component
init(component: Component) {
self.component = component
}
func getPrice() -> Int {
return component.getPrice()
}
}
Как видим класс декоратор содержит в себе компонент, цену которого он и возвращает в методе getPrice().
Теперь создадим классы конкретных опций черного цвета и кожаных сидений:
class BlackColor: Decorator {
override func getPrice() -> Int {
return super.getPrice() + 100
}
}
class LeatherSeats: Decorator {
override func getPrice() -> Int {
super.getPrice() + 200
}
}
Они наследуют от базового класса. В методе getPrice() получают стоимость, которую возвращает родительский класс, добавляют к ней свою и возвращают суммарную стоимость.
Вот как мы можем использовать наши созданные классы в нашей программе:
// Создаем базовый автомобиль
let car = Car(price: 1000)
print(car.getPrice())
// Красим его в премиальный черный цвет
let blackCar = BlackColor(component: car)
print(blackCar.getPrice())
// Помещаем в черный автомобиль кожанные сиденья
let blackCarWithLeatherSeats = LeatherSeats(component: blackCar)
print(blackCarWithLeatherSeats.getPrice())
Фасад / Facade
Фасад — это структурный паттерн проектирования, который предоставляет простой интерфейс к сложной системе классов, библиотеке или фреймворку.
Мы пишем класс видеоплеер. Сам по себе класс должен взимодействовать со множеством различных сущностей. Например, он должен скачать видео определенного формата, сохранить его в список видео, выбрать видео из списка, запустить его и т.п.
Было бы не разумно заставлять пользователя нашего класса заставлять проделывать все эти действия когда он хочет им воспользоваться. Намного лучше предоставить ему возможность просто вызвать метод play(), а всю сложную работу оставить под капотом. Для этого и предназначен паттерн фасад.
У нас есть следующие классы
Фильм.
class Movie {
var name: String
init(name: String) {
self.name = name
}
func play() {
print("Movie \(name) is playing")
}
}
Список фильмов.
class MovieList {
var movieList: [Movie] = []
}
Видео форматер.
class VideoFormatter {
func formatMovies(_ list: MovieList) {
for movie in list.movieList {
movie.name += ".mp4"
}
}
}
Создадим класс видео плеер, который и будет являться фасадом, объединяя в себе все сущности и логику, необходимые для воспроизведения видео.
final class VideoPlayer {
func playMove(_ name: String) {
let movie1 = Movie(name: "Batman")
let movie2 = Movie(name: "Spiderman")
let movie3 = Movie(name: "Avangers")
let list = MovieList()
list.movieList.append(movie1)
list.movieList.append(movie2)
list.movieList.append(movie3)
let formatter = VideoFormatter()
formatter.formatMovies(list)
for movie in list.movieList {
if movie.name == name {
movie.play()
}
}
}
}
Теперь посмотреть фильм мы можем просто передав его название в метод playMovie()
let player = VideoPlayer()
player.playMove("Spiderman.mp4")
Легковес / Flyweight
Легковес — это структурный паттерн проектирования, который позволяет вместить бóльшее количество объектов в отведённую оперативную память. Легковес экономит память, разделяя общее состояние объектов между собой, вместо хранения одинаковых данных в каждом объекте.
У нас стоит задача создать игру. В этой игре должен быть лес из тысяч различных деревьев. Создавая огромный лес мы рискуем столкнуться с нехваткой памяти (в наши дни маловероятно, но в относительно недавнем прошлом, да и сейчас в геймдеве вопрос актуальный).
По сути все деревья более менее одинаковые и отличаются только своим типом (береза, ель ...) и местом в лесу (координатами на канвасе). Тип дерева может содержать его изображение (UIImage), который то больше всего и весит, а координаты X и Y можно сказать не весят ничего.
Давайте отделим тип дерева от его координат. Тогда самую тяжеловесную часть дерева мы будем создавать один раз для каждого типа деревьев, а для каждого отдельного дерева будем создавать только координаты.
Тип дерева, содержащий самые тяжеловесные данные.
class TreeType {
var name: String
var color: UIColor
var texture: UIImage
init(name: String, color: UIColor, texture: UIImage) {
self.name = name
self.color = color
self.texture = texture
}
func draw(canvas: String, x: Int, y: Int) {
print("Drawing \(name) on \(canvas) at coordinates: x:\(x) y:\(y)")
}
}
Само дерево Содержит тип и координаты.
class Tree {
var x: Int
var y: Int
var type: TreeType
init(x: Int, y: Int, type: TreeType) {
self.x = x
self.y = y
self.type = type
}
func draw(canvas: String) {
type.draw(canvas: canvas, x: x, y: y)
}
}
Для оптимизации создания типов деревьев создадим фабрику.
class TreeFactory {
static var treesTypes: [TreeType] = []
static func getTreeType(name: String, color: UIColor, texture: UIImage) -> TreeType {
var treeType: TreeType!
for type in treesTypes {
if type.name == name && type.color == color && type.texture == texture {
treeType = type
}
}
if treeType == nil {
treeType = TreeType(name: name, color: color, texture: texture)
treesTypes.append(treeType)
}
return treeType
}
}
Создадим класс лес, в котором мы и будем сажать наши деревья.
class Forest {
var trees: [Tree] = []
func plantTree(x: Int, y: Int, name: String, color: UIColor, texture: UIImage) {
let type = TreeFactory.getTreeType(name: name, color: color, texture: texture)
let tree = Tree(x: x, y: y, type: type)
trees.append(tree)
}
func draw(canvas: String) {
for tree in trees {
tree.draw(canvas: canvas)
}
}
}
В вызывающем коде мы легко создаем лес с различными деревьями.
let forest = Forest()
forest.plantTree(x: 4,
y: 4,
name: "fir",
color: .green,
texture: UIImage(named:"brown bark"))
forest.plantTree(x: 5,
y: 5,
name: "birch tree",
color: .white,
texture: UIImage(named:"white-black bark"))
forest.plantTree(x:
6, y:
6,
name: "fir",
color: .green,
texture: UIImage(named:"brown bark"))
forest.draw(canvas: "hill")
Заместитель / Proxy
Заместитель — это структурный паттерн проектирования, который позволяет подставлять вместо реальных объектов специальные объекты-заменители. Эти объекты перехватывают вызовы к оригинальному объекту, позволяя сделать что-то до или после передачи вызова оригиналу.
Для примера представим, что мы пишем программу для просмотра видео с видеохостинга.
Каждый раз когда пользователь выбирает понравившееся видео мы отправляем запрос на сервер, получаем ответ, загружаем видео показываем его.
Хорошо было бы сделать так, что если пользователь уже запрашивал какое то видео, не нужно было бы снова посылать запрос на сервер и грузить видео.
В этом нам может помочь паттерн заместитель, с помощью которого мы можем организовать кеширование ранее запрошенных видео.
Создадим протокол, описывающий возможные действия нашего класса.
protocol RuTubeLibaryProtocol {
func listVideos() -> [String]
func getVideoInfo(id: String) -> String
func downloadVideo(id: String) -> String
}
Затем создадим наш оригинальный класс, который ходит на сервер и грузит видео.
class RuTubeLibraryClass: RuTubeLibaryProtocol {
func listVideos() -> [String] {
print("Listing videos ...")
return ["Batman", "Spiderman", "Avangers"]
}
func getVideoInfo(id: String) -> String {
print("Info about video \(id) ...")
return "Info about video \(id)"
}
func downloadVideo(id: String) -> String {
print("Downloading video \(id) ...")
var video = ""
let list = listVideos()
if list.contains(id) {
if let v = list.first(where: { $0 == id }) {
video = v
}
}
return video
}
}
Создадим наш класс заместитель, который будет кешировавть запрошенные ранее видео, а если запрашиваемое видео новое, то использовать наш класс RuTubeLibraryClass для его загрузки.
class CachedRuTubeLibraryClass: RuTubeLibaryProtocol {
private let service = RuTubeLibraryClass()
private var listCache: [String] = []
private var videoInfoCache: [String] = []
private var videoCache: [String] = []
private var needUpdeate = false
func listVideos() -> [String] {
print("Listing videos from cash ...")
if listCache.isEmpty || needUpdeate {
listCache = service.listVideos()
}
return listCache
}
func getVideoInfo(id: String) -> String {
if !videoInfoCache.contains(id) || needUpdeate {
let info = service.getVideoInfo(id: id)
videoInfoCache.append(info)
}
var videoInfo = ""
if let i = videoInfoCache.firstIndex(where: { $0 == id}) {
videoInfo = videoInfoCache[i]
}
return videoInfo
}
func downloadVideo(id: String) -> String {
if !videoCache.contains(id) || needUpdeate {
let info = service.downloadVideo(id: id)
videoCache.append(info)
}
var video = ""
if let i = videoCache.firstIndex(where: { $0 == id}) {
video = videoCache[i]
}
return video
}
}
В вызывающем коде мы используем наш класс заместитель.
let rutubeLibrary = CachedRuTubeLibraryClass()
let filmList = rutubeLibrary.listVideos()
let batmanMovie = rutubeLibrary.downloadVideo(id: "Batman")
let spidermanInfo = rutubeLibrary.getVideoInfo(id: "Spiderman")
На этом про структурные паттерны все. Надеюсь, что данная статья поможет начинающим разработчикам разобраться в такой не самой простой теме как паттерну проектирования.