Внедрение зависимостей с помощью swift с графом зависимостей двух контроллеров UIViewControl без общего родителя - PullRequest
0 голосов
/ 30 ноября 2018

Как мы можем применить внедрение зависимостей без использования Framework, когда у нас есть два UIViewController, которые очень глубоки в иерархии, и им обоим нужна та же самая зависимость, которая содержит состояние, и те два UIViewControllers, у которых нет общего родителя.

Пример:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

давайте посидим, что VC4 и VC8им обоим нужно UserService, которое содержит текущего пользователя.

Обратите внимание, что мы хотим избежать Singleton.

Есть ли элегантный способ справиться с подобными ситуациями DI?

После некоторых исследований я обнаружил, что некоторые упоминают Abstract Factory, Context interfaces, Builder, strategy pattern

Но я не смог найти пример того, как применить это на iOS

Ответы [ 7 ]

0 голосов
/ 08 декабря 2018

Я попытался решить эту проблему и загрузил пример архитектуры здесь: https://github.com/ivanovi/DI-demo

Чтобы сделать это более понятным, я упростил реализацию, используя три VC, но решение будет работать с любой глубиной,Цепочка контроллеров представления выглядит следующим образом:

Master -> Detail -> MoreDetail (куда вводится зависимость)

В предлагаемой архитектуре четыре строительных блока:

  • Хранилище координаторов: Содержит всех координаторов и общие состояния.Внедряет необходимые зависимости.

  • Координатор ViewController: выполняет переход к следующему ViewController.Координатор держит фабрику, которая производит необходимый следующий экземпляр VC.

  • Завод ViewController: отвечает за инициализацию и настройку определенного ViewController.Обычно он принадлежит координатору и вводится репозиторием Coordinator в координатор.

  • ViewController: ViewController, который будет представлен на экране.

Примечание: в примере я возвращаю только что созданный экземпляр VC для создания примера - т.е. в реальной реализации возвращать VC не нужно.

Надеюсь, это поможет.

0 голосов
/ 09 декабря 2018
let viewController = CustomViewController()
viewController.data = NSObject() //some data object
navigationController.show(viewController, sender: self)


import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var appCoordinator:AppCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = UINavigationController()
        appCoordinator = AppCoordinator(with: window?.rootViewController as! UINavigationController)
        appCoordinator?.start()
        window?.makeKeyAndVisible()
        return true
    }
}
0 голосов
/ 07 декабря 2018

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

  1. Создайте все ваши контроллеры представления с помощью фабричных методов в ViewControllerFactory.
  2. ViewControllerFactory имеетсвой собственный объект UserService.
  3. Передайте объект UserService ViewControllerFactory тем контроллерам представления, которые в нем нуждаются.

Скромный пример здесь:

struct ViewControllerFactory {

private let userService: UserServiceProtocol

init(userService: UserServiceProtocol) {
    self.userService = userService
}

// This VC needs the user service
func makeVC4() -> VC4 {
    let vc4 = VC4(userService: userService)
    return vc4
}

// This VC does not
func makeVC5() -> VC5 {
    let vc5 = VC5()
}

// This VC also needs the user service
func makeVC8() -> VC8 {
    let vc8 = VC8(userService: userService)
    return vc8
}
}  

Объект ViewControllerFactoryможет быть создан и сохранен в AppDelegate.

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

  1. Создайте протокол UserServiceProtocol, которому соответствует UserService.Это позволяет легко создавать фиктивные объекты для тестирования.
  2. Посмотрите на шаблон Координатора для обработки логики навигации.
0 голосов
/ 06 декабря 2018

Прежде всего, я полагаю, что в вашем вопросе неверное предположение.

Вы определяете иерархию VC'c следующим образом:

Пример:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

Однако на iOS (если вы не используете некоторые очень странные хаки)в какой-то момент всегда будет общий родительский элемент, например, контроллер навигации, контроллер панели вкладок, контроллер главной детали или контроллер просмотра страниц.

Поэтому я предполагаю, что правильная схема может выглядеть, например, какэто:

Контроллер панели вкладок 1 -> Контроллер навигации 1 -> VC1 -> VC2 -> VC3 -> VC4

Контроллер панели вкладок 1 -> Контроллер навигации 2 -> VC5-> VC6 -> VC7 -> VC8

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

Теперь, если вы спрашиваете мнение, в чем заключаетсялучший способ справиться с DI на iOS, я бы сказал, что лучшего способа не существует.Однако мне лично нравится придерживаться правила, согласно которому объекты не должны отвечать за свое создание / инициализацию.Так что такие вещи, как

private lazy var service: SomeService = SomeService()

, не подлежат обсуждению.Я бы предпочел инициализацию, которая требует SomeService экземпляр или, по крайней мере (легко для ViewControllers):

var service: SomeService!

Таким образом, вы передаете ответственность за выбор нужных моделей / сервисов и т. Д. До создателяНапример, вы можете реализовать свою логику с простым, но важным предположением, что у вас есть все, что вам нужно (или вы заставляете свой класс проваливаться рано (например, с помощью принудительного развертывания), что на самом деле хорошо во время разработки).

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

0 голосов
/ 06 декабря 2018

Это ваши требования, насколько я понимаю:

  1. VC4 и VC8 должны иметь возможность совместно использовать состояние через класс UserService.
  2. UserService не должно бытьsingleton.
  3. UserService должен быть передан в VC4 и VC8 с использованием внедрения зависимостей.
  4. Не следует использовать инфраструктуру внедрения зависимостей.

В этих ограничениях,Я бы предложил следующий подход:

Определите UserServiceProtocol, который имеет методы и / или свойства для доступа и обновления состояния.Например:

