はじめに
皆様こんにちは、ソーシャル経済メディア「NewsPicks」(Media Infrastructureチーム)エンジニアの北見です。
弊社では Server Side Kotlin
を採用しており、昔に書かれた一部のコードは Java
ですが、基本的に新規コードは Kotlin
で書いており、既存の Java
コードも Kotlin
化を推し進めています。
今回は Kotlin
の sealed class
を使って、コードをシンプルにする例をご紹介します。
微妙に異なるけど殆ど同じ処理を共通化
例えば、クレジットカードとキャリア決済の2通りで商品を購入するユースケースを考えてみます。
class PurchaseService( val productRepository: ProductRepository, val purchaseRepository: PurchaseRepository, val purchaseHistoryRepository: PurchaseHistoryRepository ){ fun purchaseWithCreditCard(productId: Int, creditCardNum: Int) { val product = productRepository.findProduct(productId) if (product == null){ throw ResourceNotFoundException("Product not found") } val purchaseResult = purchaseRepository.purchaseWithCreditCard(productId, creditCardNum) purchaseHistoryRepository.save(productId, purchaseResult.id) } fun purchaseWithCarrier(productId: Int, carrier: CarrierCashInfo) { val product = productRepository.findProduct(productId) if (product == null){ throw ResourceNotFoundException("Product not found") } val purchaseResult = purchaseRepository.purchaseWithCarrier(productId, carrier) purchaseHistoryRepository.save(productId, purchaseResult.id) } }
2つのメソッドは
productId
の商品があれば処理を続行、なければエラーそれぞれの方法で購入
購入履歴を保存
という処理を行っていますが、1. と 3. は全く同じ処理です。
なんとか同一メソッドで表現できないでしょうか?
class PurchaseService ( val productRepository: ProductRepository, val purchaseRepository: PurchaseRepository, val purchaseHistoryRepository: PurchaseHistoryRepository ){ fun purchase(productId: Int, creditCardSeq: Int?, carrier: CarrierCashInfo?) { val product = productRepository.findProduct(productId) if (product == null){ throw ResourceNotFoundException("Product not found") } if (creditCardSeq == null && carrierCashInfo == null){ throw InvalidArgumentException("xxxxx") } var purchaseId = 0 if (creditCardSeq != null){ purchaseId = purchaseRepository.purchaseWithCreditCard(productId, creditCardNum!!).id } else if (carrier != null){ purchaseId = purchaseRepository.purchaseWithCarrier(productId, carrier!!).id } purchaseHistoryRepository.save(productId, purchaseId) } }
うーん、強引に同じメソッドに突っ込んでみましたが、分かりにくいですね。引数によって条件が分かれてしまい、追いづらいです。
このように、微妙に異なるけど殆ど同じ処理を共通化する とき、sealed class
を使ってとてもスッキリさせることができます。
この場合、まずクレジット決済もキャリア決済も共通のプロパティを sealed class
に、固有のプロパティを継承先に定義します。
sealed class PurchaseRequest { abstract val productId: Int } data class PurchaseWithCreditCardRequest ( override val productId: Int, val creditCardNum: Int ) : PurchaseRequest() data class PurchaseWithCarrierRequest ( override val productId: Int, val carrier: CarrierCashInfo ) : PurchaseRequest()
これを when ~ is
をつかって条件分岐を表現してあげると
class PurchaseService ( val productRepository: ProductRepository, val purchaseRepository: PurchaseRepository, val purchaseHistoryRepository: PurchaseHistoryRepository ){ fun purchase(request: PurchaseRequest){ val product = productRepository.findProduct(request.productId) if (product == null){ throw ResourceNotFoundException("Product not found") } // requestの型に応じた条件分岐 val result = when (request) { is PurchaseWithCreditCardRequest -> purchaseRepository.purchaseWithCreditCard(productId, request.creditCardNum) is PurchaseWithCarrierRequest -> purchaseWithCarrier.purchaseWithCarrier(productId, request.carrier) } purchaseHistoryRepository.save(productId, result.id) }
どうでしょうか、明らかに見やすくなったのではないでしょうか?
sealed classのサブクラスはコンパイル時に把握されており、そのsealed classが定義されているモジュールやパッケージの外で他のサブクラスが出現することはありません。
そのため今回ご紹介したサンプルコードの when ~ is
の条件分岐で対応漏れが出ることはなく、保守性も上々です。
さいごに
最近 sealed class
を知った私が、自分なりにその魅力を言語化すると
微妙に異なるけど殆ど同じ処理を、if文を使わずシンプルに記述できる
ことにあるのではないかと思っています。
同様のケースで悩まれている方の力になれたのであれば幸いです。
ここまで読んで下さり、ありがとうございました。