Модульная запутанность. Как распарсить одну и туже модель в разных модулях

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Немного о себе

Являюсь лидом мобильной команды разработки в финтех компании Peter Partner. Мы реализовали систему по автоматизации торговли, которая интегрирована с крупными торговыми брокерами. Проект локализован на множество языков и им пользуется свыше 1 млн. человек в странах Азии, Африки и Южной Америки.

Что мы хотели?

Максимально изолированные модули без копипаста.

Что имеем?

  1. KMP приложение для Android/iOS

  2. Ktor+Koin+Serialization

  3. MVI

Немного теории о многомодульности

Кто работал с этим может смело листать к следующему абзацу, кто об этом только слышал то остаемся, будет интересно.

Обычно выделяют 2 группы модулей: Core(базовый функционал не имеющий прямой связи с бизнес логикой приложения) и Feature(фичи приложения). Чаще всего еще есть Utils и что-то с UI (shared, android, ios). Основные группы делят на Api и Impl, то есть интерфейс и реализация. В идеальном мире FeatureApi модули не должны ничего знать друг о друге, но на практике так получается далеко не всегда. И если с этим еще можно жить, то вот делать зависимость одного Impl модуля на другой это совсем плохой тон. Как только вы разрешите себе сделать это хоть раз, то не заметите как ваше приложение стало ужасной паутиной с бесконечными циклическими зависимостями, которые будут решаться тем, что весь код переедет в один большой монолит.

Проблема

Есть модель в одном FeatureImpl модуле, которая будет использоваться в другом модуле. Как пример могу привести подписки. В нашем приложении есть два Feature модуля, которые так или иначе умеют "получать" подписки:

  • FeatureSubscriptionApi - отвечает за весь функционал связанный с подписками. Покупка, ограничения, преимущество разных планов и еще немного всего по мелочи.

  • FeatureSettingsApi - отвечает за экраны с настройками приложения и информацией по профилю. Как не странно, но он же занимается тем, что ходит на наш бекенд и берет информацию профиля пользователя. И как раз в профиле пользователя снова есть такой пункт как подписка и вся информация по ней

Далее возникает вопрос, что со всем этим делать и как не ломая зависимости распарсить одну и туже модель.

Решение

Сразу к сути, потом разберем детали. Идея простая, но в голову она пришла далеко не сразу(как это обычно бывает). Использовать дженери в связке с DI.

Интерефейс
interface Parser<JsonObject, CommonModel> {
    fun parse(from: JsonObject): CommonModel
}

В теории, данный интерфейс может кастовать, все во все. Но давай-те не делать из него монстра и оставим ему только тот функционал, который нужно. А именно парсинг json модели с бекенда в модель, которая используется по всему приложению.

Пример реализации
@Serializable
class SubscriptionsResImpl(
    val type: String,
    val productId: String? = null,
    val benefits: SubscriptionsBenefitsImpl,
)

@Serializable
class SubscriptionsBenefitsImpl(
    /*какие-то поля*/
)


val commonJsonConfig = Json {
    ignoreUnknownKeys = true
    allowSpecialFloatingPointValues = true
}

class SubscriptionParser : Parser<JsonObject, SubscriptionInfo?> {
    override fun parse(from: JsonObject): SubscriptionInfo? {
        val item = commonJsonConfig.decodeFromJsonElement<SubscriptionsResImpl>(from)
        val type = subscriptionTypeMap[item.type] ?: return null
        return SubscriptionInfo(
            productId = item.productId.orDefault(),
            benefits = item.benefits.let {
                SubscriptionBenefits(
                    /*какие-то поля*/
                )
            },
        )
    }

    companion object {
        val subscriptionTypeMap = SubscriptionType.entries.associateBy {
            when (it) {
                SubscriptionType.None -> "none"
                SubscriptionType.Starter -> "starter"
                SubscriptionType.Premium -> "premium"
                SubscriptionType.Pro -> "pro"
            }
        }
    }
}

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

Как я писал выше, работает это в связке с DI. Тут идея следующая - взять конкретную реализацию и привязать ее к конкретному qualifier. Мы в проекте используем koin. Поэтому и показывать буду на его примере. Кто использует Dagger, пожалуйста, все тоже самое там есть.

DI

Определим какие вообще могут быть парсеры при помощи enum class.

enum class QualifierParser {
        SubscriptionParserQualifier,
        //что-то еще
}

Пропишем все биндинги в KoinModule блоке.

singleOf(::SubscriptionParser) {
        qualifier = named(Parser.QualifierParser.SubscriptionParserQualifier)
        bind<Parser<JsonObject, SubscriptionInfo?>>()
}

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

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

Вызов в FeatureSettingsImpl
@Serializable
data class ProfileResImpl(
    val email: String,
    val id: String,
    val subscription: JsonObject,
)
class ProfileMapper :
    ReqResMapper<ProfileReq, ProfileReqImpl, ProfileResImpl, ProfileRes> {

    override fun fromCommon(from: ProfileReq): ProfileReqImpl =
        ProfileReqImpl()

    override fun toCommon(from: ProfileResImpl): ProfileRes {
        val parser =
            getSingle<Parser<JsonObject, SubscriptionInfo?>>(named(Parser.QualifierParser.SubscriptionParserQualifier))
        return ProfileRes(
            Profile(
                id = from.id,
                email = from.email,
                subscriptionBenefits = parser.parse(from.subscription)!!
            )
        )
    }
}

Тут используется интерфейс ReqResMapper . Функционал у него довольно простой, маппинг моделей между слоями. Его можно легко заменить на ваше решение.

Итог

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

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

Источник: https://habr.com/ru/articles/781292/


Интересные статьи

Интересные статьи

Пока педагоги бьют в колокол, опасаясь, что ChatGPT порушит систему образования, они сами ломают её изнутри популярным нейромифом о доминирующем стиле обучения. Опросник Высшей школы экономики по...
Неинтересная цель этой статьи — показать, как можно смержить две свертки пайторча в одну. А интересная цель — потыкать непосредственно в веса моделей на примере объединения свёрток. Узнать, как они хр...
Как можно перевыполнить KPI и, тем не менее, уйти в минус, а затем поменять схему работы и выйти в плюс для обеих сторон? Рассказываем на примере сотрудничества агентства Adsbalance с разработчиком иг...
Вокруг всё чаще и чаще слышно упоминание загадочных слов LoRa. Начиная от, разумеется, Хабра, и заканчивая прайсами поставщиков различного IoT-оборудования.Было очень любопытно самому убедиться, а пра...
Микроконтроллер esp32 примечателен многим, однако его наиболее известной характеристикой (которая, кстати, вполне себе «перевернула» рынок в своё время) является встроенная возможность осуществления...