LoginSignup
0
0

More than 1 year has passed since last update.

[Kotlin] 「偽コンストラクタ」の作り方 その1

Last updated at Posted at 2021-09-22

Android Dagashi #189で紹介されていた "Underused Kotlin features" (使われなさすぎなKotlinの機能)という記事に書かれていた "Pseudoconstructors" がなかなか面白かったので、筆者なりの視点で記事にしてみました。

本稿を書いていて感じたのが、これはKotlinからプログラミングを始めた人にとっては特に目につくテクニックではないのかもしれない、ということ。現状はJavaからKotlinに移った人がまだまだ主流でしょう(特にAndroidプログラマー)から、そういう方々に、こんなコーディングのやり方もあるんだ、と思っていただくことを念頭に置いています。

「偽コンストラクタ」、すなわちコンストラクタに見える別物です。そんなめんどくさそうな「使われなさすぎ」機能を使うべきなのか、今後Kotlinのバージョンが上がったら仕様は変わるか、という個人的メモを兼ねて、Kotlinの起源であるJavaで確立されてきたデザインパターンを念頭に置いて、紹介する技術記事の少ない(日本語は特に)ニッチなテーマで書いてみます。今回のコードを検証したバージョンは Kotlin 1.5.31 です。

トップレベル関数と "new" の廃止

Javaでは、関数(Javaの用語ではメソッド)はクラスメソッドかインスタンスメソッドのいすれかでした。すなわち、メソッドはstaticメソッドとしてクラスに属するか、インスタンスに属して主にインスタンスを主語とした処理を行うものでした。これがKotlinになってトップレベル関数 (Top-level function) 、つまり何にも属していない、パッケージ名と名前だけでアクセスできる関数が定義できるようになりました。

Kotlin組み込みのトップレベル関数、たとえば print は以下のように、クラス名の記述もインスタンス作成もなしで呼び出せます。

Sample.kt
class Sample {
    fun printMessage() {
        print("Kotlin Top-level function")
    }
}

print は以下のように定義されています。

Console.kt
/** Prints the given [message] to the standard output stream. */
@kotlin.internal.InlineOnly
public actual inline fun print(message: Any?) {
    System.out.print(message)
}

トップレベル関数のほか、Kotlinにはコードの文字数を削減する仕様が多く盛り込まれました。行末のセミコロンの省略などのほか、コンストラクタを使うときの new も不要になりました。Javaでは以下のように書くのが当たり前でしたが、

Sample.java
public class Sample {
    public double multiply(double digit) {
        val multiplier = new Multiplier(3);
        return multiplier.calculate(digit);
    }
}

Kotlinなら、以下のように書けます。

Sample.kt
class Sample {
    fun multiply(digit: Double): Double {
        val multiplier = Multiplier(3)
        return multiplier.calculate(digit)
    }
}

Javaでは、以下のように書かれていたものが、

System.out.print(message);
new Multiplier(3);

Kotlinになって以下のようになり、

print("Kotlin Top-level function")
Multiplier(3)

文法上は両者の見分けがつかなくなりました。 print(...)print クラスのコンストラクタかもしれませんし、 Multiplier(...) はコンストラクタ風のトップレベル関数かもしれません。このような関数で、既存のクラス名(あるいはインターフェイス名等)と同名でありながらコンストラクタでない関数が、偽コンストラクタ (Pseudo constructor) です。

Javaでは、このような同一化が起こらないような保守的な文法を採用していました1。Kotlinはむしろこのような「擬態」を積極的に認めてコーディング生産性の向上につなげようとしているように見えます。では、偽コンストラクタが目指すものは何でしょうか?

偽コンストラクタの実装法 (1)

以下のコードでは、 Multiplier の本物のコンストラクタと偽コンストラクタの両方が定義されています。

Multiplier.kt
class Multiplier(private val value: Double) {
    init {
        println("Multiplier($value) constructed")
    }

    fun calculate(multiplied: Double) = multiplied * value
}

fun Multiplier(value: Int): Multiplier {
    println("top-level Multiplier($value) called")
    return Multiplier(value = value.toDouble())
}

以下のように、引数を Double 型にすると Multiplier クラスのプライマリコンストラクタが直接呼び出されます。他方、引数を Int 型にすると直接呼び出されるのはトップレベル関数の Multiplier 関数になり、プライマリコンストラクタはトップレベル関数から間接的に呼び出されます。実装法といっても、ただ単にトップレベル関数の名前をクラス名と同じにするだけです。

val a = Multiplier(5.0)
// => Multiplier(5.0) constructed

val b = Multiplier(5)
// => top-level Multiplier(5) called
// => Multiplier(5.0) constructed

