[Из песочницы] Clean architecture в контексте кроссплатформенной разработки
Всем привет. В последнее время довольно много статей написано на тему 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 = authRepository.auth(login.trim { it <= ' ' }, password.trim { it <= ' ' }, cityId, cloudToken)
fun restore(login: String, password: String, cityId: Int, confirmHash: String): Single = authRepository.restore(login.trim { it <= ' ' }, password.trim { it <= ' ' }, cityId, confirmHash)
fun restore(password: String, confirmHash: String): Single = 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 =
authRepository.register(login.trim { it <= ' ' },
family.trim { it <= ' ' },
name.trim { it <= ' ' },
password.trim { it <= ' ' },
cityId, confirmHash)
fun checkLoginAvailable(login: String): Single = authRepository.checkLoginAvailable(login)
fun saveTempCityInfo(authCityInfo: AuthCityInfo?) = authRepository.saveTempCityInfo(authCityInfo)
fun checkPassword(password: String): Single = 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 {
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 {
return authRepository.restore(login: login.trim(), password: password.trim(), cityId: cityId, confirmHash: confirmHash)
}
func restore(password: String, confirmHash: String) -> Observable {
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 {
return authRepository.register(login: login.trim(),
family: family.trim(),
name: name.trim(),
password: password.trim(),
cityId: cityId,
confirmHash: confirmHash)
}
func checkLoginAvailable(login: String) -> Observable {
return authRepository.checkLoginAvailable(login: login)
}
func saveTempCityInfo(authCityInfo: AuthCityInfo?) {
authRepository.saveTempCityInfo(authCityInfo: authCityInfo)
}
func checkPassword(password: String) -> Observable {
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
fun register(login: String,
family: String,
name: String,
password: String,
cityId: Int,
confirmHash: String): Single
fun restore(login: String, password: String, cityId: Int, confirmHash: String): Single
fun restore(password: String, confirmHash: String): Single
fun checkLoginAvailable(login: String): Single
fun sendCode(login: String): Single
fun checkCode(hash: String, code: String): Single
fun checkIsAuth(): String
fun saveToken(token: String)
fun removeToken()
fun notifyConfirmHashListener(confirmHash: String)
fun getResendTimer(time: Long): Observable
fun checkPassword(password: String): Single
fun saveTempCityInfo(authCityInfo: AuthCityInfo?)
fun saveTempConfirmInfo(codeConfirmInfo: CodeConfirmInfo)
fun getTempCityInfo(): AuthCityInfo?
fun getConfirmHashListener(): Observable
fun getTempConfirmInfo(): CodeConfirmInfo?
}
Swift (iOS):
protocol AuthRepository {
func auth(login: String, password: String, cityId: Int, cloudMessagingToken: String) -> Observable
func register(login: String, family: String, name: String, password: String, cityId: Int, confirmHash: String) -> Observable
func restore(login: String, password: String, cityId: Int, confirmHash: String) -> Observable
func restore(password: String, confirmHash: String) -> Observable
func checkLoginAvailable(login: String) -> Observable
func sendCode(login: String) -> Observable
func checkCode(hash: String, code: String) -> Observable
func checkIsAuth() ->String
func saveToken(token: String)
func removeToken()
func notifyConfirmHashListener(confirmHash: String)
func getResendTimer(time: Int) -> Observable
func checkPassword(password: String) -> Observable
func saveTempCityInfo(authCityInfo: AuthCityInfo?)
func saveTempConfirmInfo(codeConfirmInfo: CodeConfirmInfo)
func getTempCityInfo() -> AuthCityInfo?
func getConfirmHashListener() -> Observable
func getTempConfirmInfo() -> CodeConfirmInfo?
}
Аналогично дело обстоит и с presentation слоем, так как презентеры и view-интерфейсы на обеих платформах одинаковы. Поэтому благодаря такому переносу, моя скорость разработки увеличилась почти вдвое, так как из-за того, что на обеих платформах уже полностью сформированы domain и presentation слои, остается дело за малым — подключить специфичные библиотеки и доработать ui и data слои.
Спасибо за то что дочитали до конца. Надеюсь данная статья принесет пользу мобильным разработчикам, которые занимаются нативной разработкой. Всего наилучшего.