Парадигма MVC

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

Так уж сложилось, что за множество лет разработки приложений под iOS, наплодилось немалое количество паттернов/архитектурных фреймворков, которые как бы упрощают жизнь в повседневной разработке, но я сейчас думаю, что это ошибочное мнение.

Сейчас мой выбор это Apple Model-View-Controller (Apple MVC) и я объясню почему.

Раньше я писал код не заморачиваясь на архитектуру в целом. Когда приложение имело пару представлений, код действительно я писал в “одном” контроллере. Думаю многие из вас сейчас вспоминают дни когда вы делали то же самое 🙂 Потом, начались появляться такие термины как сервисы/менеджеры. Это было уже поинтересней. Да возможно я не так сильно углублялся в ООП и это была моя ошибка, но считаю что череда событий которая меня сейчас привела к выбору only Apple MVC была правильной.

Помниться мне, я когда-то хотел создать клиент, для одной известной социальной сети с прикольным дизайном, и в процессе создания я уже начинал задумываться над тем что, я что-то делаю не правильно. Так как файлов/кода было очень много и множество частей было очень сильно переплетено между собой. Да, оно работало, но мне это очень не нравилось. Статей тогда не очень много было на эту тему, были всякие примеры, но этого было мало. Литература по iOS в основном была просто копией одной книги из другой со своей отсебятиной рассказывая основы и что делают разные методы. Ресурс Рея, который сделал не малый вклад в разработку, кое-как помогал найти более-менее правильное решение.

Но если честно, решение всегда было рядом. Просто я его не замечал. И самое смешное, это решение и есть iOS SDK. В то время я почему-то задавал вопрос, Apple почему так сложно работать с таблицами или коллекциями, зачем нам эти кучи делегатных методов, что за разделение понятий delegate и data source. Об этом я напишу отдельный цикл статей про data source позже. В общем действительно мои контролеры превращались в massive-view-controller. На то время не было еще этого термина, но уже множество ресурсов начали трубить об такой проблеме. Позже я столкнулся с новым для меня паттерном MVVM (Model-View-View-Model) и библиотекой ReactiveCocoa. Для меня это было некое открытие. Мне понравилась эта архитектура, тем более уже на собеседованиях спрашивали про Unit-тесты (которые мне так и не приходилось писать по сей день 🙂 об этом тоже в отдельной статье) и что паттерн MVVM отлично подходит для этого. Но я не остановился, еще к этому всему я начитался за эту архитектуру и добавил слой Dependency Injection. И вуаля, проект превратился в помойку обсерверов и ассамблей, в котором я хотел начать писать юнит-тесты, но так и не срослось, заказчик не выделил на это просто денег. Знакомо? 🙂

И на этом путь MVVM для меня наверное был закрыт навсегда, так как связка DI + ReactiveCocoa имеет охренительно большой подводный камень! Это деббагинг. Считайте что его у вас нет. Забудьте за стек вызовов навсегда. Также все библиотеки типа Reactive (Reactive Cocoa, RxSwift и т/д) не идеальны. KVO на котором они построены, я считаю это самое последнее оружие, которое нужно использовать в повседневной разработке. Если есть возможность не использовать его, не используйте. В целом это крутая штука, и с ней нужно уметь работать, так как и в целом со всем Runtime’ом. Но это тоже отдельная тема, которую я постараюсь написать позже.

Далее были потуги поучить VIPER (View-Interaction-Presenter-Entity-Router) но мне он показался каким то бессмысленным, с самого начала. Насколько мне известно этот паттерн зародился в команде разработчиков проекта Rambler, где со времени его там и похоронили.

А теперь я хочу поделиться мнением почему мы имеем на рынке iOS множество разных архитектур и все дружно говорим что MVC это прошлый век.

Глобальной проблемой является недопонимание архитектуры MVC в целом.
Множество программистов пришло в мир iOS из других платформ, из-за недопонимания Apple MVC решили применить свой паттерн из чужого мира. Доказательством этого есть MVVM, который пришел из мира C# .NET, библиотеки Reactive, RX из мира JavaScript и так далее. Да это все работает хорошо там, но не здесь. Здесь это костыль который вы пытаетесь использовать потому что вам там было удобней.

Теперь давайте еще глубже углубимся в это. Вот есть такая сейчас популярная библиотека как Redux. Зачем она нужна для iOS? Это JavaScript библиотека, все эти принципы нужны WEB, ну никак здесь. Хорошо, допустим вы хорошо разбираетесь с ним в вебе, (и то не факт что redux это хороший выбор для веба в целом) но допустим, далее вам стало скучно и вы решили заняться разработкой под айфоны. Вы начинаете изучать Swift, думать что он похож к javascript и думаете, а почему так сложно?, что за Apple MVC, давайте Redux сюда, это же круто. Он же так “прекрасно” ложится в iOS SDK. Потом вы пишете пару говенных статеек на Media и другие начинают подхватывать эту волну хайпа. И что в итоге получаем? Разработчики которые не поняли Apple MVC, начинают учить Redux стиль, и что в итоге получаем? Правильно, код, который будет рано или поздно переписан в третью архитектуру. 🙂

Таким образом мы получаем на выходе не качественный код в проектах. На своей практике, очень мало кода я видел который, был выдержан в одном стиле. Но тот что я видел, это был Apple MVC 🙂 Часто видно, как разработчики пытались из-за тупиковой ситуации применять другую архитектуру, так как код уже не возможно было нормально масштабировать, и эта смесь приводит к тому что, новой команде которая начинает продолжать поддерживать проект приходиться очень туго. Каждый разработчик должен задуматься над тем, что он не будет всю жизнь поддерживать один и тот же проект, потому и были изначально придуманы принципы, архитектуры, парадигмы и т/д.

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

