Skip to content

j-jeudesprit/Structural-patterns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 

Repository files navigation


Ссылки на конкретный паттерн:


Адаптер — это структурный паттерн проектирования, который преобразует интерфейс одного класса в интерфейс другого, который ожидают клиенты.

Представим, что мы разрабатываем графический редактор. Большая часть отображаемых элементов нашего графического редактора реализуют некоторый интерфейс Shape. Вот мы реализовали класс Line, который реализует наш интерфейс Shape. Теперь мы хотим реализовать класс для отображения текста -TextShape. Мы немного подумали и поняли, что это задача достаточно сложна для нас и мы бы хотели для этих целей использовать уже готовую библиотеку для работы с текстом. Но вот незадача, в коде мы работали лишь с интерфейсам типа Shape, а наша библиотека никогда не слышала об этой интерфейсе, да и не должна была. Возникает вопрос, как же заставить работать библиотеку в нашем случаи. Этот вопрос решает паттерн Адаптер.

Суть Адаптера заключается в том, что он реализует наш интерфейс Shape в терминах нашей новой библиотеки, скажем с классом TextView(класс нашей библиотеки). Для этих целей мы создаём наш класс TextShape, который реализует интерфейс Shape и наследуется от класса TextView. Давайте посмотрим:

protocol Shape {
    // ...
}

class TextView {
    // ...
}

class TextShape: TextView, Shape {
    // ...
}

Теперь осталось реализовать методы интерфейса Shape используя возможности класса TextView. Получившийся класс TextShape будет удовлетворять нашему интерфейсу и работать с новой библиотекой.

Нужно понимать, что Адаптер может работать как с классом нашей новой библиотеки, так и с объектами этого класса. Пример выше - работа с классом. Работа с объектом подразумевает хранение объекта класса TextView. Давайте посмотрим на это:

protocol Shape {
    // ...
}

class TextView {
    // ...
}

class TextShape: Shape {
    private let textView: TextView

    init(textView: TextView) {
        self.textView = textView
    }

    // ...
}

Так в чём же их различие? Если мы адаптируем класс, то в таком случаи мы может переопределять некоторую функциональность наследуемого класса, в случаи же адаптирования по второму типу, мы этого сделать не можем, т.к. мы уже не наследуемся, а имеем лишь ссылку на объект. Но при этом, если мы адаптируемся по второму типу, мы сможем работать с подклассами TextView, т.к. мы содержим ссылку на базовый класс, то и любой подкласс так же будет удовлетворять этому типу, что недоступно при адаптировании класса, в этом случаи мы к нему жёстко привязаны.

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


Мост — это структурный паттерн проектирования, который разделяет абстракцию и реализацию на две иерархии.

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

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

protocol Shape {
    func draw()
    // ...
}

//#######################

class Circle: Shape {
    func draw() {
        // ...
    }

    // ...
}

class Square: Shape {
    func draw() {
        // ...
    }

    // ...
}

//#######################

class CircleWindowsDraw: Circle {
    override func draw() {
        // ...
    }

    // ...
}

class CircleLinuxDraw: Circle {
    override func draw() {
        // ...
    }

    // ...
}

class SquareWindowsDraw: Square {
    override func draw() {
        // ...
    }

    // ...
}

class SquareLinuxDraw: Square {
    override func draw() {
        // ...
    }

    // ...
}

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

Другим пример будет написание предметов и их экзаменов. Представим интерфейс - Предмет. Реализуем наш интерфейс конкретными предметам: Физика, Математика, Русский Язык, ... . Каждый из предметов может сдаваться как в виде устного экзамена, так и письменного. И снова для каждого из предметов нам нужно писать по два класса с нужной реализацей сдачи экзамена. Получим клубок взаимосвязанных классов.

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

protocol Shape {
    var implement: Draw { get }
    func draw()
    // ...
}

//#######################

