Компоновщик
Published: 3 November 2017 Tagged:
PatterniOSSwift
Часто в приложениях встречаются табличные представления UITableView, UICollectionView. В зависимости от задачи я использовал в качестве структуры данных массив либо словарь. Это решение несло ряд ограничений и не было гибким. Для меня более удобным стало использование компоновщика. Проект с примером на Git-Hub :octocat:
Компоновщик — структурный паттерн проектирования. Он позволяет сгруппировать объекты в древовидную структуру, а затем работать с ними как с единым объектом.
Теория
Как будет выглядеть наше дерево? Табличное представление состоит из списка секций. В свою очередь, секции содержат модели ячеек (тоже самое для коллекций). Таким образом нам нужны элементы двух типов которые могут содержать дочерние элементы (контейнеры) и конечные элементы (листья) в которые нельзя добавлять детей.
Контейнер (Композит) — составной элемент дерева. Содержит дочерние элементы — Листья или другие Контейнеры — но не знает, какие именно, так как работает с ними только через общий интерфейс Компонента (CompoundItemProtocol). Методы этого класса переадресуют основную работу своим дочерним компонентам, хотя и могут добавлять что-то своё к результату. Таким образом, паттерн Компоновщик позволяет нам работать с деревьями объектов любой сложности, не обращая внимание на конкретные модели, формирующие дерево.
Реализация
Напишем общий интерфейс для элементов дерева CompoundItemProtocol, все элементы нашей структуры должны релиазовать этот протокол. Так же создадим перечисление с доступными уровнями вложенности CompoundItemLevel.
// уровень вложенностиenumCompoundItemLevel{caseroot,section,item}protocolCompoundItemProtocol{// уникальный идентификаторvaridentifier:String{get}// уровень на котором находится элементvarlevel:CompoundItemLevel{get}// список дочерних элементов элемента дереваvarchildren:[CompoundItemProtocol]{get}// список элементов для работыvaritems:[CompoundItemProtocol]{get}// добавить дечерний элементfuncadd(_model:CompoundItemProtocol...)// удалить дочерний элементfuncremove(_model:CompoundItemProtocol)}// Возможность сравнивать объекты с протоколом CompoundItemProtocol func==(lhs:CompoundItemProtocol,rhs:CompoundItemProtocol)->Bool{returnlhs.identifier==rhs.identifier}
Компонент определяет общий интерфейс для простых и составных компонентов дерева.
Так как все модели должны его реализовывать - напишем базовый каласс BaseCompoundItem на базе которого будем добавлять необходимые модели нашего дерева:
classBaseCompoundItem:CompoundItemProtocol{varchildren:[CompoundItemProtocol]=[]letlevel:CompoundItemLevelletidentifier:Stringvaritems:[CompoundItemProtocol]{returnchildren}// инициализация без параметров создает рутовый элемент структурыinit(){self.identifier="root"self.level=.root}init(identifier:String,level:CompoundItemLevel){self.identifier=identifierself.level=level}funcadd(_model:CompoundItemProtocol...){self.children.append(contentsOf:model)}funcremove(_model:CompoundItemProtocol){ifletindex=self.children.index(where:{$0==model}){children.remove(at:index)}}}
Контейнер (или Композит) — это составной элемент дерева.
Базовая реализация готова. К ней можно добавить конкретные модели: секций и ячееки. Для создания модели элементов дерева будем использовать паттерн декоратор, чтобы расширить нашу базовую реализацию BaseCompoundItem. Этот структурный паттерн позволит динамически добавить объектам новую функциональность, используя так называемые обвертки.
Напишем модель для секции:
// Сделаем нашей модели read only протокол, только с необходимыми полямиprotocolSectionCompoundItemProtocol{varheader:String?{get}varfooter:String?{get}}classSectionCompoundItem:CompoundItemProtocol{privateletdecoratedComposite:CompoundItemProtocol// основные поля которые мы добавили с помощью декоратораvarheader:String?varfooter:String?varidentifier:String{returnself.decoratedComposite.identifier}// из нашего декарируемого объекта будем рекурсивно // запрашивать элементы, вернет только листьяvarchildren:[CompoundItemProtocol]{returnself.decoratedComposite.children}varitems:[CompoundItemProtocol]{returnself.decoratedComposite.children.flatMap{$0.items}}varlevel:CompoundItemLevel{returnself.decoratedComposite.level}init(identifier:String,header:String?,footer:String?){// Для упрощения мы не будем передавать декарируемый // объект, а просто созданим его с нужным уровнем вложенностиself.decoratedComposite=BaseCompoundItem(identifier:identifier,level:.section)self.header=headerself.footer=footer}// В секцию можно добавить все кроме секций и корневых элементовfuncadd(_model:CompoundItemProtocol...){foriteminmodel{ifitem.level!=.section&&item.level!=.root{self.decoratedComposite.add(item)}else{print("You can`t add section in other section")}}}funcremove(_model:CompoundItemProtocol){self.decoratedComposite.remove(model)}}
Модель секции тоже является контейнером так как может содержать дочернии элементы.
И модель для ячейки:
// Сделаем нашей модели read only протокол, только с необходимыми полямиprotocolCellCompoundItemProtocol{vartitle:String?{get}varsubtitle:String?{get}varcellIdentifier:String{get}}classCellCompoundItem:CompoundItemProtocol,CellCompoundItemProtocol{privateletdecoratedComposite:CompoundItemProtocolvartitle:String?varsubtitle:String?letcellIdentifier:Stringvaridentifier:String{returnself.decoratedComposite.identifier}// это лист дерева так что возвращаем самого себяvartems:[CompoundItemProtocol]{return[self]}// детей у листов быть не можетvarchildren:[CompoundItemProtocol]{return[]}varlevel:CompoundItemLevel{returnself.decoratedComposite.level}init(identifier:String,cellIdentifier:String,title:String?,subtitle:String?){self.decoratedComposite=BaseCompoundItem(identifier:identifier,level:.item)self.title=titleself.subtitle=subtitleself.cellIdentifier=cellIdentifier}funcadd(_model:CompoundItemProtocol...){print("Нельзя добавить дочерние элементов, этот элемент является листом структуры")}funcremove(_model:CompoundItemProtocol){print("У листьев нет детей, нечего удалять")}}
Модель ячейки является листом – это простой элемент дерева, не имеющий ответвлений.
Использование
Код написан, можно опробовать нашу новую структуру данных:
// создаем корневой контейнерself.datasource=BaseCompoundItem()// создаем модели секцииletsection1=SectionCompoundItem(identifier:"Section1",header:"Вселенная MARVEL",footer:"Marvel Comics — американская компания, издающая комиксы, подразделение корпорации «Marvel Entertainment»")letsection2=SectionCompoundItem(identifier:"Section2",header:"Вселенная DC",footer:"DC Comics — одно из крупнейших и наиболее популярных издательств комиксов, наравне с Marvel Comics")// создаем модели ячеекletcap=CellCompoundItem(identifier:"cell2",cellIdentifier:"CellID",title:"Captain America",subtitle:"Стивен Роджерс")letbatman=CellCompoundItem(identifier:"cell5",cellIdentifier:"CellID",title:"Batman",subtitle:"Брюс Вэйн")// добавляем в секции модели ячеек и в корневой контейнер секцииsection1.add(cap)section2.add(batman)datasource.add(section1,section2)
Теперь выполняем tableView.reloadData(), и готово. Посмотреть пример кода можно тут
В заключении
Что полезного можно добавить:
Фильтры в модели ячеек. Добавим дополнительный параметр isVisible — от него зависит, будут ли модели возвращаться.
Проверка валидации всех полей таблицы. Добавляем новый метод func isValid() -> Bool в CompoudItemProtocol. Реализуем его во всех моделях ячеек нашего дерева.
Проверка изменений данных таблицы при создании сложных форм ввода данных (создается по аналогии с п. 2).