Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

Rubyエンジニア、Kotlinエンジニアへの道:Extension functionsって使っていいの?

はじめに

こんにちは。福岡開発拠点でエンジニアの小林です。

これまでは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を使っていくぞ!


マネーフォワード福岡開発拠点では、エンジニアを募集しています!

求人情報はこちらです。

hrmos.co

福岡開発拠点のサイトはこちらです。

fukuoka.moneyforward.com