Кластеры пользовательских классов в Swift

Это довольно распространенный шаблон проектирования:

/questions/16694560/tsel-c-yavlyaetsya-li-init-plohim-mestom-dlya-realizatsii-fabriki/16694568#16694568

Это позволяет вам вернуть подкласс из вашего init звонки.

Я пытаюсь найти лучший метод достижения того же самого, используя Swift.

Я знаю, что вполне вероятно, что есть лучший способ добиться того же с помощью Swift. Тем не менее, мой класс будет инициализирован существующей библиотекой Obj-C, которую я не могу контролировать. Так что это должно работать таким образом и быть вызываемым из Obj-C.

Любые указатели будут очень цениться.

4 ответа

Решение

Я не верю, что этот шаблон может напрямую поддерживаться в Swift, потому что инициализаторы не возвращают значение, как в Objective C - поэтому у вас нет возможности вернуть экземпляр альтернативного объекта.

Вы можете использовать метод типа как фабрику объектов - довольно надуманный пример -

class Vehicle
{
    var wheels: Int? {
      get {
        return nil
      }
    }

    class func vehicleFactory(wheels:Int) -> Vehicle
    {
        var retVal:Vehicle

        if (wheels == 4) {
            retVal=Car()
        }
        else if (wheels == 18) {
            retVal=Truck()
        }
        else {
            retVal=Vehicle()
        }

        return retVal
    }

}

class Car:Vehicle
{
    override var wheels: Int {
      get {
       return 4
      }
    }
}

class Truck:Vehicle
{
    override var wheels: Int {
      get {
          return 18
       }
     }
}

main.swift

let c=Vehicle.vehicleFactory(4)     // c is a Car

println(c.wheels)                   // outputs 4

let t=Vehicle.vehicleFactory(18)    // t is a truck

println(t.wheels)                   // outputs 18

"Быстрый" способ создания кластеров классов на самом деле заключается в предоставлении протокола вместо базового класса.

По-видимому, компилятор запрещает статические функции для протоколов или расширений протоколов.

До тех пор, пока, например, https://github.com/apple/swift-evolution/pull/247 (заводские инициализаторы) не будет принят и реализован, единственный способ, которым я мог бы найти это, - это следующее:

import Foundation

protocol Building {
    func numberOfFloors() -> Int
}

func createBuilding(numberOfFloors numFloors: Int) -> Building? {
    switch numFloors {
    case 1...4:
        return SmallBuilding(numberOfFloors: numFloors)
    case 5...20:
        return BigBuilding(numberOfFloors: numFloors)
    case 21...200:
        return SkyScraper(numberOfFloors: numFloors)
    default:
        return nil
    }
}

private class BaseBuilding: Building {
    let numFloors: Int

    init(numberOfFloors:Int) {
        self.numFloors = numberOfFloors
    }

    func numberOfFloors() -> Int {
        return self.numFloors
    }
}

private class SmallBuilding: BaseBuilding {
}

private class BigBuilding: BaseBuilding {
}

private class SkyScraper: BaseBuilding {
}

,

// this sadly does not work as static functions are not allowed on protocols.
//let skyscraper = Building.create(numberOfFloors: 200)
//let bigBuilding = Building.create(numberOfFloors: 15)
//let smallBuilding = Building.create(numberOfFloors: 2)

// Workaround:
let skyscraper = createBuilding(numberOfFloors: 200)
let bigBuilding = createBuilding(numberOfFloors: 15)
let smallBuilding = createBuilding(numberOfFloors: 2)

Поскольку init() не возвращает значения как -init в Objective C, использование фабричного метода кажется самым простым вариантом.

Один трюк - пометить ваши инициализаторы как private, как это:

class Person : CustomStringConvertible {
    static func person(age: UInt) -> Person {
        if age < 18 {
            return ChildPerson(age)
        }
        else {
            return AdultPerson(age)
        }
    }

    let age: UInt
    var description: String { return "" }

    private init(_ age: UInt) {
        self.age = age
    }
}

extension Person {
    class ChildPerson : Person {
        let toyCount: UInt

        private override init(_ age: UInt) {
            self.toyCount = 5

            super.init(age)
        }

        override var description: String {
            return "\(self.dynamicType): I'm \(age). I have \(toyCount) toys!"
        }
    }

    class AdultPerson : Person {
        let beerCount: UInt

        private override init(_ age: UInt) {
            self.beerCount = 99

            super.init(age)
        }

        override var description: String {
            return "\(self.dynamicType): I'm \(age). I have \(beerCount) beers!"
        }
    }
}

Это приводит к следующему поведению:

Person.person(10) // "ChildPerson: I'm 10. I have 5 toys!"
Person.person(35) // "AdultPerson: I'm 35. I have 99 beers!"
Person(35) // 'Person' cannot be constructed because it has no accessible initializers
Person.ChildPerson(35) // 'Person.ChildPerson' cannot be constructed because it has no accessible initializers

Это не так хорошо, как Objective C, так как private означает, что все подклассы должны быть реализованы в одном исходном файле, и есть небольшая разница в синтаксисе Person.person(x) (или же Person.create(x) или что угодно) вместо просто Person(x), но практически говоря, это работает так же.

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

Есть способ добиться этого. Хорошая это практика или плохая - это отдельный разговор.