protocol Draw {
    func drawFigure()
    // ...
}

//#######################

class WindowsDraw: Draw {
    func drawFigure() {
        // ...
    }

    // ...
}

class LinuxDraw: Draw {
    func drawFigure() {
        // ...
    }

    // ...
}

//#######################

class Circle: Shape {
    let implement: Draw

    init(draw: Draw) {
        self.implement = draw
    }

    func draw() {
        implement.drawFigure()
        // ...
    }

    // ...
}

class Square: Shape {
    let implement: Draw

    init(draw: Draw) {
        self.implement = draw
    }

    func draw() {
        implement.drawFigure()
        // ...
    }

    // ...
}

Как видно, абстракция и реализация теперь отделены и не зависят друг от друга. Мы с лёгкость можем добавлять новые фигуры и добавлять новые платформы. Обе стороны моста могут наращивать функционал независимо. При этом, мы всегда может добавить нужный нам отрисовщик в нашу фигуру, не зная заранее какой из них нам может понадобиться.

// Main

let draw = LinuxDraw()  
// с другой стороны мы могли бы сделать и так WindowsDraw()  
// и нам непришлось ничего менять в нашей абстракции

let circle = Circle(draw: draw)
let square = Square(draw: draw)  

circle.draw()
square.draw()

Компоновщик — это структурный паттерн проектирования, компонующий объекты для представления древовидной иерархии. Позволяет единообразно работать как с обычными так и составными объектами.

Продолжим тему графических примитивов. Представим, что мы разрабатываем графический редактор, который позволяет строить сложные схемы из более простых примитивов. Для начала это текст, линии, фигуры - простые графические примитивы. Помимо них есть составные объекты, которые могут содержать в себе какое-то количество простых примитивов. Далее мы можем построить ещё более сложные объекты, которые в свою очередь будут содержать составные объекты. Таким образом мы можем добиться любой сложности.

Возникает проблема, для каждого типа объектов, будь то примитив или составной объект, мы работает с каждым из них по разному. И примитива одни операции, и составных другие. Хотелось бы иметь возможность работать с любым типом объектов нашей структуры одинаково. Решить эту проблему помогает паттерн Компоновщик.

Компоновщик определяет общий интерфейс объектов иерархии, которые реализуют абсолютно всё объекты, будь то примитив или составной объект.

@objc protocol Graphic {
    func draw()
    @objc optional func add(_ child: Graphic)
    @objc optional func remove(_ child: Graphic)
    @objc optional func getChild(_ index: Int)
}

// Примитивы

class Line: Graphic {
    func draw() {
        // ...
    }
}

class Rectangle: Graphic {
    func draw() {
        // ...
    }
}

class Text: Graphic {
    func draw() {
        // ...
    }
}

// Составной

class Picture: Graphic {
    private var childs = [Graphic]()

    func draw() {
        // вызываем draw() для каждого объекта внутри нас
    }

    func add(_ child: Graphic) {
        // ...
    }

    func remove(_ child: Graphic) {
        // ...
    }

    func getChild(_ index: Int) {
        // ...
    }
}

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


Декоратор — это структурный паттерн проектирования, динамически добавляющий классу новую функциональность.

Итак, допустим мы работаем над некоторой системой оповещений. Изначально мы реализовали лишь три типа оповещений, это почта, SMS и push-уведомления. Пока всё идёт по плану. Мы работаем с единым интерфейсом Notification, а соответствующие классы реализуют нужный функционал. В какой-то момент кто-то захотел получать сразу несколько видов оповещений. Одним нужны были SMS и почта, другим же, почта и push-уведомления. Хорошо - сказали мы, и реализовали новый класс для каждой из этих комбинаций(множественного наследования у нас нет). Теперь в систему добавили кучу других разновидностей оповещений и стало быть, появилось ещё больше их комбинаций, что в свою очередь приводит к огромному числу классов.

protocol Notificator {
    func send()
    // ...
}

