Я пытаюсь реализовать шаблон репозитория в Swift в общем виде. Проблема, с которой я сейчас сталкиваюсь, заключается в том, что мне кажется, что мне нужно писать обертки стирания типов для всех моих репозиториев. Я что-то упустил здесь? Есть ли лучший способ сделать это или сделать компилятор счастливым на данный момент?
// 1
class Item {}
// 2
protocol Repository {
associatedtype T
}
// 3
protocol AnyItemRepository: Repository where T == Item {}
// 4
class ItemRepository: AnyItemRepository {
static let shared = ItemRepository()
private init() {}
}
// 5
class ViewController {
// 6
var itemRepository: AnyItemRepository? = ItemRepository.shared
}
Protocol 'AnyItemRepository' can only be used as a generic constraint because it has Self or associated type requirements
Вам не нужен тип AnyItemRepository
. Просто напишите методы расширения на Repository
вот так:
public extension Repository where T == Item {
func doSomethingSpecial(with items: [Item]) {
// blah blah
}
}
В вашем контроллере представления вы не можете использовать Repository
или AnyItemRepository
таким образом, потому что это ограничения универсального типа. Вы должны либо использовать конкретный тип, либо обобщённо параметризовать ViewController
.
class RepositoryViewController<R>: UIViewController where R: Repository, R.T == Item {
var itemRepository: R { get }
}
class ViewController: RepositoryViewController<ItemRepository> {
override var itemRepository: ItemRepository {
return ItemRepository.shared
}
}
(Выше приведен непроверенный псевдокод, предназначенный для того, чтобы дать вам представление о сути. Он никогда никем не запускался и, возможно, даже не компилируется.)
Не совсем понятно, для чего вы планируете использовать этот репозиторий. Репозиторий предназначен для сред с определенными характеристиками (например, сильными связями с базами данных строкового типа). Я расскажу о других шаблонах, которые часто лучше работают в обычных приложениях для iOS (и даже в небольших приложениях для Mac).
Обычно мне очень не нравится стирание шрифта, и это часто указывает на проблему дизайна. Но в этом случае я думаю, что стирание шрифта может быть разумным ответом.
Итак, мы начнем с видов предметов, которые мы можем хранить. Вероятно, им понадобится какой-то идентификатор и возможность хеширования для многих распространенных бэкендов (но, возможно, вам не понадобится хеширование; если нет, удалите его).
protocol Identified {
associatedtype ID
var id: ID { get }
}
typealias Storable = Identified & Hashable
А еще есть вещи, которые могут выступать в качестве хранилища. Не существует такого понятия, как «RepositoryStorage». Это просто говорит о том, что «если вы соответствуете этим требованиям, то Repository может вас использовать».
protocol RepositoryStorage {
associatedtype Item: Storable
func get(identifier: Item.ID) -> Item?
func add(item: Item)
func delete(identifier: Item.ID)
func allItems() -> [Item]
}
И затем стандартный, несколько утомительный шаблон стирания текста (есть еще один шаблон, который использует stdlib, еще более утомительный, но этот достаточно хорош для большинства случаев).
// I'm making it a class because I assume it'll have reference semantics.
final class Respository<Item: Storable>: RepositoryStorage {
init<Storage: RepositoryStorage>(storage: Storage) where Storage.Item == Item {
self._get = storage.get
self._add = storage.add
self._delete = storage.delete
self._allItems = storage.allItems
}
let _get: (Item.ID) -> Item?
func get(identifier: Item.ID) -> Item? { return _get(identifier) }
let _add: (Item) -> Void
func add(item: Item) { _add(item) }
let _delete: (Item.ID) -> Void
func delete(identifier: Item.ID) { _delete(identifier) }
let _allItems: () -> [Item]
func allItems() -> [Item] { return _allItems() }
}
Так что все в порядке, это репозиторий общего назначения. И это имеет разумный смысл, если вы имеете дело с большим набором элементов, которые, вероятно, будут храниться в базе данных SQLite. Но, по моему опыту, часто бывает и слишком много, и слишком мало. Слишком много, если это всего лишь несколько элементов, и слишком мало, если у вас много элементов, и поэтому, вероятно, вам придется делать гораздо больше, чем просто CRUD. Вероятно, вам нужен Query and Join, а этого недостаточно. (Делая что-то гибким в одном направлении, вы часто отрезаете себя в других направлениях. Не существует универсального «универсального».)
Так можем ли мы сделать это проще для случая, когда это действительно всего несколько элементов? Вот подход, который я использую регулярно:
class DataStore<Key: Hashable & Codable, Value: Codable> {
let identifier: String
private(set) var storage: DataStorage
var dictionary: [Key: Value] {
didSet {
storage[identifier] = try? PropertyListEncoder().encode(dictionary)
}
}
init(identifier: String, storage: DataStorage = UserDefaults.standard) {
self.identifier = identifier
self.storage = storage
let data = storage[identifier] ?? Data()
self.dictionary = (try? PropertyListDecoder().decode([Key: Value].self,
from: data)) ?? [:]
}
subscript(key: Key) -> Value? {
get { return dictionary[key] }
set { dictionary[key] = newValue }
}
}
Хранилище данных действует как словарь, в котором вы можете хранить пары ключ/значение:
let ds = DataStore<String: Item>(identifier: "Item")
ds["first"] = item
Он может хранить что угодно Codable. С небольшой модификацией вы можете переключить его с интерфейса, подобного словарю, на интерфейс, подобный массиву или набору; Я просто обычно хочу словарь.
Когда он обновляется, он кодирует все хранилище данных в свое хранилище как данные:
protocol DataStorage {
subscript(identifier: String) -> Data? { get set }
}
Это очень быстро и эффективно для десятков предметов. Я мог бы переосмыслить, если бы было более сотни предметов, и это было бы неуместно для сотен или более предметов. Но для небольших наборов это очень и очень быстро.
Очень распространенным DataStorage является UserDefaults:
extension UserDefaults: DataStorage {
subscript(identifier: String) -> Data? {
get { return data(forKey: identifier) }
set { set(newValue, forKey: identifier) }
}
}
Ключевой урок заключается в том, что это избавляет от всех прыжков со стиранием типов, создавая общую валюту (данные), с которой работают исключительно нижние слои. В любое время, когда вы можете сделать это, отделив универсальный интерфейс верхнего уровня от неуниверсального интерфейса нижнего уровня, вы избавите себя от утомительной работы.
Это может или не может работать для вашей ситуации. Он адаптирован для хранилищ ключей/значений, а не для баз данных, и предназначен для чтения гораздо чаще, чем для записи. Но для этого использования он намного проще и, как правило, быстрее, чем шаблон репозитория. Именно такие компромиссы я имею в виду, когда говорю, что важно знать свой вариант использования.
Спасибо за ваш ответ Роб. Я очень ценю усилия, которые вы вложили в это. Вероятно, этот шаблон совершенно излишен для моего варианта использования, но я хочу потратить на него столько времени, сколько потребуется, чтобы действительно понять его. Моя кодовая база уже выглядит почти так же, как вы предложили сначала. Я также считаю, что можно написать оболочку стирания типов для репозитория общего назначения. Однако необходимость писать обертки стирания типов для каждого специализированного репозитория кажется мне ошибкой. Вы бы сказали, что я должен сделать это в любом случае, и это просто неудобство, которое я должен принять?
Я бы сказал, что вы должны начать с того, чем на самом деле являются ваши специализированные репозитории, и строить их оттуда. Если вы создаете тонны стирок для шрифтов, вы почти наверняка делаете что-то не так. Как вы говорите, «вероятно, это излишне для моего варианта использования». Сосредоточьтесь на своем сценарии использования. Не создавайте проблем, которых у вас нет. Не существует такой вещи, как «общее» решение неограниченной проблемы. Когда вы накладываете узы на проблему, мы можем создавать решения, которые живут в этих рамках.
Итак, сколько реализаций репозитория у вас сейчас есть и какие они? (Если ответ «один», тогда остановитесь. Использование дженериков в этом случае очень вредно. Оно загоняет вас в рамки, и вам будет сложнее сделать систему гибкой в будущем, когда вы обнаружите фактическую ось, вдоль которой вам нужна гибкость. )
Спасибо за совет Роб. У меня будет 6 репозиториев для этого приложения. Основная причина, по которой я создал это приложение, — улучшить свои знания Swift. Вот почему я не слишком беспокоюсь о том, что это перебор. По сути, это побочный проект обучения.
Однако чрезмерность не означает более глубокое знание. Если вы действительно хотите расширить свое понимание Swift, создайте вариант использования, который действительно нуждается в гибкости, а затем изучите различные способы его решения. Я работал над этим проектом большую часть сегодняшнего дня, потому что я думаю, что его можно нарезать более разумно, чем ластик. Мой вариант использования — это один движок, который может поддерживать бэкэнд CloudKit и бэкэнд Dictionary. Вот мое текущее решение для полного стирания шрифтов, из которого я работаю над удалением ластика. gist.github.com/rnapier/03627315078286415214615399552ed7
Интересная суть. Спасибо, что поделились этим с нами. Я пересмотрю свою архитектуру. Может быть, я смогу придумать что-то более подходящее.
Возможно, вы захотите прочитать мой обновленный раздел ДОПОЛНИТЕЛЬНЫЕ МЫСЛИ вверху. Я понял, что даже в моем варианте использования не было сопоставления с реальным миром (CloudKit так не работает).
Спасибо за ваши дальнейшие размышления. Мне нравится шаблон ластика типа stdlib, который вы там используете. Это не ограничивает один, когда дело доходит до именования методов и т. д.
Спасибо за ответ Григорий. Я уже пробовал очень похожее решение, подобное тому, которое вы предлагаете, и мне очень нравится эта идея. Однако оказывается, что универсальные параметризованные классы не поддерживаются раскадровками.