protocol UserServiceProtocol {
    func login(user: String, password: String) -> Bool
    func logout()
    var loggedInUser: User? //where User is some model you define
}

Определить класс UserService, который реализует протокол и где-то сохраняет его состояние.

Если состояние должно длиться до тех пор, пока приложение работает, вы можете сохранить состояние в конкретном экземпляре , но этот экземпляр должен бытьделится между VC4 и VC8.

В этом случае я бы порекомендовал создать и сохранить экземпляр в AppDelegate и передать его через цепочку VC.

Если состояние должно сохраняться между запусками приложения или если вы не хотите передавать экземпляр через цепочку VC, вы можете сохранить состояние в пользовательских значениях по умолчанию, Core Data, Realm илилюбое количество мест вне самого класса.

В этом случае вы можете создать UserService в VC3 и VC7 и передать его в VC4 и VC8.VC4 и VC8 будут иметь var userService: UserServiceProtocol?.UserService потребуется восстановить его состояние из внешнего источника.Таким образом, даже если VC4 и VC8 имеют разные экземпляры объекта, состояние будет одинаковым.

0 голосов
/ 05 декабря 2018

Я считаю, что шаблон проектирования координатор / маршрутизатор наиболее подходит для внедрения зависимостей и обработки навигации по приложению.Посмотрите на этот пост, он мне очень помог https://medium.com/@dkw5877/flow-coordinators-333ed64f3dd

0 голосов
/ 03 декабря 2018

Хорошо, я попробую.

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

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

Как правильно сказал Джош, контроллеры представления не должны (много) знать друг о друге [1], но тогда как, например, координатор или какая-либо зависимость передается / доступна?Есть несколько шаблонов, которые предлагают, как, но у большинства есть проблема, которая в основном идет вразрез с вашим требованием: они более или менее делают координатора синглтоном (либо сами по себе, либо как свойство другого синглтона, такого как AppDelegate).Координатор также часто де-факторирует и синглтон (но не всегда, и это не обязательно).

Я склонен полагаться на простые инициализированные свойства или (чаще всего) ленивые свойства и протоколно-ориентированное программирование.Давайте построим пример: UserService должен быть протоколом, определяющим все функциональные возможности, необходимые для вашей службы, MyUserService его реализующую структуру.Давайте предположим, что UserService является конструктивной конструкцией, которая в основном функционирует как система получения / установки для некоторых пользовательских данных: токены доступа (например, сохраненные в цепочке для ключей), некоторые предпочтения (URL-адрес изображения аватара) и тому подобное.При инициализации MyUserService также подготавливает данные (например, загружает с пульта).Это должно использоваться на нескольких независимых экранах / контроллерах представления и не является одиночным.

Теперь у каждого контроллера представления, который заинтересован в доступе к этим данным, есть простое свойство:

lazy var userService: UserService = MyUserService()

Я держу это публично, потому что это позволяет мне легко смоделировать / заглушить его в модульных тестах (если мне нужно это сделать, я могу создать пустышку TestUserService, которая высмеивает / заглушает поведение).Инстанцирование также может быть закрытием, которое я могу легко отключить во время теста, если init требует параметры.Очевидно, что свойства не обязательно должны быть lazy в зависимости от того, что на самом деле делают объекты.Если создание экземпляра объекта раньше времени не причиняет вреда (помните о модульных тестах, а также об исходящих соединениях), просто пропустите lazy.

Трюк явно предназначен для дизайнаUserService и / или MyUserService таким образом, чтобы не создавать проблем при создании нескольких его экземпляров .Однако я обнаружил, что в действительности это не проблема в 90% случаев, поскольку фактические данные, на которые должен полагаться экземпляр, сохраняются где-то еще, в единой точке истины, например, в цепочке для ключей, стеке основных данных,пользовательские настройки по умолчанию, или удаленный бэкэнд.

Я знаю, что это своего рода ответ на отказ, так как я просто говорю, описывая подход, который (по крайней мере, часть) многих общих шаблонов там.,Однако я обнаружил, что это самая общая и простая форма для подхода к внедрению зависимостей в Swift.Шаблон координатора можно использовать ортогонально к нему, но я обнаружил, что он менее «похож на Apple» при повседневном использовании.Это решает проблему, но в основном ту, которую вы получаете, вы неправильно используете раскадровки, как они предназначены (особенно: просто используйте их как «репозитории VC», создайте их экземпляры оттуда и перенесите себя в код).

[1] За исключением некоторых базовых и / или второстепенных вещей, которые вы можете передать в обработчике завершения или prepareForSegue.Это спорно и зависит от того, насколько строги вы будете следовать координатор или другой шаблон.Лично я иногда использую ярлык, если он не раздувается и становится грязным.Некоторые всплывающие окна проще сделать таким образом.


В качестве заключительного замечания фраза «Заметьте, что мы хотим избежать Синглтона», а также ваш комментарий относительно этого вопроса дают мне впечатление, что вы просто следовали этому совету, не задумываясь об обосновании.Я знаю, что «Синглтон» часто считается антипаттерном, но так же часто это суждение неверно информировано.Синглтон может быть допустимой архитектурной концепцией (что видно по тому, что он широко используется в фреймворках и библиотеках).Плохая вещь в том, что это слишком часто побуждает разработчиков использовать ярлыки в дизайне и использовать его как «хранилище объектов», чтобы им не приходилось думать о том, когда и где создавать объекты.Это приводит к беспорядку и плохой репутации шаблона.

A UserService, в зависимости от того, что на самом деле делает в вашем приложении, может быть хорошим кандидатом на синглтон.Мое личное эмпирическое правило таково: «Если оно управляет состоянием чего-то особенного и уникального, например, определенного пользователя, который может быть только в одном состоянии в данный момент времени», я мог бы пойти на одиночный.

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

...