もっとも上記は説明用のコードで、現実にこのように書くKotlinプログラマーはいないでしょう。単純に多様なコンストラクタが欲しいなら、以下のように偽でなく本物のセカンダリコンストラクタを定義すれば充分です。

class Multiplier(private val value: Double) {
    constructor(value: Int) : this(value.toDouble()) {
        println("Secondary constructor called")
    }

    init {
        println("Multiplier($value) constructed")
    }

    fun calculate(multiplied: Double) = multiplied * value
}

このプライマリ・セカンダリコンストラクタもKotlinが始めた仕様で、Javaのデフォルトコンストラクタの概念を発展的に置き換えています2。では、偽コンストラクタをどこで使うか?

偽コンストラクタの使いどころ

Androidなどのスマートフォンアプリでは、カメラやGPS、重力センサーなどの端末のデバイスを利用する部分に特に妙味があります。アプリは内部で多数のインスタンスを生成しますが、デバイスの個数は限られているので、デバイスアクセス用のインスタンスは「シングルトン」で実装するのが常道です3。コンストラクタは常に新しいインスタンスを生成してしまいシングルトンになりませんので、代わりに偽コンストラクタを使って実装してみたのが以下のコードです。

DeviceManager.kt
fun DeviceManager(context: Context): DeviceManager = DeviceManager.getInstance(context)
fun DeviceManager(): DeviceManager = DeviceManager.getInstance()

interface DeviceManager {

    fun manage()

    companion object {
        private var instance: DeviceManagerImpl? = null

        internal fun getInstance(context: Context): DeviceManager = instance
            ?: synchronized(this) {
                instance ?: run {
                    val deviceManager = DeviceManagerImpl(context)
                    instance = deviceManager
                    deviceManager
                }
            }

        internal fun getInstance(): DeviceManager = instance!!
    }
}

private class DeviceManagerImpl(context: Context) : DeviceManager {
    // private val adapter = DeviceAdapter.getAdapter(context)

    override fun manage() {
        // adapter.access(...)
    }
}

DeviceManager(context: Context) は、最初に呼び出されたときは private なコンストラクタを使って Context インスタンスを足がかりにデバイスにアクセスするインスタンスを生成します。 DeviceManager(context: Context) が2度目以降に呼び出されたときは、すでに生成されたインスタンスを返します。また、引数のない DeviceManager() は、 DeviceManager(context: Context) が1度以上確実に呼び出されている状況で使えます。

なお、ここでは実装の都合上、 DeviceManager はインターフェイスになっています。このように、偽コンストラクタのテクニックはインターフェイスのコンストラクタや直接呼び出せないはずの抽象クラスのコンストラクタも偽装できます。

ところで、「Singletonパターン」はKotlinが登場するはるか以前からあるパターンであり、当然Javaでも実装できます。上のコードで示した偽コンストラクタの実装は、昔ながらの要領で実装した DeviceManager.getInstance(context: Context) の結果を返しているだけです。では、この関数をわざわざ偽コンストラクタで実装するべきでしょうか?

コンストラクタは記述が簡単・覚えやすい

Javaなどのオブジェクト指向言語でよく使われるデザインパターンのうち、インスタンスの取得に関するパターンには、SingletonパターンやFactoryパターンがあります。両方とも間接的にコンストラクタを呼び出すパターンです4。Factoryパターンはときに複雑になるインスタンス生成過程をコンストラクタとは別のインスタンスメソッドなどに分離するもので、コンストラクタ記述を最小限にしてクラスの複雑化/肥大化を防ぐなどの効能があります。上のコードで DeviceManagergetInstance(context: Context), getInstance() もFactory関数です5。たとえばJava SEでは、 java.util.Calendar, java.security.MessageDigest などのクラスでコンストラクタを protected にして直接呼び出せないようにし、代わりにクラスメソッドの getInstance(...) を使ってインスタンスを生成するパターンが昔から使われています。

しかし、これらのクラスを利用するプログラマーには、なぜコンストラクタが使えずFactory関数(Factoryメソッド)を使わないとインスタンスが作れないのか、よくわかりません。もちろん java.util.Calendarjava.security.MessageDigest にはFactoryメソッドを用意している理由がそれぞれにあるのですが、それはクラス設計や実装の都合であって、利用する側のプログラマーにとっては重要なことではないでしょう。便利なクラスがあると聞いてIDE6でクラス名をタイプしてみたがコンストラクタ引数の補完がなぜか利かない、実はコンストラクタが使えないFactoryパターンだった、と知る/気づくまでにプログラマーは貴重な時間を浪費してしまうかもしれません。 getInstance(...) を使うタイプのFactoryパターンに対してあえて酷な表現を使うなら、クラス側の都合を利用者が知らなければならないという点で「関心事の分離」に失敗しているアンチパターンなのでは?

