Swift - Singleton без глобального доступа - PullRequest
0 голосов
/ 24 сентября 2018

Я хочу создать Swift Singleton без глобального доступа.Шаблон, который я хочу создать, должен гарантировать, что всегда существует только один экземпляр класса, но этот класс не должен быть доступен через обычный глобальный синтаксис MyClass.shared.Причина этого в том, что я хочу, чтобы класс был полностью и правильно тестируемым (что на самом деле невозможно с глобальными синглетонами).Затем я буду использовать внедрение зависимостей для передачи одного экземпляра из viewcontroller в viewcontroller.Таким образом, проблема «доступа» решается без глобального статического экземпляра.

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

Так что требование:

  • гарантирует, что во время компиляции будет создан только один экземпляр класса
  • нет глобального доступа
  • гарантия того, что создается только один класс, не должна применяться во время модульных тестов, поэтому можно проверить его правильно

Моя первая попытка решить эту проблему былачто-то вроде этого:

enter image description here

class WebService {
    private static var instances = 0

    init() {
        assertSingletonInstance()
    }

    private func assertSingletonInstance() {
        #if DEBUG
        if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false {
            WebService.instances += 1
            assert(WebService.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
        }
        #endif
    }
}

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

Обычно этот шаблон работает довольно хорошо.Моя единственная проблема с этим - я должен дублировать этот код снова и снова для каждого возможного синглтона.Что не приятно.Я бы предпочел многоразовое решение.

Singleton Protocol Extension

Одним из решений было создать расширение протокола:

protocol Singleton {
    static var instances: Int { get set }
    func assertSingletonInstance()
}

extension Singleton {
    // Call this assertion in init() to check for multiple instances of one type.
    func assertSingletonInstance() {
        if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false {
            Self.instances += 1
            assert(Self.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
        }
        #endif
    }
}

И затем использовать его следующим образом:

class WebService: Singleton {)
    static var instances = 0

    init() {
        assertSingletonInstance()
    }
}

Проблема этого подхода заключается в том, что переменная instances не private.Таким образом, кто-то может просто установить эту переменную в 0, прежде чем создавать экземпляр класса, и проверка больше не будет работать.

Базовый класс Singleton

Другой попыткой был базовый класс Singleton.В этом случае можно использовать private static var instances.

class Singleton {
    private static var instances = 0

    required init() {
        assertSingletonInstance()
    }

    private func assertSingletonInstance() {
        #if DEBUG
            if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false {
                Singleton.instances += 1
                assert(Singleton.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
            }
        #endif
    }
} 

Проблема этого подхода в том, что он не работает.Инкремент Singleton.instance добавляет 1 к static instances типа Singleton, а не к классу, производному от базового класса Singleton.

Теперь мне остается либо ничего не делать и полагаться на дисциплину и понимание всех разработчиков или, по крайней мере, использовать расширение протокола с доступом internal или public.

Пример реализации можно найти здесь .

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

Ответы [ 2 ]

0 голосов
/ 25 сентября 2018

В ответ на ответ Кристика:

Это действительно хорошее решение!type(of: self) решает проблему базового класса.И выпуск этой вещи в deinit - это отличная идея, чтобы все это можно было выполнить в модульных тестах.Вы правы - я сохраняю ссылки на все синглтоны "вверх по течению" и добавляю их позже.Отлично.

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

Код игровой площадки:

import Foundation

class Singleton {
    static private var instances = 0

    // Sync the access to instances
    private var serialQueue = DispatchQueue(label: "com.yourcompany.app.singletoncheck")

    init() {
        serialQueue.sync {
            type(of: self).instances += 1
            assert(type(of: self).instances == 1, "Do not create multiple instances of this class living at the same time.")
        }
    }

    deinit {
        type(of: self).instances = 0
    }
}

class Derived: Singleton {

}

var a: Derived? = Derived()
//a = nil // release to prevent the assertion from failing
var b: Derived? = Derived() // assertion fails here, works!

А вот еще более интересное решение, которое можно использовать везде без каких-либо специальных знаний и никаких утверждений.Он использует неисправный инициализатор.

Код игровой площадки:

import Foundation

class Singleton {
    static private var instances = 0

    // Sync the access to instances
    let serialQueue = DispatchQueue(label: "com.yourcompany.app.singletoncheck")

    // This failable initializer assures that at the same time only one instance of this class exists.
    init?() {
        var singleInstance = false
        serialQueue.sync {
            type(of: self).instances += 1
            if type(of: self).instances == 1 {
                singleInstance = true
            }
        }
        if !singleInstance {
            return nil
        }
    }

    deinit {
        serialQueue.sync {
            type(of: self).instances = 0
        }
    }
}

class Derived: Singleton {
    var a = 0
    func increment() {
        serialQueue.sync {
            a += 1
            print(a)
        }
    }
}

var a = Derived()
a?.increment() // call to synchonized version of increment
//a = nil //either a or b is alive
var b = Derived()

print (a) //prints Optional(__lldb_expr_15.Derived)
print (b) //prints nil

Это, на мой взгляд, "настоящий" синглтон, описанный Бандой четырех.Глобальный доступ был только деталью реализации в то время.

Таким образом, по сравнению с обычным шаблоном Singleton он:

  • не имеет глобального доступа
  • - шаблон многократного использования
  • все разделяемые изменяемые состояния могут быть синхронизированы (если позаботиться о них)
  • полностью тестируется в модульных тестах

Таким образом, он обладает всеми преимуществами Singleton, нобез обычных проблем.

0 голосов
/ 24 сентября 2018

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

class Singleton {

    static private var hasInstance = atomic_flag()

    init() {
        // use precondition() instead of assert() if you want the crashes to happen in Release builds too
        assert(!atomic_flag_test_and_set(&type(of: self).hasInstance), "Singleton here, don't instantiate me more than once!!!")
    }

    deinit {
        atomic_flag_clear(&type(of: self).hasInstance)
    }
}

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...