Вернемся к Apple MVC.

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

Я нарисовал диаграмму, как выглядит MVC в моем понимании.

Во главе – три основных слоя. Это View, Controller, Model. Которые не нарушают главные принципы работы паттерна MVC. То есть, представление ничего не знает об модели, модель ничего не знает об представлении. Контроллер служит мостиком между моделью и представлением. Далее вы можете увидеть множество дополнительных под-слоев. Давайте рассмотрим сервисный слой по работе например с Http запросами (также это может быть слой по работе с аудио, блютуз и т/д.)

Сервисный слой включает класс сервис, который непосредственно работает с источником взаимодействия (бэкендом, аудио модулем, блютуз модулем). Обратите внимание, что сервисный слой ничего не должен знать о модели данных, а тем более с представлением :). Это важно! У него есть набор функций с входящими параметрами, если они нужны, и результатом функции должен быть ответ допустим с сервера в формате Data или ошибка.


    static func doRequestTo(url: URL,
                            jsonData: Data? = nil,
                            success: @escaping (Data) -> (),
                            failure: @escaping (Error) -> ()) -> Void {
        
        
        guard isInternetAvailable() else {
            failure(RequestError.internetNotAvailable)
            return
        }
        
        var session = URLSession.shared
        let sessionConfig = URLSessionConfiguration.default
        sessionConfig.timeoutIntervalForRequest = 30.0
        sessionConfig.timeoutIntervalForResource = 60.0
        session = URLSession(configuration: sessionConfig)
        
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = "POST"
        
        if let jsonData = jsonData {
            urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
            urlRequest.httpBody = jsonData
        }
        
        session.dataTask(with: urlRequest,
                         completionHandler:
            { (data, response, error) -> Void in
                    if let error = error {
                        failure(error)
                    } else {
                        success(data)
                    }
        }).resume()
    }

Далее это всё обрабатывает наш менеджер, который парсит сырые данные в модель или есть другой класс который это делает, это не столь важно на данный момент. Менеджеров может быть много, каждый из них отвечает за конкретные сущности. К примеру, пользователь – UserManager, который будет готовить данные для запроса, именно что касается пользователя. Этот класс может выступать как синглтон к примеру, который будет хранить временный токен, без которого мы не можем делать запросы. Токен опять-таки может быть сохранен в связку ключей, где для этого мы напишем еще один сервис, который работает со связкой ключей непосредственно. Смысл в том, что сервисы вы можете переиспользовать в других приложениях, они не зависят ни от кого. Менеджеры это мосты между сервисами и контроллерами. В таком примере контроллеру не нужно ничего знать как формируется запрос и какие дополнительные данные для запроса нужно. Контроллер должен только вызвать функцию из менеджера и передать принимающие аргументы, или вообще их не передавать. Допустим нам нужна модель пользователя, для того чтоб вывести имя на экран.


UserManager.shared.user(success: { user in {
            // do something
        }) { error in
            // do something
        }

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

Такой подход помогает, быстро проектировать приложение.

Вот как может выглядеть функция для получения пользователя в менеджере.


func user(success: @escaping (User) -> Void,
          failure: @escaping (Error) -> Void) {
        AuthService.login(withToken: user.token, success: { user in
            success(user)
        }) { error in
            failure(error)
        }
    }

или


func user(success: @escaping (User) -> Void,
          failure: @escaping (Error) -> Void) {
        success(User(name: "Jon"))
    }

Смысл один и тот же, вы можете получать юзера с любого места в вашем приложении. Завтра к примеру нужны будут дополнительные параметры в headers, без проблем :) в сервисе добавили строчку кода, и ничего больше не трогаем.

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

Вы могли обратить внимание на слои по работе с базой данных. Адаптером является такой же мост, как и в случае с менеджером. Представьте себе что вы использовали Realm в приложении, и захотели поменять базу данных на Core Data, или наоборот. Если не придерживаться такого подхода, то будет одна только боль, чтоб это сделать, а скорее всего вообще не возможным, так как модели Realm’a гуляют по всему приложению.

В следующих статьях я покажу как это выглядит всё на практике. Мы будем разбирать простые примеры, где такой принцип MVC будет во всей своей красе.

Я более склоняюсь называть паттерн MVC как парадигма, так как это не разделение на модель-представление-контроллер, а совокупность идей и понятий, определяющих стиль написания кода.

Это сложно понять новичкам, и также разработчикам которые возможно уже не первый год пишут приложения и успешно работают в компаниях. Да, да есть и такие :) Но я попробую объяснить и показать эти принципы. Когда я для себя открыл это, мне стало просто и быстро реализовывать сложные решения. Также это начало приносить множество плодов, когда заказчики просили поменять часть бизнес логики или добавить новый функционал. Багов стало в разы меньше так как и самого кода, код стал масштабируемым и наконец-то независимый друг от друга. На данный момент я отказался от множества библиотек таких как Alamofire к примеру, так как у меня уже есть свой сервис который делает любой http запрос который мне нужно. И этот сервис каждый раз только улучшается, и переходит с проекта в проект. И я уверенный в нём на 100%, что не могу сказать за библиотеку которую я не писал.

Цель данной статьи, рассказать что Apple MVC это не спагетти, и не массивный контроллер которым его так называют, а вполне нормальный и правильный подход к разработке приложений под iOS.

Следите за обновлениями. 🙂

Поделиться

Оставить комментарий