Clean architecture в контексте кроссплатформенной разработки

в 15:06, , рубрики: android, clean architecture, iOS, Разработка под android, разработка под iOS

Всем привет. В последнее время довольно много статей написано на тему clean architecture. То есть чистой архитектуры, которая позволяет писать приложения, удобные в сопровождении и тестировании. Про саму чистую архитектуру вы можете прочитать в таких замечательных статьях как: Заблуждения Clean Architecture или Чистая архитектура, поэтому не вижу смысла повторять то, что уже написано.

Для начала позвольте представиться, меня зовут Какушев Расул. Так уж получилось что я одновременно занимаюсь нативной разработкой на ios и android, а так же разработкой backend-кода мобильных приложений, в компании Navibit. Это пока еще малоизвестная компания, которая только готовится выйти на рынок продажи строительных материалов. У нас очень маленькая команда и поэтому разработка мобильных приложений целиком и полностью ложится на мои (еще не слишком профессиональные) плечи.

В моей работе часто приходится делать одно приложение на ios и android, и как вы понимаете, в силу различий платформ, часто приходится писать один и тот же функционал несколько раз. Это занимает довольно много времени, и поэтому некоторое время назад, когда я познакомился с clean architecture, мне пришла в голову такая мысль: языки kotlin и swift довольно похожи, однако платформы различаются, но в clean architecture есть domain слой, который не привязан к платформе, а содержит чистую бизнес-логику. Что будет если просто взять весь domain слой из android и перенести его в ios, с минимальными изменениями?

Что же, задумано — сделано. Я начал перенос. И действительно идея оказалась в большинстве своем верной. Сами посудите. К примеру вот один интерактор на kotlin и swift:

Kotlin (Android)

class AuthInteractor @Inject
internal constructor(private val authRepository: AuthRepository,
                     private val profileRepository: ProfileRepository) {

    fun auth(login: String, password: String, cityId: Int): Single<Auth> = authRepository.auth(login.trim { it <= ' ' }, password.trim { it <= ' ' }, cityId, cloudToken)

    fun restore(login: String, password: String, cityId: Int, confirmHash: String): Single<AuthInfo> = authRepository.restore(login.trim { it <= ' ' }, password.trim { it <= ' ' }, cityId, confirmHash)

    fun restore(password: String, confirmHash: String): Single<AuthInfo> = authRepository.restore(password.trim { it <= ' ' }, confirmHash)

    fun getToken(): String = authRepository.checkIsAuth()

    fun register(login: String,
                 family: String,
                 name: String,
                 password: String,
                 cityId: Int,
                 confirmHash: String): Single<AuthInfo> =
            authRepository.register(login.trim { it <= ' ' },
                    family.trim { it <= ' ' },
                    name.trim { it <= ' ' },
                    password.trim { it <= ' ' },
                    cityId, confirmHash)

    fun checkLoginAvailable(login: String): Single<LoginAvailable> = authRepository.checkLoginAvailable(login)

    fun saveTempCityInfo(authCityInfo: AuthCityInfo?) = authRepository.saveTempCityInfo(authCityInfo)

    fun checkPassword(password: String): Single<AuthInfo> = authRepository.checkPassword(password)

    fun auth(auth: Auth) {
        authRepository.saveToken(auth.token!!)
        profileRepository.saveProfile(auth.name!!, auth.phone!!, auth.location!!)
    }

    companion object {
        const val AUTH_ERROR = "HTTP 401 Unauthorized"
    }
}

Swift (iOS):

class AuthInteractor {
    
    public static let AUTH_ERROR = "HTTP 401 Unauthorized"
    
    private let authRepository: AuthRepository
    private let profileRepository: ProfileRepository
    private let cloudMessagingRepository: CloudMessagingRepository
    
    init(authRepository: AuthRepository,
            profileRepository: ProfileRepository,
            cloudMessagingRepository: CloudMessagingRepository) {
        self.authRepository = authRepository
        self.profileRepository = profileRepository
        self.cloudMessagingRepository = cloudMessagingRepository
    }
    
    func auth(login: String, password: String, cityId: Int) -> Observable<Auth> {
        return authRepository.auth(login: login.trim(), password: password.trim(), cityId: cityId, cloudMessagingToken: cloudMessagingRepository.getCloudToken())
    }
    
    func restore(login: String, password: String, cityId: Int, confirmHash: String) -> Observable<AuthInfo> {
        return authRepository.restore(login: login.trim(), password: password.trim(), cityId: cityId, confirmHash: confirmHash)
    }
    