しかし、もしファクトリ関数がすべてコンストラクタ(偽物を含む)で記述できるなら、プログラマーはすんなりとインスタンスを生成するコードを書けそうです。覚えることが少なくて済む7、というだけでなく、以下のように書けることはとても簡潔で読みやすく、機能を想像しやすいインスタンスが生成されそうです。

val deviceManager = DeviceManager(context)

// ...

val deviceManager = DeviceManager()

型推論のおかげもあって、コードのタイプ数がとても少なくなります。タイプ数を少なくする手立てを多数用意したKotlinの言語設計8は、この簡潔さでプログラマーのやる気を増進するねらいもありそうです。

以上、シングルトンオブジェクトを偽コンストラクタで使える例を示しました。このほか、純粋なシングルトンでなくオブジェクトの一部の実装だけを共有したい場合など、いろいろなバリエーションが考えられ、そのバリエーションの実装にも偽コンストラクタは使えそうです。

ただし、仮に getInstance(...) を使うタイプのFactoryパターンが軒並み偽コンストラクタで書かれるようになっても、KotlinのコードからFactory関数が消滅することはないでしょう。たとえば、他のクラスのメンバ関数からインスタンスを取得する使い方が通常であるクラスには、その偽コンストラクタは必要なさそうです。

偽コンストラクタの注意事項

本物のコンストラクタでは、例外が発生する場合を除いて、当該クラス(サブクラスでなく)のインスタンスを必ず取得できますが、偽コンストラクタではそもそも戻り値が得られる保証もありません。偽コンストラクタを実装するプログラマーは、少なくとも正常終了時には関数名と同名のクラスのインスタンス、または同名クラス(あるいはインターフェイス)を継承したクラスのインスタンスを必ず返すように実装すべきでしょう。上のコードの DeviceManager(context) / DeviceManager() は実装クラスの DeviceManagerImpl を返しますが、戻り値の型にはインターフェイスの DeviceManager を指定しています。

上記の deviceManager について、IDEで "Specify type explicitly" を実行すると、

val deviceManager: DeviceManager = DeviceManager(context)

// ...

val deviceManager: DeviceManager = DeviceManager()

と変数の型名が補完されるはずです。通常は変数の型推論を利用して簡潔なコードを書けばよいと思いますが、このようにいつでも(本物/偽)コンストラクタの戻り値を確認できることはプログラマーの安心につながると思います。

また、コーディングルールについても考慮しておくのがよいでしょう。通常のKotlinのコーディングルールはおおむねJavaの流儀を継承したルールになっています。なので型名は大文字で始まる名前になっており、それと同名のコンストラクタも大文字で始まる名前になりますが、ここでの偽コンストラクタはコンストラクタではなくトップレベル関数なので、通常のルールなら小文字で始まるはずです。関数名であっても偽コンストラクタの場合には大文字で始まってもよい、ただし偽コンストラクタを担当するプログラマーは同名の型のインスタンスが得られることを保証する責務を負う、ということを確認しておくのが望ましいと思います。

トップレベル関数型偽コンストラクタの罠

コンストラクタと同じ型の関数、という小ネタにもかかわらず長大な記事になってしまいましたが、まだ続きがあります。トップレベル関数を偽コンストラクタとして記述したとき、ちょっとしたことでコンパイルエラーになることがあります。それを回避するテクニックを、次の記事: [Kotlin] 「偽コンストラクタ」の作り方 その2 に書きました。

関連記事

参考文献


  1. staticインポートがサポートされた後は、 import static ... 宣言でクラスメソッド指定すれば以後コード中でクラス名なしでクラスメソッドを書けるようになっていますが、コンストラクタのnewは不可欠です。 

  2. 複数のコンストラクタ定義すること自体はJavaでも初期から可能です。 

  3. 必須というわけではありませんが。 

  4. そうでないものもあるのかもしれませんが筆者は存じません。 

  5. Singletonパターンの実装をFactoryパターンで関数定義しています。 

  6. IDE: 統合開発環境 (Integrated Development Environment)。高性能になった最近のIDEなら、Factory関数の補完ができたり、それを可能にするプラグインがあったりするかもしれません。 

  7. コンストラクタとFactory関数をクラスに応じて使い分けなければならない、という問題を解決するだけなら、すべてのクラスでコンストラクタを使わせずFactory関数を用意させる、という手も考えられますが、さすがにそんな面倒な発想はJavaにもありませんでした。 

  8. 言語仕様の複雑さと引き換え、ではありますが。 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0