Предотвращение стирания типов при реализации шаблона репозитория - PullRequest
2 голосов
/ 06 апреля 2019

Я пытаюсь реализовать шаблон репозитория в 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

}
  1. Одна из многих сущностей
  2. Базовый интерфейс репозитория, который может быть расширен при необходимостиили реализовано напрямую
  3. Специальный интерфейс хранилища элементов, который гарантирует дополнительные функциональные возможности поверх базового хранилища
  4. Конкретная реализация хранилища для определенного типа сущности
  5. Некоторый класс, к которому требуется доступданные
  6. Зависимость от любого хранилища элементов.Ошибки компилятора в этой строке: Protocol 'AnyItemRepository' can only be used as a generic constraint because it has Self or associated type requirements

Ответы [ 2 ]

0 голосов
/ 06 апреля 2019

Не совсем понятно, для чего вы планируете использовать этот репозиторий.Репозиторий предназначен для сред, которые имеют определенные характеристики (например, сильные связи с базами данных в стиле строк).Я расскажу о других шаблонах, которые часто работают лучше в обычных приложениях для 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 }
    }
}

DataStore действует как словарь, в котором можно хранить пары ключ / значение:

let ds = DataStore<String: Item>(identifier: "Item")
ds["first"] = item

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

Когда он обновляется, он кодирует все хранилище данных в свое хранилище как Данные:

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) }
    }
}

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

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

0 голосов
/ 06 апреля 2019

Вам не нужен тип 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
   }
}

(Вышеприведенный псевдокод, разработанный для того, чтобы дать вам суть. Он никогда не использовался кем-либо и может даже не компилироваться.)

...