Кластеры пользовательских классов в Swift
Это довольно распространенный шаблон проектирования:
Это позволяет вам вернуть подкласс из вашего 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
) возвращают разные подклассы на основе значений, заданных во время инициализации.