Когда бы вы предпочли перечисление со связанными значениями статической фабрике?

В Swift вы можете определить перечисление и присвоить ему свойство через связанное значение, например:

protocol SizeEnum {
    var length : Double? { get } // Length should be >= 0 - has to be an Optional for errors
}

enum SizesEnum : SizeEnum {
    case Short(length : Double) // 0 <= length <= maxShort
    case Long(length : Double) // length > maxShort
    private static let maxShort =  1.0
    var length : Double? {
    get {
        switch self {
        case let .Short(length):
            if length >= 0 && length <= SizesEnum.maxShort { // Need to error check every access
                return length
            }
        case let .Long(length):
            if length > SizesEnum.maxShort { // Need to error check every access
                return length
            }
        }
        return nil // There was an error
    }
    }
}

SizesEnum.Short(length: 0.5).length // [Some 0.5]
SizesEnum.Short(length: 2).length // nil
SizesEnum.Long(length: 2).length // [Some 2.0]
SizesEnum.Long(length: -1).length // nil

Однако это не идеально, потому что:

  1. Проверка ошибки для параметра длины может быть выполнена только при доступе, вы не можете перехватить инициализацию
  2. Параметр длины на удивление длинный.

Альтернативой, которая мне кажется лучше, является использование статической фабрики, например:

protocol SizeStruct {
    var length : Double { get } // Length should be >= 0 - is *not* an Optional
}

struct SizesStruct : SizeStruct {
    static func Short(length : Double) -> SizeStruct? {
        if length >= 0 && length <= maxShort { // Check at creation only
            return SizesStruct(length)
        }
        return nil
    }
    static func Long(length : Double) -> SizeStruct? {
        if length > maxShort { // Check at creation only
            return SizesStruct(length)
        }
        return nil
    }
    let length : Double
    private static let maxShort = 1.0
    private init(_ length : Double) {
        self.length = length
    }
}

SizesStruct.Short(0.5)?.length // [Some 0.5]
SizesStruct.Short(2)?.length // nil
SizesStruct.Long(2)?.length // [Some 2.0]
SizesStruct.Long(-1)?.length // nil

Учитывая, что статическое фабричное решение более аккуратное, когда мне на самом деле использовать перечисление со значениями? Я что-то упускаю? Есть ли убойный вариант использования?

В ответ на драваг

Для Optional других языков, например. Java и Scala, вы используете фабрики, версия Java описана здесь: http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html фабрикой является of метод.

В Swift вы бы сделали что-то вроде:

class Opt { // Note only Some stores the value, not None
    //class let None = Opt() - class variables not supported in beta 4!
    class Some<T> : Opt {
        let value : T
        init(_ value : T) {
            self.value = value
        }
    }
    private init() {} // Stop any other ways of making an Opt
}

Opt.Some(1).value // 1

Вероятно, это оптимальный пример для enum, так как проверка ошибок не требуется, но даже при этом заводская версия конкурентоспособна. Пример Optional настолько прост, что вам даже не нужна фабрика, вы просто создаете Some напрямую. Обратите внимание, что None не использует хранилище.

Пример со штрих-кодом показывает, насколько лучше фабричная техника; на практике не все коллекции из 4 Int являются действительными UPCA, и не все String являются действительным QR-кодом, поэтому вам нужна проверка ошибок, которая болезненна с enums. Вот заводская версия:

class Barcode { // Note seperate storage for each case
    class UPCABarcode : Barcode {
        let type : Int, l : Int, r : Int, check : Int
        private init(type : Int, l : Int, r : Int, check : Int) {
            (self.type, self.l, self.r, self.check) = (type, l, r, check)
        }
    }
    class func UPCA(#type : Int, l : Int, r : Int, check : Int) -> UPCABarcode? {
        if ok(type: type, l: l, r: r, check: check) {
            return UPCABarcode(type: type, l: l, r: r, check: check)
        }
        return nil
    }
    class func QRCode(#s : String) -> Barcode? { // Have not expanded this case; use same pattern as UPCA
        return Barcode()
    }
    private init() {} // Prevent any other types of Barcode
    class func ok(#type : Int, l : Int, r : Int, check : Int) -> Bool {
        return true // In practice has to check supported type, range of L and R, and if check digit is correct
    }
}

Barcode.UPCA(type: 0, l: 1, r: 2, check: 3)

Если вы используете enum версию Barcode, то каждый раз, когда вы используете Barcode, вы должны проверять его действительность, потому что нет ничего, что могло бы остановить неверные штрих-коды. В то время как заводская версия выполняет проверку при создании. Обратите внимание, что у Barcode нет хранилища, а у UPCA есть собственное хранилище. Я не кодировал QRCode, потому что он использует тот же шаблон проектирования, что и UPCA.

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


person Howard Lovatt    schedule 30.07.2014    source источник


Ответы (1)


Я считаю, что самый большой вариант использования убийцы — это Optional. Он встроен в язык, но необязательный — это просто перечисление:

enum Optional<T> {
    case None
    case Some(T)
}

В этом случае переменная-член, такая как value, не имеет смысла, потому что в случае None значения буквально нет.

Кроме того, прямо из Быстрый обзор:

enum Barcode {
    case UPCA(Int, Int, Int, Int)
    case QRCode(String)
}

Со структурой было бы много ненужных переменных-членов и запутанный интерфейс для моделирования такого рода данных.

Я не думаю, что имеет смысл использовать связанное значение для перечисления, если один и тот же тип используется для каждого случая. В этом случае переменная-член чище. Связанные значения предназначены для более точного моделирования определенных типов данных. Наиболее полезными являются случаи, когда разные экземпляры типа могут иметь разные данные, связанные с ними. Потенциально это можно было бы сделать с помощью подклассов, но тогда для доступа к более конкретным переменным потребовалось бы понижающее приведение, а объявления были бы гораздо более подробными. Перечисления — это краткий способ представления этого типа данных.

Другим примером могут быть веб-запросы:

struct NetRequest {
    enum Method {
        case GET
        case POST(String)
    }

    var URL: String
    var method: Method
}

var getRequest = NetRequest(URL: "http://drewag.me", method: .GET)
var postRequest = NetRequest(URL: "http://drewag.me", method: .POST("{\"username\": \"drewag\"}"))

Когда я думаю о «enum», я вообще не думаю о «фабрике». Обычно фабрики предназначены для более крупных и сложных структур классов. Перечисления должны быть очень маленькими фрагментами данных с небольшой логикой.

person drewag    schedule 30.07.2014
comment
Спасибо за комментарии, отредактировал оригинал, чтобы дать ответ, потому что я хотел показать код, который я не могу сделать в комментариях. - person Howard Lovatt; 30.07.2014
comment
@HowardLovatt, ваш пример реализации Optional значительно сложнее, чем перечисление, и я думаю, что вы слишком полагаетесь на шаблон Java. Почему бы просто не использовать метод validate() -> Bool для перечисления? Я думаю, перечисления кажутся вам болезненными, потому что вы хотите использовать существующие шаблоны проектирования. Честно говоря, каждый приведенный вами пример фабричного шаблона кажется мне невероятно болезненным с точки зрения понимания Типа с первого взгляда. Возврат nil для недопустимых объектов не так очевиден, как наличие метода validate. Хотя я уверен, что это личное предпочтение - person drewag; 30.07.2014