riceIsland

学習メモ・備忘録

← 記事一覧に戻る

Now in Android(コードリーディング①)

内容

1. core/modelをなぜ最初に読んだのか?

純粋なKotlinのdata classで定義されたクラスで、他のデータ層などに依存しておらず、 この部分を読めば、どのようなビジネス上の概念を扱うのかを理解できるため。

2. ドメインモデルと合成ドメインモデルの分離パターン

  • ドメインモデル:アプリの目的に沿うデータ。ニュースアプリならニュース、レジピアプリならレシピ、食材。
  • 合成ドメインモデル:ドメインモデルにユーザー固有のデータを合成したクラス。

実際に、nowinandroidcore/model/dataでは以下のようにドメインモデルと合成ドメインモデルを定義している。

NewsResource.kt:記事クラス(ドメインモデル)

data class NewsResource(
    val id: String,
    val title: String,
    val content: String,
    val url: String,
    val headerImageUrl: String?,
    val publishDate: Instant,
    val type: String,
    val topics: List<Topic>,
)

UserNewsResource.kt:ユーザと記事の関係を集約したクラス(合成ドメインモデル)

data class UserNewsResource internal constructor(
    val id: String,
    val title: String,
    val content: String,
    val url: String,
    val headerImageUrl: String?,
    val publishData: Instant,
    val type: String,
    val followableTopics: List<Topics>,
    val isSaved: Boolean,
    val hasBeenViewed: Boolean,
) {
    constructor(newResource: NewsResource, userData: UserData) : this(
        id = newsResource.id,
        title = newsResource.title,
        content = newsResource.content,
        url = newsResource.url,
        headerImageUrl = newsResource.headerImageUrl,
        publishDate = newsResource.publishDate,
        type = newsResource.type,
        followableTopics = newsResource.topics.map { topic ->
            FollowableTopic(
                topic = topic,
                isFollowed = topic.id in userData.followedTopics,
            )
        },
        isSaved = newsResource.id in userData.bookmarkedNewsResources,
        hasBeenViewed = newsResource.id in userData.viewedNewsResources,        
    )
}

fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> =
    map { UserNewsResource(it, userData) }

ドメインモデルであるNewsResourceと合成ドメインモデルであるUserNewsResourceの違いとして、UserNewsResourceクラスには、 followableTopicsisSavedなどのユーザーが記事のトピックをフォローしているのかや保存しているのかなどのプロパティが追加してある。 ドメインモデルであるNewsResourcefollowableTopicsisSavedなどのプロパティを定義しないのは、NewsResourceはサーバー(ネットワーク)から取得するデータであり、 サーバーから取得するデータがユーザーに依存していると、サーバーへのリクエストが増えてしまい、ユーザーが多いアプリだと負荷が高くなってしまい、スケーラビリティというシステム負荷への対応力 が低い。サーバーから取得するデータがユーザーに非依存だとキャッシュに保存された同じデータを全員に返せるため、スケーラビリティの観点で良い。

3. UserDataの設計意図(なぜSet<String>なのか)

UserData.ktが以下のように定義されており、 ユーザーがブックマークしている記事を表すbookmarkedNewsResourceや記事を過去に見ているのかを表すviewedNewsResourceプロパティのデータ型がなぜ Set<String>であるかの理解に詰まった。

UserData.kt

data class UserData(
    val bookmarkedNewsResources: Set<String>,
    val viewedNewsResources: Set<String>,
    val followedTopics: Set<String>,
    val themeBrand: ThemeBrand,
    val darkThemeConfig: DarkThemeConfig,
    val useDynamicColor: Boolean,
    val shouldHideOnboarding: Boolean,
)

Set<NewsResource>ではなくて、Set<String>であるのは、このUserDataクラスはユーザー固有の設定を保持するクラスであり、少量のデータを格納するのに最適なDataStoreに保存されるため、 NewsResourceというニュース記事に纏わる多くの情報を含むオブジェクトを格納するのは不適で、記事のidだけわかれば大丈夫。

*ライセンス表記:本記事のコードはnowinandroidcore/modelを参考・引用しています。 Copyright 2022 The Android Open Source Project


独り言

サーバーから取得するデータとローカルから取得するデータを合成してクラスを定義することを合成パターン(Composition Pattern)と言うらしくて、この知識は他のプロジェクトにも活かせそう。 core/modelをリーディングする中で、Kotlinの知識の復習にもなった。(SetListの違い、data classdata objectの違い、enumについて、map構文、in演算子)