Money Forward Developers Blog

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

20230215130734

Rubyエンジニア、Kotlinエンジニアへの道:Companion objectとは何なのか

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

これまではRubyで開発をしてきましたが、最近はサーバーサイドKotlinでの開発を行っています。 私にとってKotlinを使って実装するのは全くの初めてです。日々悪戦苦闘しながら学びを深めています。

Ruby一筋で育ってきたからなのか、Kotlinでの実装最中に「Rubyだとあんな風に書けるけど、これKotlinでどう書くんだろうな」だったり「なんでこういう書き方してるんだろうな」だったりを感じることが多くあるのですが、そういった場合におけるRubyエンジニア観点からの記事があまり見当たらないなと感じました。

そこで、私なりの視点でRubyエンジニアがサーバーサイドKotlinを実装するにあたり疑問に思う(かも?)しれないポイントを見つかり次第ブログにしていければと思います。

この記事はそれの第一弾です。

対象読者

RubyエンジニアでKotlinを学び始めた方

疑問:Companion objectとは何なのか

Kotlinで開発しているとKotlin経験のあるチームメンバーから「ここはCompanion objectの方が良いよ」と教えてもらうことがありました。

私:「Companion objectって何?」

今回はこの疑問について調べます。

まずは公式リファレンスの説明を読む

公式リファレンスを読むと以下のように書かれています。

An object declaration inside a class can be marked with the companion keyword:

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

Members of the companion object can be called simply by using the class name as the qualifier:

val instance = MyClass.create()

...

うーん、たしかにMyClass.create() のように直接呼び出せるけど、それだけの話なのだろうか?直接呼ぶことがなければCompanion objectとして書かなくても良い?

そんなことを思いつつ、いろいろコードを見たり調べたりしていると、こちらのブログの概要に書かれている説明がヒントになりました。

> There are times we need to use a companion object to define class members that are going to be used independently of any instance of that class. 

この部分「class members that are going to be used independently of any instance of that class」

あ、なるほど。「そのクラスのインスタンスによらずに使うようなもの」と捉えるとかなり腑に落ちてきました。

それではここでRubyで書く場合とKotlinで書く場合を比較してみます。

Rubyで考える

Rubyで以下のようなコードを書きたいとします。

class Japanese
  COUNTRY = '日本'

  attr_accessor :note

  def initialize(name:, age:)
    @name = name
    @age = age
    @note = '特にありません。'
  end

  def self.where_here_is
    puts "ここは#{COUNTRY}です。"
  end

  def greeting
    puts "こんにちは。私の名前は#{name}です。#{age}歳です。一言、#{note}"
  end

  private

  attr_reader :name, :age
end

出力してみましょう。

Japanese.where_here_is

kobayashi = Japanese.new(name: '小林', age: 20)

kobayashi.note = "ゲームが好きです。"
kobayashi.greeting

---

# ここは日本です。
# こんにちは。私の名前は小林です。20歳です。一言、ゲームが好きです。

Kotlinで書くと?

上記のコードをKotlinで置き換えると以下のように書けます。
ただし、定数へのアクセス可能な範囲など全てが完全に等価という意味ではありませんのでご注意ください。

class Japanese(private val name: String, private val age: Int) {
    var note: String = "特にありません。"

    fun greeting() {
        println("こんにちは。私の名前は${name}です。${age}歳です。一言、${note}")
    }

    companion object {
        private const val COUNTRY = "日本"

        fun whereHereIs() {
            println("ここは${COUNTRY}です。")
        }
    }
}

はい、Companion objectが出てきました。実行してみます。

fun main() {
    Japanese.whereHereIs()

    val kobayashi = Japanese("小林", 20)

    kobayashi.note = "ゲームが好きです。"
    kobayashi.greeting()
}

main()

---

// ここは日本です。
// こんにちは。私の名前は小林です。20歳です。一言、ゲームが好きです。

もう少し深堀りする

実は公式リファレンスの「Companion objects」は「Object declarations」のセクションの中にあります。

「Object declarations」ではキーワード「object」について説明が書かれており、「object」キーワードをつけることで、Singletonオブジェクトを作成できることを説明しています。

// 公式リファレンスより抜粋
object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

Singletonオブジェクトはその名の通り唯一のオブジェクトを示します。Kotlinでは「object」キーワードをつけるとSingletonオブジェクトを宣言できます。

Companion objectも、キーワード「object」が宣言されているためSingletonオブジェクトの一種です。Companion objectの場合は「宣言されたクラス内に属する唯一のオブジェクト」ということになります。

「クラス内に属する唯一のオブジェクト」と考えると、Rubyでいうところのクラス内で宣言する定数やクラスメソッドなどは、KotlinではCompanion objectの中で定義するのが適切だと理解できます(次項の「注釈」もご一読ください)。

Rubyで開発しているときはSingletonを利用する機会がほとんどありませんでした(どちらかというと「Singleton取扱注意!」という認識だった)。この背景もあったからか、なかなか理解に苦しんだ気がします。

余談

ここからは余談、かつ、Kotlinへの(今のところの)私見になります。

Kotlinが静的型付け言語であることからDI(Dependency Injection)フレームワークを利用することがほとんどだと思います。

そうすると何かのクラスからインスタンスを自ら作成する機会があまりなく、インスタンスとして定義すべき内容かそれともクラスに定義すべき内容かに意識が向きづらいなと感じています。

なので、上で示した例だと以下のように書いてしまうことがちょこちょこあり、その度に「なぜCompanion objectに書く必要があるのか・・・?」と混乱していました。

class Japanese(private val name: String, private val age: Int) {
    // private val として定義しようとしてしまう。
    private val country = "日本"

    var note: String = "特にありません。"

    /**
      何でもかんでもインスタンスメソッドとして定義しようとしてしまう。
      (「今回の例はインスタンスメソッドが適切なのでは?」というのはスルーしてください)
    */
    fun whereHereIs() {
        println("ここは${country}です。")
    }

    fun greeting() {
        println("こんにちは。私の名前は${name}です。${age}歳です。一言、${note}")
    }
}

ロジックをインスタンスが持つべきかそれともクラスが持つべきかを考えて、クラスとして持つべき内容であればCompanion objectに定義しましょう(下の注釈もご一読ください)。

注釈:
今回の記事を社内レビューしていただいた過程で、「Companion objectにはFactoryパターン用のメソッドを置くぐらいが大半でそれ以外の関数を置くことはあまりなく、トップレベルで関数を定義することが推奨されていることが多い」とのコメントをいただきました。 定数の定義についても諸説あるようで、Javaとの互換性のためにCompanion objectに定数を定義することがある一方、Kotlinだけの実装であればトップレベルでの定数定義のほうがメタスペース(JVMにおける、クラスやメソッドなど永続的に参照される静的オブジェクトを管理する領域)の節約になるとのことです。

うーん、わかったつもりがわからないことが増えてしまいました。Rubyでの書き方をKotlinで書こうとしてしまっていることが問題なのかもしれません。 この点については未来の私のさらなる執筆に期待したいと思います。

おわりに

今回はCompanion objectについて学んだことを書きました。Rubyエンジニアが戸惑う(かもしれない)ポイントを今後も残していけたらと思います。


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

求人情報はこちら。

hrmos.co

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

fukuoka.moneyforward.com