
【サーバーサイドKotlin】データクラスの継承について
はじめに
以前の記事でもご紹介した通り、現在アプリボットでは新たなサーバーサイドの言語として、Kotlinの導入を進めています。
これまで主にJavaで作っていた基盤をKotlinに置き換える作業をしており、その中で引っかかった問題点を随時ご紹介していきたいと思います。
今回はKotlin独自の機能の一つである、データクラスについてのお話です。
問題点
データクラスを親クラスにできない
データクラスは下記のようにクラス定義に data
というキーワードを付けコンストラクタを設定することで、各プロパティの getter
setter
equals
toString
hashCode
copy
のメソッドを生成してくれる機能です。
テーブルに紐付いたエンティティクラスを作る時などにとても便利です。
data class User(
// varの場合はgetter、setter両方が作られる
var id: Int,
// valの場合はgetterのみ作られる
val name: String
)
しかし、ここで問題になったのが継承でした。
データクラスは継承させることができません。
もともとアプリボットの基盤では、APIのRequest、Responseのパラメータを表すクラスをを下記のような形で実装していました。
public abstract class BaseRequest {
private Integer id;
private Date requestTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Date getRequestTime() {
return requestTime;
}
public void setRequestTime(Date requestTime) {
this.requestTime = requestTime;
}
}
public class SampleRequest extends BaseRequest {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
パラメータには全APIで共通して持っている項目があり、それをBaseRequestという親クラスに定義していました。
そして各APIのクラスでBaseRequestを継承し、それぞれ必要なパラメータを子クラスで定義しています。
これがKotlinではBaseRequestをデータクラスとして定義した時点で継承できなくなりました。
対応方法
対応方法として下記の3つを検討しました。
- BaseRequestをインターフェースにする
- 継承をやめDelegationを使う
- データクラスとして定義するのをやめる
BaseRequestをインターフェースにする
データクラスは継承はできませんが、インターフェースを実装することはできます。
interface BaseRequest {
var id: Int
var requestTime: Date?
}
data class SampleRequest(override var id: Int, override var requestTime: Date?, var name: String?) : BaseRequest
このようにすれば、データクラスに共通項目を持たせることができます。
同様の問題の対応方法としては、一般的によくある手段です。
ただ、この形を取ると全てのRequestクラスで共通項目のプロパティをオーバーライドする必要があります。
APIの数の多いプロダクトでは、BaseRequestにプロパティを追加した際の影響が大きくなるため、不採用としました。
継承をやめDelegationを使う
KotlinではDelegationという機能があります。
インターフェースを実装する際、都度オーバーライドする必要のないプロパティや関数の処理を、別の実装クラスに委譲する方法です。
下記のように、インターフェースの宣言の後ろに by
というキーワードでつなげることで実装できます。
interface BaseSampleService {
fun execute1()
fun execute2()
}
class CommonSampleService : BaseSampleService {
override fun execute1() {
println("common1")
}
override fun execute2() {
println("common2")
}
}
class SpecialSampleService : BaseSampleService by CommonSampleService() {
override fun execute1() {
println("special1")
}
}
この例では、BaseSampleServiceを実装したSpecialSampleServiceで、execute1だけオーバーライドしてexecute2の処理はCommonSampleServiceと同じものになります。
つまり、execute2の処理をCommonSampleServiceに委譲しているということになります。
この機能を利用して、下記の形でRequestクラスを作りました。
interface BaseRequest {
var id: Int
var requestTime: Date?
}
class BaseRequestImpl : BaseRequest {
override var id: Int = 0
override var requestTime: Date? = null
}
data class DelegateSampleRequest(var name: String?): BaseRequest by BaseRequestImpl()
各Requestクラスで BaseRequest by BaseRequestImpl()
とDelegationを書いてBaseRequestを実装すれば、毎回プロパティをオーバーライドする必要がなく、BaseRequestにプロパティが追加されたとしても各Requestクラスに変更を加える必要がありません。
ただ、Delegationはあくまでインターフェースの処理を委譲する機能のため、この実装のためにBaseRequestをインターフェースと実装クラスとそれぞれ用意しなければなりません。
これではやや無駄があって分かりづらく感じたため、この方法も不採用となりました。
また、データクラスで自動生成される toString
hashCode
と言ったメソッドに、BaseRequestで持っているプロパティが反映されないため、不完全でもあります。
データクラスとして定義するのをやめる
今回はこちらの方法を取りました。
データクラスとして定義せず、下記のようにプロパティを定義するだけでもKotlinではgetter、setterは作られます。
open class BaseRequest {
var id: Int = 0
var requestTime: Date? = null
}
class SampleRequest : BaseRequest() {
var name: String? = null
}
こうすれば継承することもできます。
データクラスにした際と何が違うかというと、equals
hashCode
toString
copy
が自動では作られない点です。
これらのメソッドは本来作られている方がオブジェクトとしては望ましいですが、RequestとResponseのオブジェクト同士で直接比較したり、Mapのキーに使ったりすることは基本的にないため、不要と判断しました。
また、もし必要なクラスがあれば都度実装すれば問題ないため、ここに関してはデータクラスの使用をやめました。
この観点で言うと前述のDelegationを使う方法で equals
や hashCode
が不完全な点も問題ないのですが、実装のシンプルさからこちらを採用しています。
まとめ
今回の件では使わないという選択肢を取りましたが、データクラスは機能としてはとても便利なものです。
アプリボットでもRequest、Response以外で同様の用途のクラス、では基本的にデータクラスを使っています。
手軽に作れてコードもシンプルになるので、不都合のない場面であれば積極的に使うべきだと思います。
最後に
今後もこういった形で実際に直面した問題に対して、取った対応方法をご紹介していきます。
その中で実践で使えるサーバーサイドKotlinのノウハウをお伝えしていくので、次回以降も是非御覧ください。
この記事へのコメントはありません。