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

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

Определение

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

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

Каждый объект в дереве имеет один и тот же интерфейс, поэтому клиенты могут рассматривать их как один и тот же тип.

Иллюстрация

Составной паттерн включает древовидную иерархию коллекций и отдельных объектов.

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

  • Иерархия - ›Справочник
  • Объект коллекции - ›Папка
  • Индивидуальный объект - ›mp3 файл

Это позволяет клиентам одинаково относиться к каждому иерархическому элементу.

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

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

UML

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

Примитив: ранее назывался «индивидуальный объект». Примитивы - это просто компоненты в дереве, не содержащие дочерних компонентов.

Составной: ранее назывался «объект коллекции». Композиты - это объекты, которые содержат массив компонентов. Хотя составные и примитивные объекты имеют один и тот же интерфейс, композиты содержат дополнительные методы для управления своими дочерними объектами.

Просто предупреждаем: есть несколько способов обращения к нашим конкретным компонентам:

Лично я предпочитаю термины «составной» и «примитивный». Разобравшись с этим, давайте перейдем к коду!

Реализация

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

Составная часть

protocol Payee {
  var name: String { get }
  var bonusAmount: Double { get }
  func receive(bonus: Double)
}

Сначала мы создаем Payee протокол, которому будут соответствовать наши составные и примитивные объекты. Это будет средство, с помощью которого мы будем относиться к ним единообразно.

Работник

class Employee: Payee {
  // 1
  private var _name: String
  private var _bonusAmount: Double = 0
  
  var name: String { return _name }
  var bonusAmount: Double { return _bonusAmount }
  
  init(name: String) {
    self._name = name
  }
  
  // 2
  func receive(bonus: Double) {
    _bonusAmount += bonus
  }
}

Давайте подробно рассмотрим наш код:

  1. Мы создаем сотрудника, соответствующего Payee, реализуя свойства name и bonusAmount.
  2. receive(bonus:) настроен как единственный способ манипулировать суммой бонусов каждого сотрудника.

отделение

Теперь давайте реализуем Department:

class Department: Payee {
  private var _name: String
// 1
  private var _bonusAmount: Double {
    get {
      var bonus = 0.0
      
      for subunit in subunits {
        bonus += subunit.bonusAmount
      }
      return bonus
    }
    set {
      let splitCount = Double(subunits.count)
      let splitBonus = newValue / splitCount
      for subunit in subunits {
        subunit.receive(bonus: splitBonus)
      }
    }
  }
  // 2
  private let subunits: [Payee]

  var name: String { return _name }
  var bonusAmount: Double { return _bonusAmount }
  
  init(name: String, subunits: [Payee] = []) {
    self._name = name
    self.subunits = subunits
  }
  
  func receive(bonus: Double) {
    _bonusAmount += bonus
  }
}

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

  1. Создать частную собственность _bonusAmount. Его получатель вычисляет бонус, обращаясь к subunits - набору подразделений и сотрудников - и возвращая сумму их бонусных сумм. Кроме того, мы включаем сеттер, который делит любой назначенный бонус и распределяет его по каждому дочернему компоненту.
  2. Department действует как наша совокупность, имея набор получателей.

Обратите внимание, что в отделе (и его подразделениях) нет никаких бонусов; они всегда распределяют бонус до тех пор, пока он не достигнет сотрудника.

Также стоит повторить, что композиты выходят за рамки согласования с общим протоколом. Они также реализуют методы для управления дочерними компонентами. Ожидается наличие таких методов, как add(Payee:) и remove(Payee:), но это выходит за рамки этого примера. Мы просто устанавливаем дочерние компоненты во время инициализации и предотвращаем дальнейшее изменение, делая subunits частной константой.

Использование составного паттерна

// 1
let joan = Employee(name: "Joan")
let tom = Employee(name: "Tom")
let cleo = Employee(name: "Cleo")
let alex = Employee(name: "Alex")

// 2
let graphicDesignDepartment = Department(name: "Graphic Design", subunits: [cleo, alex])
let marketingDepartment = Department(name: "Marketing", subunits: [joan, tom, graphicDesignDepartment])

// 3
marketingDepartment.receive(bonus: 1000)

Вот что мы сделали шаг за шагом:

  1. Создайте несколько сотрудников, часть из которых будет использоваться при создании отделов.
  2. Создайте простую древовидную иерархию: создайте маркетинговый отдел, содержащий отдел графического дизайна, с разбросанными по нему сотрудниками.
  3. Дайте отделу маркетинга бонус в размере 1000 долларов. Он автоматически распределяется между его подразделениями и сотрудниками. Подотделы далее делят премию, пока она не достигнет сотрудника.

Сценарий использования

Как обычно, важно понимать, когда применяется шаблон. Обратите внимание на следующие индикаторы:

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

Заключение

Ты сделал это! Теперь вы можете эффективно работать с древовидными структурами данных.

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

Первоначально опубликовано на emanharout.com.