// Базовые

class SMS: Notificator {
    func send() {
        // ...
    }
}

class Email: Notificator {
    func send() {
        // ...
    }
}

class Push: Notificator {
    func send() {
        // ...
    }
}

// Комбинации

// email+SMS, email+push, ...
// ...

Причём неважно, как вы будете делать комбинации, в виде отдельных классов или придумаете что-то ещё. Проблема будет в нарастании сложности и количества таких комбинаций.

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

Декоратор определяет интерфейс, который наследует основной интерфейс оповещений - Notification. Более того Декоратор в своём интерфейсе обязывает хранить ссылку на объект типа Notification, тем самым делегируя работу этому объекту. Но сама суть заключается в том, что перед делегированием или после, мы можем добавить свою функциональность. То есть мы оборачиваем один объект типа Notification в другой и можем делать это динамически.

protocol Notificator {
    func send()
    // ...
}

protocol Decorator: Notificator {
    var wrapper: Notificator! { get }
    // ...
}

extension Decorator {
    // Определяем базовую функциональность(альтернатива абстрактным классам др. языков).
    func send() {
        wrapper.send()
    }
}

// Базовые

class SMS: Decorator {
    var wrapper: Notificator!

    init() { }

    init(wrapper: Notificator?) {
        self.wrapper = wrapper
    }

    func send() {
        print("SMS+", terminator: " ")
        if let wrap = wrapper {
            wrap.send()
        }
        // ...
    }

    // ...
}

class Email: Decorator {
    var wrapper: Notificator!

    init() { }

    init(wrapper: Notificator?) {
        self.wrapper = wrapper
    }

    func send() {
        print("Email+", terminator: " ")
        if let wrap = wrapper {
            wrap.send()
        }
        // ...
    }

    // ...
}

class Push: Decorator {
    var wrapper: Notificator!

    init() { }

    init(wrapper: Notificator?) {
        self.wrapper = wrapper
    }

    func send() {
        print("Push+", terminator: " ")
        if let wrap = wrapper {
            wrap.send()
        }
        // ...
    }

    // ...
}

// Main

let sms = SMS()
sms.send() // SMS+

let smsAndEmail = SMS(wrapper: Email())
smsAndEmail.send() // SMS+Email

let smsAndEmailAndPush = SMS(wrapper: Email(wrapper: Push()))
smsAndEmailAndPush.send() // SMS+Email+Push
// ...

Как видно, объекты sms, smsAndEmail и smsAndEmailAndPush одного типа - Notificator, что позволяет легко работать с ними в коде. При этом мы динамически может оборачивать один объект в другой, что бы придать ему ту или иную функциональность.


Фасад — это структурный паттерн проектирования, предоставляющий унифицированный интерфейс, вместо набора интерфейсов некоторой подсистемы. Фасад является интерфейсом более высокого уровня.

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

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


Приспособленец — это структурный паттерн проектирования, использующий разделение для поддержания множества мелких объектов.

Во многих приложения, в особенности играх, покрытие всего что можно - объектами, бывает очень полезным, но недопустимо расточительным решением. Возьмём к примеру символы в текстовом редакторе или частицы в некоторой игре. Было бы здорово, если каждый символ(частица) были бы представлены отдельным объектом, что давало бы полный контроль над происходящим. Однако прямая реализация такого подхода очень сильно скажется на производительности приложения, ведь число символов(частиц) может быть просто огромное количество и было бы неверным решеним для такого количества иметь объект представляющий каждый из них, будь то символ или частица. Для решения возникшей проблемы обычно применяют паттерн Приспособленец.

class NonFlyweight {
    private var externalState: Any!
    private var internalState: Any!

    func operation1() {
        // ...
    }

    func operation2() {
        // ...
    }

    // ...
} 

