Как избежать стирания типов при реализации шаблона репозитория

Я пытаюсь реализовать шаблон репозитория в 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
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
225
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

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

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

Martin 06.04.2019 19:25
Ответ принят как подходящий

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

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

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

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

Martin 06.04.2019 19:45

Я бы сказал, что вы должны начать с того, чем на самом деле являются ваши специализированные репозитории, и строить их оттуда. Если вы создаете тонны стирок для шрифтов, вы почти наверняка делаете что-то не так. Как вы говорите, «вероятно, это излишне для моего варианта использования». Сосредоточьтесь на своем сценарии использования. Не создавайте проблем, которых у вас нет. Не существует такой вещи, как «общее» решение неограниченной проблемы. Когда вы накладываете узы на проблему, мы можем создавать решения, которые живут в этих рамках.

Rob Napier 06.04.2019 20:15

Итак, сколько реализаций репозитория у вас сейчас есть и какие они? (Если ответ «один», тогда остановитесь. Использование дженериков в этом случае очень вредно. Оно загоняет вас в рамки, и вам будет сложнее сделать систему гибкой в ​​будущем, когда вы обнаружите фактическую ось, вдоль которой вам нужна гибкость. )

Rob Napier 06.04.2019 20:16

Спасибо за совет Роб. У меня будет 6 репозиториев для этого приложения. Основная причина, по которой я создал это приложение, — улучшить свои знания Swift. Вот почему я не слишком беспокоюсь о том, что это перебор. По сути, это побочный проект обучения.

Martin 06.04.2019 20:48

Однако чрезмерность не означает более глубокое знание. Если вы действительно хотите расширить свое понимание Swift, создайте вариант использования, который действительно нуждается в гибкости, а затем изучите различные способы его решения. Я работал над этим проектом большую часть сегодняшнего дня, потому что я думаю, что его можно нарезать более разумно, чем ластик. Мой вариант использования — это один движок, который может поддерживать бэкэнд CloudKit и бэкэнд Dictionary. Вот мое текущее решение для полного стирания шрифтов, из которого я работаю над удалением ластика. gist.github.com/rnapier/03627315078286415214615399552ed7

Rob Napier 06.04.2019 20:55

Интересная суть. Спасибо, что поделились этим с нами. Я пересмотрю свою архитектуру. Может быть, я смогу придумать что-то более подходящее.

Martin 06.04.2019 21:11

Возможно, вы захотите прочитать мой обновленный раздел ДОПОЛНИТЕЛЬНЫЕ МЫСЛИ вверху. Я понял, что даже в моем варианте использования не было сопоставления с реальным миром (CloudKit так не работает).

Rob Napier 06.04.2019 21:48

Спасибо за ваши дальнейшие размышления. Мне нравится шаблон ластика типа stdlib, который вы там используете. Это не ограничивает один, когда дело доходит до именования методов и т. д.

Martin 07.04.2019 07:12

Другие вопросы по теме