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

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

Иллюстрация

Для начала воспользуемся классическим примером текстового редактора. Текстовые редакторы создают и повторно используют все 26 букв алфавита. Например, при вводе «HELLO WORLD» мы воссоздаем символ «L» три раза. Это расточительно, потому что мы создаем три символьных объекта, представляющих одну и ту же букву. Целью легковесного шаблона является совместное использование многократно используемых объектов вместо их бесполезного дублирования, что позволяет нашему текстовому редактору быть легковесным.

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

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

Обобщить:

  • Внутренние данные неизменяемы, идентичны, не зависят от контекста и, как следствие, могут использоваться повторно.
  • Внешние данные изменчивы и контекстуальны и, как следствие, не могут использоваться повторно во всех случаях.

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

Реализация

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

protocol Soldier {
  func render(from location: CGPoint, to newLocation: CGPoint)
}

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

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

Наилегчайший вес

Давайте посмотрим, как выглядит наш объект Flyweight:

class Infantry: Soldier {
  
  private let modelData: Data
  
  init(modelData: Data) {
    self.modelData = modelData
  }
  
  func render(from location: CGPoint, to newLocation: CGPoint) {
    // Remove rendering from original location
    // Graphically render at new location
  }
}

Наш Infantry соответствует Soldier и действует как наш легковесный объект. Как наш легковес, он хранит внутренние данные в свойстве modelData. Это фиктивное свойство содержит графику для рендеринга пехоты. Поскольку все пехотные подразделения нашей армии выглядят одинаково, мы используем одну модель для их визуализации.

Об объектах-легковесах следует обратить внимание на две вещи:

  1. Они содержат ссылки исключительно на внутреннее состояние.
  2. Они должны взаимодействовать с внешним состоянием

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

Клиент

Нам все еще не хватает клиента для хранения всех наших внешних данных.

class SoldierClient {
  
  // 1
  var currentLocation: CGPoint
  
  // 2
  let soldier: Soldier
  
  init(currentLocation: CGPoint, soldier: Soldier) {
    self.currentLocation = currentLocation
    self.soldier = soldier
  }
  
  // 3
  func moveSoldier(to nextLocation: CGPoint) {
    soldier.render(from: currentLocation, to: nextLocation)
    currentLocation = nextLocation
  }
}

Приведенный выше код выполняет следующее:

  1. Сохраните CGPoint, представляющий текущее местоположение солдата.
  2. Сохраните ссылку на объект-легковес.
  3. Создайте функцию-оболочку, которая передает новое местоположение легковесу солдата. Легковес солдата удаляет солдата с его исходного местоположения и перерисовывает его на новом месте. Впоследствии currentLocation обновляется, чтобы отразить новое местоположение.

Все внешние данные хранятся в клиенте, но используются легковесом по мере необходимости. Другими словами, несколько клиентов могут повторно использовать легковес для рендеринга пехоты в желаемых местах.

Фабрика

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

Вот где становится полезным фабричный объект:

class SoldierFactory {
  
  // 1
  enum SoldierType {
    case infantry
  }
  
  // 2
  private var availableSoldiers =  [SoldierType: Soldier]()
  
  // 3
  static let sharedInstance = SoldierFactory()
  
  private init(){}
  
  private func createSoldier(of type: SoldierType) -> Soldier {
    switch type {
    case .infantry:
      let infantry = Infantry(modelData: Data())
      availableSoldiers[type] = infantry
      return infantry
    }
  }
  
  // 4
  func getSoldier(type: SoldierType) -> Soldier {
    if let soldier = availableSoldiers[type] {
      return soldier
    } else {
      let soldier = createSoldier(of: type)
      return soldier
    }
  }
}

Задача фабричного объекта - обеспечить создание только одного конкретного солдата заданного типа. Если объект не существует, он создается, добавляется в словарь availableSoldiers и возвращается вызывающей стороне. В следующий раз, когда другому клиенту понадобится пехота, он просто повторно использует существующую. Вот пошаговая разбивка:

  1. Составьте список всех возможных конкретных солдат. У нас есть только один, но вы можете представить, что со временем будет добавляться больше (Лучники, кто-нибудь?)
  2. Храните частный словарь, содержащий всех созданных солдат.
  3. Мы заботимся о том, чтобы наша фабрика была одноэлементной. Мы хотим, чтобы все вызывающие объекты ссылались на один и тот же пул объектов, чтобы они также совместно использовали фабрику. Для правильного обращения с одиночками, пожалуйста, прочтите Swift Solutions: Singleton.
  4. Каждый раз, когда клиент запрашивает солдата, мы проверяем, существует ли он уже в availableSoldiers. Если нет, мы создаем экземпляр и сохраняем его в словаре, а затем возвращаем его. Все будущие запросы пехоты используются повторно, а не заново.

использование

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

let soldierFactory = SoldierFactory.sharedInstance
let infantry = soldierFactory.getSoldier(type: .infantry)
let firstSoldier = SoldierClient(currentLocation: CGPoint.zero, soldier: infantry)
let secondSoldier = SoldierClient(currentLocation: CGPoint(x: 5, y: 10), soldier: infantry)

firstSoldier.moveSoldier(to: CGPoint(x: 1, y: 5))

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

Последние мысли

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

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

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

Первоначально опубликовано на сайте emanharout.com 16 сентября 2017 г.