はじめに
この記事はAndroidAdventCalendar13日目の記事です。AdventCalendar初参加!
ここ1年間はCleanArchitectureでSwiftとKotlin開発やってました。
この記事ではAndroid開発でKotlin採用した際に検討したこと、特にドメイン設計周りをJavaと比較してみようと思います。
Kotlinどれくらいやってた
はじめて4ヶ月くらいのひよっこです
春くらいまではiOS(Swift)で、8月くらいからAndroid(Kotlin)を始めてみました。
CleanArchitectureは我流な所あるので単語の使用など間違いがあったらごめんなさい
Kotlinを採用する前に考えたこと
CleanArchitectureはPresentation層の特にMVPに目がいきがちですが、Domain層の表現でアプリ全体の設計が決まってくると考えています。そこで、KotlinのNull安全
とフィールドに自動生成されるAccessor
(ここではDefault Accessorと呼びます。)とやらがドメインモデルをスッキリ堅牢にできて全体の設計に寄与できそうだなと考えてました。
あとJavaと共存できるので書き方分からなかったらJavaで書けばいいや
と考えていました
KotlinとJavaを比較してみる
KotlinとJavaでそれぞれ作成したUserクラスを比較してみましょう。
まずはNullチェック周りからみてみます。
final & Exception vs val
public class JavaUser {
private final String userId;
public JavaUser(String userId) {
if (userId == null) {
throw new IllegalArgumentException("Function argument is null!");
}
this.userId = userId;
}
public String getUserId() {
return userId;
}
}
Javaの場合上記のようにモデルを作成していました。
コンストラクタでnullかどうかチェックし、nullが入っていた場合IllegalArgumentExceptionで意図的にクラッシュさせています。
class KotlinUser(val userId: String)
Kotlinの場合上記のように書けます。
呼び出し時
JavaUserモデルは実行時にクラッシュしてスタックトレースを見るまで気付けません。
しかし、KotlinUserモデルはビルドエラーになるためすぐ気付くことができます。
NotNull アノテーション vs val
JavaではNotNull系アノテーションを使うという手もあります。
public class JavaUser2 {
private final String userId;
public JavaUser2(@NotNull String userId) {
this.userId = userId;
}
public String getUserId() {
return userId;
}
}
ここではコンストラクタの引数にNotNullアノテーションを追加しています。
呼び出し時
JavaUser2もKotlinUserもビルド時エラーになるので同様の結果になりました。
ただし、アノテーションは既存の記述に「追加」する形になるので「書くのを忘れやすい!」という問題があります。
※主観的な問題ですが、同僚に聞いても賛同を得られたのである程度共通の問題と思っています。
Getter & Setter vs Default Accessor
次はフィールドアクセスについて比較してみましょう。
UserクラスにfirstNameとlastNameを追加し、fullName()でフルネームを返却する振る舞いを追加します。
public class JavaUser {
private final String userId;
private final String firstName;
private final String lastName;
public JavaUser(String userId, String firstName, String lastName) {
if (userId == null || firstName == null || lastName == null) {
throw new IllegalArgumentException("Function argument is null!");
}
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
}
public String fullName() {
return firstName + lastName;
}
public String getUserId() {
return userId;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
Javaの場合、フィールドアクセスにはGetter&Setterを定義すると思います。
公開したいフィールド数に応じて増えていきます。
class KotlinUser(val userId: String, val firstName: String, val lastName: String) {
fun fullName() : String {
return firstName + lastName
}
}
Kotlinの場合、暗黙的にアクセッサが生成されます。
valの場合getterのみ。
varの場合getter/setterが生成されます。
モデルの見通し
ここで比較したいのはモデルの見通しです。
ドメインモデルでは振る舞いに着目したいので、各UserクラスではfullName()に着目します。
ただし、JavaUserクラスではgetter/setterによって関数が増えているので見通しが悪くなっています。
振る舞いが増えていくほど見通しが悪くなっていきます。
ドメインモデルの影響範囲
今回作成した各Userクラスをドメインのエンティティとして利用した場合、以下のように利用されると思います。
矢印はデータの流れを意味しています。
ユーザ作成画面:CreateUserActivity
↓ "mitsuha", "miyamizu"でsubmit
CreateUserUseCase
↓ new User("1234", "mitsuha", "miyamizu")
UserRepository
↓ userの各フィールドをgetしてストレージに書き込む
ストレージ
ユーザ表示画面:UserActivity
↑ userの各フィールドをgetして表示する
GetUserUseCase
↑ user
UserRepository
↑ ストレージから読み込んでnew User("1234", "mitsuha", "miyamizu")
ストレージ
ドメインのエンティティ(今回はUserクラス)はドメイン層を中心に、プレゼンテーション層/データ層とのIF部分までエンティティのまま受け渡され、大体はViewのIF/DBのIFまで利用されるんじゃないかなと思います。つまりアプリ全体に影響があると思っています。MVCなど他の設計パターンでも同様だと思います。
NullPointerException対応からの解放、読みやすいクラスで設計が破綻しにくくなるのではないかと。
導入してみてどうだった?
一番はKotlin先駆者様方同様、NullPointerExceptionが発生しなくなったことです。最高です。
Nullチェックが大幅に減ったので本来注目したい実装に集中できるようになりました。
クラスもGetter/Setterなどコード量が減ったのでぱっと見わかりやすいクラスが増えていい感じです。
最後に
Kotlin先駆者様方によってKotlinの色んなメリットが挙げられていますが、「Null安全」、「コード量削減」だけ見てもかなり優位性を持っています。
エンジニアとして良い設計と作業効率を上げる為にも、良いものはどんどん取り入れていかないといけないし、何より書いていてKotlin快適なのでぜひぜひ皆さんにも使ってもらいたいと思っています。
個人的にはここからcompanion object
map
lazy
lateinit
Class Extension
辺りを導入していくとよりKotlinらしいコードになると思うのでぜひお試しを
最後まで読んでいただきありがとうございました!