Паттерн Приспособленец вводит такие понятия, как внутренние и внешние состояния. Внутренние состояния - всё что не зависит от контекста и остаётся в самом объекте, внешние же состояния напрямую зависят от контекста и выносятся из объекта и становятся входными параметрами. Вспомним наш объект представляющий символ в текстовом редакторе. Размер символа, его шрифт, начертание - всё это внешние состояния и они зависят от контекста в котором находится этот символ. Тогда как представление самого символа, его код в некоторой кодировке, является внутренним состоянием и не изменяется от контекста к контексту.

class Flyweight {
    private var internalState: Any!

    func operation1(with externalState: Any?) {
        // ...
    }

    func operation2(with externalState: Any?) {
        // ...
    }

    // ...
} 

Возвращаясь к символам, нам остаётся создать такой класс Приспособленец, для каждого символа, скажем ASCII. При таком подходе число объектов будет куда меньше, чем если бы мы создавали объекты под каждый символ в тексте. Теперь каждый символ текста будет ссылаться на конкретный, нужный ему символ(объект Приспособленец) и передавать ему внешнее состояние.

Иногда бывает удобно создать фабрику, где в качестве параметра будет передаваться внешнее состояние.

final class FlyWeightFactory {
    private var cache = [Flyweight]()

    // internalState нужен для поиска нужного приспособленца в кэше
    func getFlyWeight(_ internalState: Any?, with externalState: Any?) -> Flyweight {
        // Если его нет в кэше, создаём его и помещаем туда
        // А если есть - вернём из кэша
    }
}

protocol Flyweight {
    var internalState: Any! { get }
    func operation1(with externalState: Any?)
    func operation2(with externalState: Any?)
}

class Flyweight1: Flyweight {
    var internalState: Any!

    func operation1(with externalState: Any?) {
        // ...
    }

    func operation2(with externalState: Any?) {
        // ...
    }

    // ...
}

class Flyweight2: Flyweight {
    var internalState: Any!

    func operation1(with externalState: Any?) {
        // ...
    }

    func operation2(with externalState: Any?) {
        // ...
    }

    // ...
}


Заместитель — это структурный паттерн проектирования, который создаёт суррогат другого объекта и контролирует доступ к нему. Паттерн Заместитель применяется всегда, когда требуется более сложный доступ к объекту, нежели ссылка на него.

Представьте, что вы отображаете некоторый документ, где помимо текста, есть множество картинок. Любой документ должен открываться настолько быстро, насколько это возможно. Для того, что бы этого добиться, мы не будем отображать картинки в документе напрямую, зная что это затратная операция. Мы будем создавать такие объекты по требованию, да и вообще в один конкретный момент на экране мы видим лишь часть документа, соотвественно смысла в загрузке всех - нету никакого. Так как же создавать изображения по требования? При чём наша оптимизация не должна отражаться на коде, который мы уже написали, до этого момента. В этом нам поможет паттерн Заместитель.

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

protocol Graphic {
    func draw()
    func getExtent()
    func store()
    func load()
}

class Image: Graphic {
    private var extent: Any!

    func draw() {
        // ...
    }

    func getExtent() {
        // ...
    }

    func store() {
        // ...
    }

    func load() {
        // ...
    }
}

class ImageProxy: Graphic {
    private var extent: Any!
    private var image: Image!

    func draw() {
        if image == nil {
            image = Image()
        }
        image.draw()
        // ...
    }

    func getExtent() {
        if image == nil {
            print(extent)
        } else {
            print(image.getExtent())
        }
        // ...
    }

    func store() {
        // ...
    }

    func load() {
        // ...
    }
}

В нашем коде ничего не поменяется, вот только теперь мы будем использовать наш заместитель, который по требованию будет создавать изображения. У этого паттерна ещё достаточно много применений, но все они заключаются в одном, в использовании паттерна тогда, когда нужен более сложный доступ к объекту, нежели по прямой ссылке на него.

About

Structural patterns on Swift

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published