1. イントロ

レガシーコードベースを使用する場合、外部ライブラリを使用する場合、またはフレームワークに対して統合する場合、さまざまなオブジェクトまたはデータ構造間でマッピングするユースケースが定期的にあります。

このチュートリアルでは、組み込みのKotlin機能を使用してこの目標を簡単に達成する方法を見ていきます。

2. 単純な拡張機能

次の例を使用してみましょう。クラスUserがあります。これは、コアドメインのクラスである可能性があります。 リレーショナルデータベースからロードするエンティティである可能性もあります。

data class User(
  val firstName: String,
  val lastName: String,
  val street: String,
  val houseNumber: String,
  val phone: String,
  val age: Int,
  val password: String)

ここで、このデータについて別のビューを提供したいと思います。 このクラスをUserViewと呼ぶことにしました。これは、Webコントローラーからの応答送信として使用されていると想像できます。 ドメイン内の同じデータを表していますが、一部のフィールドはUserクラスのフィールドの集合であり、一部のフィールドは単に異なる名前を持っています。

data class UserView(
  val name: String,
  val address: String,
  val telephone: String,
  val age: Int
)

ここで必要なのは、 User -> UserViewをマッピングするマッピング関数です。 UserView はアプリケーションの外層にあるため、この関数をUserクラスに追加したくありません。 また、 UserView オブジェクトを作成するために、Userクラスのカプセル化を解除し、ヘルパークラスを使用してUserオブジェクトにアクセスし、そのデータを取得する必要はありません。

幸い、Kotlinは拡張機能と呼ばれる言語機能を提供します。 Userクラスで拡張関数を定義し、定義したパッケージスコープ内でのみアクセスできるようにすることができます。

fun User.toUserView() = UserView(
  name = "$firstName $lastName",
  address = "$street $houseNumber",
  telephone = phone,
  age = age
)

テスト内でこの関数を使用して、使用方法を理解してみましょう。

class UserTest {

    @Test
    fun `maps User to UserResponse using extension function`() {
        val p = buildUser()
        val view = p.toUserView()
        assertUserView(view)
    }

    private fun buildUser(): User {
        return User(
          "Java",
          "Duke",
          "Javastreet",
          "42",
          "1234567",
          30,
          "s3cr37"
        )
    }

    private fun assertUserView(pr: UserView) {
        assertAll(
          { assertEquals("Java Duke", pr.name) },
          { assertEquals("Javastreet 42", pr.address) },
          { assertEquals("1234567", pr.telephone) },
          { assertEquals(30, pr.age) }
        )
    }

3. Kotlinリフレクション機能

上記の例は非常に単純ですが(したがって、ほとんどのユースケースで推奨されます)、それでもボイラープレートコードが少し含まれています。 多くのフィールド(おそらく数百)を持つクラスがあり、それらのほとんどをターゲットクラスの同じ名前のフィールドにマップする必要がある場合はどうなりますか?

この場合、 Kotlin Reflection 機能を使用して、ほとんどのマッピングコードを記述しないようにすることを検討できます。

リフレクションを使用したマッピング関数は次のようになります。

fun User.toUserViewReflection() = with(::UserView) {
    val propertiesByName = User::class.memberProperties.associateBy { it.name }
    callBy(parameters.associate { parameter ->
        parameter to when (parameter.name) {
            UserView::name.name -> "$firstName $lastName"
            UserView::address.name -> "$street $houseNumber"
            UserView::telephone.name -> phone
            else -> propertiesByName[parameter.name]?.get(this@toUserViewReflection)
        }
    })
}

Kotlin with()関数を使用して、メソッド呼び出しレシーバーとしてUserViewデフォルトコンストラクターを使用しています。 with()、に提供されるラムダ関数内で、リフレクションを使用して、 Map のメンバープロパティ(メンバー名をキー、メンバープロパティを値)を取得します。 X197X] User :: class.memberProperties.associateBy{it.name}。

次に、カスタムパラメーターマッピングを使用してUserViewコンストラクターを呼び出します。 ラムダ内では、whenキーワードを使用して条件付きマッピングを提供します。

興味深い事実は、単純な Strings の代わりに、 UserView :: name.name のように、リフレクションを使用して取得した実際のパラメーター名をマップできることです。 これは、ここでKotlinコンパイラを完全に活用できることを意味します。コードが破損することを恐れずにリファクタリングの場合に役立ちます。

パラメータname、address、phoneにはいくつかの特別なマッピングがありますが、他のすべてのフィールドにはデフォルトの名前ベースのマッピングを使用しています。

リフレクションベースのアプローチは一見非常に興味深いように見えますが、これによりコードベースがさらに複雑になり、リフレクションを使用すると実行時のパフォーマンスに悪影響が及ぶ可能性があることに注意してください。

4. 結論

組み込みのKotlin言語機能を使用して、単純なデータマッピングのユースケースを簡単に解決できることを確認しました。 マッピングコードを手動で記述することは単純なユースケースでは問題ありませんが、リフレクションを使用してより複雑なソリューションを記述することもできます。

すべてのコード例はGitHubにあります。