Можете ли вы определить перечисление для представления значений, явно известных вашему приложению, но по-прежнему обрабатывать неизвестные значения, декодированные из серверной части? - PullRequest
2 голосов
/ 11 апреля 2020

Можете ли вы определить перечисление для представления известных значений свойства в вашей модели, в то же время позволяя возвращать неизвестные значения из бэкэнда?

Краткий ответ: Да, вы можете !

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

Однако в нашем приложении вместо того, чтобы иметь дело с беспорядочными строковыми константами, мы хотим определить эти значения как перечисление, которое мы помечаем как Codable, поэтому компилятор автоматически обрабатывает кодирование / декодирование для фактических случаев перечисления.

Вот типичное перечисление для такого сценария ios ...

enum FeatureFlag : String, CaseIterable, Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
}

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

Есть несколько способов справиться с этим сценарием:

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

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

Но что если есть номер пять? Что если вы можете сделать перечисление само по себе изящно обрабатывать неизвестные значения во время выполнения, оставаясь без потерь в процессе и не прибегая к дополнительным функциям?

Ну, это точное решение Представляю ниже ! Наслаждайтесь!

Ответы [ 2 ]

3 голосов
/ 11 апреля 2020

Как уже упоминалось выше, наше приложение имеет определенный набор известных флагов функций. Сначала их можно определить так:

enum FeatureFlag : String, CaseIterable, Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
}

Достаточно просто. Но опять же, теперь любое значение, определенное с типом FeatureFlag, может обрабатывать только один из этих указанных c известных типов.

Теперь, скажем, благодаря новой функции в бэкэнде, новый флаг allowsSavings определяется и передается в ваше приложение. Если вы вручную не написали логи декодирования c (или не использовали дополнительные опции), декодер не сможет работать.

Но что, если вам не нужно было их писать? Что если enum может обрабатывать неизвестные случаи автоматически?

Хитрость заключается в том, чтобы определить один дополнительный случай other со связанным значением типа String. Этот новый регистр обрабатывает все неизвестные типы, передаваемые ему при декодировании.

Вот наш обновленный enum:

enum FeatureFlag : Codable {
    case allowsTrading
    case allowsFundScreener
    case allowsFundsTransfer
    case other(String)
}

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

extension FeatureFlag : CaseIterable {

    typealias AllCases = [FeatureFlag]

    static let allCases:AllCases = [
        .allowsTrading,
        .allowsFundScreener,
        .allowsFundsTransfer
    ]
}

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

По той же причине, что и раньше - случай other, имеющий ассоциированное значение - мы также должны вручную реализовать RawRepresentable, но это действительно то, где случаются маги c!

Маги c Соус

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

Если совпадение не найдено, используйте новый случай other, поместив неизвестное значение внутрь.

Аналогично, на обратном пути через геттер rawValue сначала проверьте, является ли он типом other, и, если да, верните соответствующее значение. В противном случае верните строку, описывающую известный случай.

Вот реализация обоих:

extension FeatureFlag : RawRepresentable {

    init?(rawValue: String) {

        self = FeatureFlag.allCases.first{ $0.rawValue == rawValue }
               ??
               .other(rawValue)
    }

    var rawValue: String {

        switch self {
            case let .other(value) : return value
            default                : return String(describing:self)
        }
    }
} 

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

init?(rawValue: String) {

    guard let knownCase = FeatureFlag.allCases.first(where: { $0.rawValue == rawValue }) else {

        print("Unrecognized \(FeatureFlag.self): \(rawValue)")
        self = .other(rawValue)
        return
    }

    self = knownCase
}

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

var rawValue: String {

    switch self {
        case .allowsTrading       : return "ALLOWS_TRADING"
        case .allowsFundScreener  : return "ALLOWS_FUND_SCREENER"
        case .allowsFundsTransfer : return "ALLOWS_FUNDS_TRANSFER"
        case let .other(value)    : return value
    }
}

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

let a = FeatureFlag.allowsTrading
let b = FeatureFlag(rawValue: "allowsTrading")!
let c = FeatureFlag.other("allowsTrading")

let x = a == b // x is 'true'
let y = a == c // y is 'true'
let z = b == c // z is 'true'

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

extension FeatureFlag : Hashable {}

Теперь вы можете использовать его в наборах или в качестве ключей в словаре. Используя 'a', 'b' и 'c' сверху - опять же, все равны - вы можете использовать их так, как ...

var items = [FeatureFlag:Int]()

items[a] = 42
print(items[a] ?? -1) // prints 42
print(items[b] ?? -1) // prints 42
print(items[c] ?? -1) // prints 42

С учетом вышеизложенного вы можете Теперь закодируйте или расшифруйте любую строку в этот тип перечисления, но при этом сохраняйте доступ к известным случаям, которые вас интересуют, и все это без необходимости писать какие-либо пользовательские логики декодирования c в типах вашей модели. А когда вы «знаете» о новом типе, просто добавьте новый случай, и вы хорошо справляетесь с go!

Побочным эффектом: Кодирование / декодирование без потерь

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

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

Наслаждайтесь!

1 голос
/ 14 апреля 2020

Люблю это предложенное решение ! Одно небольшое предложение, добавьте немного регистрации в случае, если система обнаружит неизвестные типы.

init?(rawValue: String) {
    if let item = Self.allCases.first(where: { $0.rawValue == rawValue }) {
        self = item
    } else {
        self = Self.other(rawValue)
        if #available(iOS 12.0, *) {
            os_log(.error, "Unknown FeatureFlag: %s", rawValue)
        } else {
            print("Error: Unknown FeatureFlag: \(rawValue)")
        }
    }
}
...