    func restore(password: String, confirmHash: String) -> Observable<AuthInfo> {
        return authRepository.restore(password: password.trim(), confirmHash: confirmHash)
    }
    
    func getToken() -> String {
        return authRepository.checkIsAuth()
    }
    
    func register(login: String,
            family: String,
            name: String,
            password: String,
            cityId: Int,
            confirmHash: String) -> Observable<AuthInfo> {
        return authRepository.register(login: login.trim(),
                                    family: family.trim(),
                                    name: name.trim(),
                                    password: password.trim(),
                                    cityId: cityId,
                                    confirmHash: confirmHash)
    }
    
    func checkLoginAvailable(login: String) -> Observable<LoginAvailable> {
        return authRepository.checkLoginAvailable(login: login)
    }
    
    func saveTempCityInfo(authCityInfo: AuthCityInfo?) {
        authRepository.saveTempCityInfo(authCityInfo: authCityInfo)
    }
    
    func checkPassword(password: String) -> Observable<AuthInfo> {
        return authRepository.checkPassword(password: password)
    }
    
    func auth(auth: Auth) {
        authRepository.saveToken(token: auth.token)
        profileRepository.saveProfile(name: auth.name, phone: auth.phone, location: auth.location)
    }
}

Или же вот пример того как выглядят интерфейсы репозиториев на различных платформах:

Kotlin (Android)

interface AuthRepository {

    fun auth(login: String, password: String, cityId: Int, cloudMessagingToken: String): Single<Auth>

    fun register(login: String,
                 family: String,
                 name: String,
                 password: String,
                 cityId: Int,
                 confirmHash: String): Single<AuthInfo>

    fun restore(login: String, password: String, cityId: Int, confirmHash: String): Single<AuthInfo>

    fun restore(password: String, confirmHash: String): Single<AuthInfo>

    fun checkLoginAvailable(login: String): Single<LoginAvailable>

    fun sendCode(login: String): Single<CodeCheck>

    fun checkCode(hash: String, code: String): Single<CodeConfirm>

    fun checkIsAuth(): String

    fun saveToken(token: String)

    fun removeToken()

    fun notifyConfirmHashListener(confirmHash: String)

    fun getResendTimer(time: Long): Observable<Long>

    fun checkPassword(password: String): Single<AuthInfo>

    fun saveTempCityInfo(authCityInfo: AuthCityInfo?)

    fun saveTempConfirmInfo(codeConfirmInfo: CodeConfirmInfo)

    fun getTempCityInfo(): AuthCityInfo?

    fun getConfirmHashListener(): Observable<String>

    fun getTempConfirmInfo(): CodeConfirmInfo?
}

Swift (iOS):

protocol AuthRepository {
    
    func auth(login: String, password: String, cityId: Int, cloudMessagingToken: String) -> Observable<Auth>
    
    func register(login: String, family: String, name: String, password: String, cityId: Int, confirmHash: String) -> Observable<AuthInfo>
    
    func restore(login: String, password: String, cityId: Int, confirmHash: String) -> Observable<AuthInfo>
    
    func restore(password: String, confirmHash: String) -> Observable<AuthInfo>
    
    func checkLoginAvailable(login: String) -> Observable<LoginAvailable>
    
    func sendCode(login: String) -> Observable<CodeCheck>
    
    func checkCode(hash: String, code: String) -> Observable<CodeConfirm>
    
    func checkIsAuth() ->String
    
    func saveToken(token: String)
    
    func removeToken()
    
    func notifyConfirmHashListener(confirmHash: String)
    
    func getResendTimer(time: Int) -> Observable<Int>
    
    func checkPassword(password: String) -> Observable<AuthInfo>
    
    func saveTempCityInfo(authCityInfo: AuthCityInfo?)
    
    func saveTempConfirmInfo(codeConfirmInfo: CodeConfirmInfo)
    
    func getTempCityInfo() -> AuthCityInfo?
    
    func getConfirmHashListener() -> Observable<String>
    
    func getTempConfirmInfo() -> CodeConfirmInfo?
}

Аналогично дело обстоит и с presentation слоем, так как презентеры и view-интерфейсы на обеих платформах одинаковы. Поэтому благодаря такому переносу, моя скорость разработки увеличилась почти вдвое, так как из-за того, что на обеих платформах уже полностью сформированы domain и presentation слои, остается дело за малым — подключить специфичные библиотеки и доработать ui и data слои.

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

Автор: Raserad

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js