はじめに
こんにちは。福岡開発拠点でエンジニアの小林です。
これまではRubyで開発をしてきましたが、最近はサーバーサイドKotlinでの開発を行っています。
Rubyエンジニアの私がKotlinを学ぶ上で疑問に思ったこととして、前回は「Companion objectとは何なのか」について紹介しました。
今回はExtension functionsについて学んだことを書きます。
対象読者
RubyエンジニアでKotlinを学び始めた方
疑問:Extension functionsって使っていいの?
Extension functionsとは、Kotlinでクラスに新しい関数を拡張する機能です。
Extension functionsを使う場面を、Rubyのコードと比較しながら見てみましょう。
まず、Rubyでこのようなコードがあるとします。
class Traveler def greeting puts "Hello!" end end class Waiter def greeting puts "Welcome!" end end class Pub def initialize(waiter:) @waiter = waiter @travelers = [] end def accept(travelers) @waiter.greeting travelers.each do |traveler| traveler.greeting @travelers << traveler end end end traveler1 = Traveler.new traveler2 = Traveler.new waiter = Waiter.new pub = Pub.new(waiter:) traveler_list = [traveler1, traveler2] pub.accept(traveler_list)
実行するとこのようになります。
Welcome! Hello! Hello!
さて、ここでクラスPubに以下のようなserve_beers
メソッドを追加したいとします。
class Pub ... def serve_beers ## Waiterは「Here are your beers. Enjoy!」と言う ## Travelersは「Cheers!」と言いビールを楽しむ end end
Rubyで書く場合、どのように解決できるでしょうか?
私であれば、まずWaiterにメソッドを追加します。
class Waiter ... ## 追加 def bring_beers puts "Here are your beers. Enjoy!" end end
ウェイターがビールを運んでくるという行為はWaiterクラスの責務の範囲かなと思うので、特に違和感はありません。 Travelerも同じような方法で解決させてみます。
class Traveler ... def enjoy_beer puts "Cheers!" end end
Pubのserve_beers
メソッドも完成させ、実行させてみましょう。
class Traveler def greeting puts "Hello!" end def enjoy_beer puts "Cheers!" end end class Waiter def greeting puts "Welcome!" end def bring_beers puts "Here are your beers. Enjoy!" end end class Pub def initialize(waiter:) @waiter = waiter @travelers = [] end def accept(travelers) @waiter.greeting travelers.each do |traveler| traveler.greeting @travelers << traveler end end def serve_beers @waiter.bring_beers @travelers.each do |traveler| traveler.enjoy_beer end end end traveler1 = Traveler.new traveler2 = Traveler.new waiter = Waiter.new pub = Pub.new(waiter:) traveler_list = [traveler1, traveler2] pub.accept(traveler_list) pub.serve_beers --- Welcome! Hello! Hello! Here are your beers. Enjoy! Cheers! Cheers!
たしかにやりたいことが実現できました。
しかし、Waiterにbring_beers
メソッドを追加したのはWaiterクラスの責務という観点でも良さそうですが、Travelerのenjoy_beer
メソッドは特定のユースケースだけのものになってしまったように思えます(Pubクラスのためだけのメソッドのように思える)。
もちろん設計次第ではありますが、このやり方だとTravelerクラスはどんどん肥大化していきそうです。
この疑問が適切だったとして、Rubyではどのように解決できるでしょうか?
パッと思いつくのはメソッドをModuleとして切り出してTravelerクラスにミックスインさせるとかでしょうか。しかし、利用箇所が一つしかない段階でModuleとして切り出すのはやややり過ぎのようにも思えます。
Kotlinでは、このユースケースをExtension functionsを使うことで解決できます。
Extension functionsを使う
Kotlinで以下のように書き換えることができます。
class Traveler { fun greeting() { println("Hello!") } } class Waiter { fun greeting() { println("Welcome!") } fun bringBeers() { println("Here are your beers. Enjoy!") } } class Pub( private val waiter: Waiter, ) { private val travelers = mutableListOf<Traveler>() fun accept(travelers: List<Traveler>) { waiter.greeting() for (traveler in travelers) { traveler.greeting() this.travelers.add(traveler) } } fun serveBeers() { waiter.bringBeers() for (traveler in travelers) { traveler.enjoyBeer() } } private fun Traveler.enjoyBeer() { println("Cheers!") } } fun main() { val traveler1 = Traveler() val traveler2 = Traveler() val waiter = Waiter() val pub = Pub(waiter) val travelerList = listOf(traveler1, traveler2) pub.accept(travelerList) pub.serveBeers() } main()
実行すると同じ結果になります。
Welcome! Hello! Hello! Here are your beers. Enjoy! Cheers! Cheers!
Pubクラス内に記載したprivate fun Traveler.enjoyBeer()
がExtension functionsになります。その名の通り、Travelerクラスを拡張してenjoyBeer()
メソッドをTravelerクラスに追加しています。
そのため、serveBeers()
メソッドのブロック内でTravelerクラスのインスタンスはenjoyBeer()
を呼ぶことができます。
そ、そんなことして大丈夫なのか?!
Kotlin公式リファレンスの「Coding conventions」において、Extension functionsに対しこのように記載しています。
Use extension functions liberally. Every time you have a function that works primarily on an object, consider making it an extension function accepting that object as a receiver. To minimize API pollution, restrict the visibility of extension functions as much as it makes sense. As necessary, use local extension functions, member extension functions, or top-level extension functions with private visibility.
意訳すると「遠慮なく使ってください。何かオブジェクトに対して機能させるようなときはExtension functionsにすることを検討してね。ただし、APIがクリーンに保たれるようExtension functionsを適用するスコープには気をつけてね」といった感じでしょうか。
適用されるスコープというのは、上記の例で言うと、Traveler.enjoyBeer()
はPubクラス内のみで拡張されています。Pubクラス内で定義しているからです。反対にもしPubクラス以外でも適用させたい場合には、Pubクラス外のトップレベルで定義することでPubクラスと同じパッケージ内で利用可能になります。
Rubyの場合オープンクラスを使って似たような拡張を行うことができますが、オープンクラスは強力が故に使い方には注意が必要です。その点、Extension functionsでは定義場所によって適用させるスコープを限定させることができます。
またRubyではクラス継承を自由にできますが、Kotlinはクラスがデフォルトで閉じられており継承できません。そのため、なかなか継承を使ってクラスを拡張させるということができません。
このような場合にKotlinではExtension functionsが活躍し、プリミティブ型やサードパーティーのライブラリ拡張などにも使うことができます。
最初見たときは気軽にやってるように見えて「そんなカジュアルにやって大丈夫なのか?!」と思いましたが、「あるクラスに関係するExtension functionsはそのクラスの近くに置きましょう」というのは、コードの読み手に対して拡張させる意図を伝えやすいですし、またスコープも限定できるため、便利で合理的だなと考えるようになりました。
おわりに
今回はExtension functionsについて学んだことを書きました。Rubyエンジニアが戸惑う(かもしれない)ポイントを今後も書いていけたらと思います。
適切にExtension functionsを使っていくぞ!
マネーフォワード福岡開発拠点では、エンジニアを募集しています!
求人情報はこちらです。
福岡開発拠点のサイトはこちらです。