Всем привет. В последнее время довольно много статей написано на тему clean architecture. То есть чистой архитектуры, которая позволяет писать приложения, удобные в сопровождении и тестировании. Про саму чистую архитектуру вы можете прочитать в таких замечательных статьях как: Заблуждения Clean Architecture или Чистая архитектура, поэтому не вижу смысла повторять то, что уже написано.
Для начала позвольте представиться, меня зовут Какушев Расул. Так уж получилось что я одновременно занимаюсь нативной разработкой на ios и android, а так же разработкой backend-кода мобильных приложений, в компании Navibit. Это пока еще малоизвестная компания, которая только готовится выйти на рынок продажи строительных материалов. У нас очень маленькая команда и поэтому разработка мобильных приложений целиком и полностью ложится на мои (еще не слишком профессиональные) плечи.
В моей работе часто приходится делать одно приложение на ios и android, и как вы понимаете, в силу различий платформ, часто приходится писать один и тот же функционал несколько раз. Это занимает довольно много времени, и поэтому некоторое время назад, когда я познакомился с clean architecture, мне пришла в голову такая мысль: языки kotlin и swift довольно похожи, однако платформы различаются, но в clean architecture есть domain слой, который не привязан к платформе, а содержит чистую бизнес-логику. Что будет если просто взять весь domain слой из android и перенести его в ios, с минимальными изменениями?
Что же, задумано — сделано. Я начал перенос. И действительно идея оказалась в большинстве своем верной. Сами посудите. К примеру вот один интерактор на kotlin и swift:
Kotlin (Android)
Swift (iOS):
Или же вот пример того как выглядят интерфейсы репозиториев на различных платформах:
Kotlin (Android)
Swift (iOS):
Аналогично дело обстоит и с presentation слоем, так как презентеры и view-интерфейсы на обеих платформах одинаковы. Поэтому благодаря такому переносу, моя скорость разработки увеличилась почти вдвое, так как из-за того, что на обеих платформах уже полностью сформированы domain и presentation слои, остается дело за малым — подключить специфичные библиотеки и доработать ui и data слои.
Спасибо за то что дочитали до конца. Надеюсь данная статья принесет пользу мобильным разработчикам, которые занимаются нативной разработкой. Всего наилучшего.
Для начала позвольте представиться, меня зовут Какушев Расул. Так уж получилось что я одновременно занимаюсь нативной разработкой на 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 слои.
Спасибо за то что дочитали до конца. Надеюсь данная статья принесет пользу мобильным разработчикам, которые занимаются нативной разработкой. Всего наилучшего.