Я лично использовал его, чтобы разрешить расширение компонента в плагинах, не подвергая остальной код знанию расширений. Это соответствует целям шаблонов Factory и AbstractFactory в отделении кода от деталей создания экземпляров и конкретных классов реализации.

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

Я не помню, был ли такой подход доступным в 2014 году - но он есть сейчас.

import Foundation

struct InterfaceType {
    let impl: Interface.Type
}

class Interface {

    let someAttribute: String

    convenience init(_ attribute: String, type: InterfaceType = .concrete) {
        self.init(impl: type.impl, attribute: attribute)
    }

    // need to disambiguate here so you aren't calling the above in a loop
    init(attribute: String) {
        someAttribute = attribute
    }

    func someMethod() {}

}

protocol _Factory {}

extension Interface: _Factory {}

fileprivate extension _Factory {

    // Protocol extension initializer - has the ability to assign to self, unlike class initializers.
    init(impl: Interface.Type, attribute: String) {
        self = impl.init(attribute: attribute) as! Self;
    }

}

Затем в конкретном файле реализации...

import Foundation

class Concrete: Interface {

    override func someMethod() {
        // concrete version of some method
    }

}

extension InterfaceType {
    static let concrete = InterfaceType(impl: Concrete.self)
}

В этом примере Concrete является стандартной реализацией, поставляемой с завода.

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

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

import Foundation

class Interface {
        
    required init() {}
    
    convenience init(_ discriminator: Int) {
        let impl: Interface.Type
        switch discriminator {
            case 3:
                impl = Concrete3.self
            case 2:
                impl = Concrete2.self
            default:
                impl = Concrete1.self
        }
        self.init(impl: impl)
    }
    
    func someMethod() {
        print(NSStringFromClass(Self.self))
    }
    
}

protocol _Factory {}

extension Interface: _Factory {}

fileprivate extension _Factory {
    
    // Protocol extension initializer - has the ability to assign to self, unlike class initializers.
    init(impl: Interface.Type) {
        self = impl.init() as! Self;
    }
    
}

class Concrete1: Interface {}

class Concrete2: Interface {}

class Concrete3: Interface {
    override func someMethod() {
        print("I do what I want")
    }
}

Interface(2).someMethod()
Interface(1).someMethod()
Interface(3).someMethod()
Interface(0).someMethod()

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

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

class MyClass
{
    var name: String?

    convenience init(type: Int)
    {
        self.init()

        var subclass: AnyClass?
        if type == 1
        {
            subclass = MySubclass1.self
        }
        else if type == 2
        {
            subclass = MySubclass2.self
        }

        object_setClass(self, subclass)
        self.customInit()
    }

    func customInit()
    {
        // to be overridden
    }
}

class MySubclass1 : MyClass
{
    override func customInit()
    {
        self.name = "instance of MySubclass1"
    }
}

class MySubclass2 : MyClass
{
    override func customInit()
    {
        self.name = "instance of MySubclass2"
    }
}

let myObject1 = MyClass(type: 1)
let myObject2 = MyClass(type: 2)
println(myObject1.name)
println(myObject2.name)
protocol SomeProtocol {
   init(someData: Int)
   func doSomething()
}

class SomeClass: SomeProtocol {

   var instance: SomeProtocol

   init(someData: Int) {
      if someData == 0 {
         instance = SomeOtherClass()
      } else {
         instance = SomethingElseClass()
      }
   }

   func doSomething() {
      instance.doSomething()
   }
}

class SomeOtherClass: SomeProtocol {
   func doSomething() {
      print("something")
   }
}

class SomethingElseClass: SomeProtocol {
   func doSomething() {
     print("something else")
   }
}

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

Например, если вы писали класс массива, который переключался между LinkedList или необработанным массивом, тогда SomeOtherClass и SomethingElseClass могли называться LinkedListImplementation или PlainArrayImplementation, и вы могли решить, какой из них создать или переключиться на более эффективный.

Мы можем воспользоваться причудой компилятора - selfразрешено назначать в расширениях протокола - https://forums.swift.org/t/assigning-to-self-in-protocol-extensions/4942.

Таким образом, мы можем иметь что-то вроде этого:

/// The sole purpose of this protocol is to allow reassigning `self`
fileprivate protocol ClusterClassProtocol { }

extension ClusterClassProtocol {
    init(reassigningSelfTo other: Self) {
        self = other
    }
}

/// This is the base class, the one that gets circulated in the public space
class ClusterClass: ClusterClassProtocol {
    
    convenience init(_ intVal: Int) {
        self.init(reassigningSelfTo: IntChild(intVal))
    }
    
    convenience init(_ stringVal: String) {
        self.init(reassigningSelfTo: StringChild(stringVal))
    }
}

/// Some private subclass part of the same cluster
fileprivate class IntChild: ClusterClass {
    init(_ intVal: Int) { }
}

/// Another private subclass, part of the same cluster
fileprivate class StringChild: ClusterClass {
    init(_ stringVal: String) { }
}

А теперь давайте попробуем:

print(ClusterClass(10))    // IntChild
print(ClusterClass("abc")) // StringChild

Это работает так же, как в Objective-C, где некоторые классы (например, NSString, NSArray, NSDictionary) возвращают разные подклассы на основе значений, заданных во время инициализации.

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