Kotlin でクラスとオブジェクトを使用する

1. 始める前に

この Codelab では、Kotlin でクラスとオブジェクトを使用する方法について説明します。

クラスは、オブジェクトを作成する際の土台にする設計図を提供します。オブジェクトは、そのオブジェクト用のデータで構成されるクラスのインスタンスです。オブジェクトとクラス インスタンスは同じ意味です。

家を建てることを考えてみましょう。クラスは建築家が描いた設計プランに似ています。これは設計図とも呼ばれます。設計図は家ではありません。家を建てる方法を示した説明書です。家は実在物であり、設計図に従って作成されたオブジェクトであると言えます。

家の設計図に複数の部屋があり、各部屋に独自の設計と用途があるように、各クラスには独自の設計と用途があります。クラスの設計方法を理解するには、データ、ロジック、動作をオブジェクトで包み込む方法を指南する、オブジェクト指向プログラミング(OOP)というフレームワークについて理解する必要があります。

OOP を使用すると、実世界の複雑な問題を小さなオブジェクトにまとめて単純化することができます。OOP には 4 つの基本コンセプトがあります。それぞれのコンセプトの詳細については、この Codelab で後ほど説明します。

  • カプセル化。関連するプロパティと、それらのプロパティにアクションを実行するメソッドをクラスで包みます。スマートフォンについて考えてみましょう。カメラ、ディスプレイ、メモリカードなどのハードウェア部品やソフトウェア部品がカプセル化されています。部品が内部でどのように接続されているかを気にする必要はありません。
  • 抽象化。カプセル化の延長で、内部の実装ロジックを可能な限り隠すという考え方です。たとえば、スマートフォンで写真を撮るには、カメラアプリを開き、撮影するシーンにスマートフォンを向けて、ボタンをクリックする必要があります。カメラアプリの作成方法や、スマートフォンのカメラ ハードウェアの仕組みを知る必要はありません。つまり、カメラアプリの内部の仕組みやモバイルカメラが写真を撮影する方法が抽象化され、重要なタスクを実行できるようになっています。
  • 継承。親子関係を作ることで、他のクラスの特性と動作のうえにクラスを作成できるようにします。たとえば、各メーカーは Android OS を搭載したさまざまなモバイル デバイスを製造していますが、デバイスごとに UI は異なります。つまり、メーカーは Android OS の機能を継承し、そのうえにカスタマイズを行っていると言えます。
  • ポリモーフィズム。この単語は、ギリシャ語の「ポリ」(多数)と「モーフィズム」(形態)を合わせたものです。ポリモーフィズムとは、異なるオブジェクトを単一かつ共通の方法で使用することを指します。たとえば、Bluetooth スピーカーをスマートフォンに接続する場合、スマートフォンが知る必要があるのは Bluetooth で音声を再生できるデバイスがあることだけです。さまざまな Bluetooth スピーカーを使用できますが、スマートフォンが各スピーカーの使い方を個別に知っている必要はありません。

最後に、プロパティ委譲についても学習します。これを使用すると、プロパティ値を操作する再利用可能なコードを簡潔な構文で記述できます。この Codelab では、スマートホーム アプリのクラス構造を構築することを題材にして、こうしたコンセプトについて学習します。

前提条件

  • Kotlin のプレイグラウンドでコードを開き、編集し、実行できる。
  • Kotlin プログラミングの基礎知識(変数と関数を含む)、および println() 関数と main() 関数の知識

学習内容

  • OOP の概要
  • クラスとは何か
  • コンストラクタ、関数、プロパティを備えたクラスを定義する方法
  • オブジェクトをインスタンス化する方法
  • 継承とは何か
  • IS-A 関係と HAS-A 関係の違い
  • プロパティと関数をオーバーライドする方法
  • 可視性修飾子とは
  • 委譲とは何か、by 委譲の使用方法

作成するコードの概要

  • スマートホームのクラス構造
  • スマートテレビやスマートライトなどのスマート デバイスを表すクラス

必要なもの

  • ウェブブラウザがインストールされた、インターネットに接続できるパソコン

2. クラスを定義する

クラスを定義するときには、そのクラスのすべてのオブジェクトに必要なプロパティとメソッドを指定します。

クラス定義は class キーワードで始まり、その後に名前、中括弧の組が続きます。構文の左中括弧の前の部分は、クラスヘッダーとも呼ばれます。中括弧の内側では、クラスのプロパティと関数を指定できます。プロパティと関数については後ほど説明します。クラス定義の構文を次の図に示します。

class キーワードで始まり、名前、左中括弧と右中括弧の組と続きます。中括弧には、設計図を記述したクラスの本体が入ります。

クラスには、以下のような命名規則が推奨されています。

  • クラス名は任意に選択できますが、Kotlin のキーワードは使用しません(例: fun キーワード)。
  • クラス名は PascalCase(各単語が大文字で始まり、単語間にスペースがない)で記述します。たとえば、SmartDevice では、各単語の先頭を大文字にし、単語間にはスペースを入れません。

クラスには、主に 3 つの構成要素があります。

  • プロパティ。クラスのオブジェクトの属性を指定する変数。
  • メソッド。クラスの動作とアクションを含んでいる関数。
  • コンストラクタ。クラスを定義しているプログラムの中でそのクラスのインスタンスを作成する特別なメンバー関数。

クラスを扱う課題は今回が初めてではありません。これまでの Codelab で、IntFloatStringDouble などのデータ型について学習しました。Kotlin では、これらのデータ型がクラスとして定義されています。次のコード スニペットに示すように変数を定義すると、1 の値でインスタンス化された Int クラスのオブジェクトが作成されます。

val number: Int = 1

SmartDevice クラスを定義しましょう。

  1. Kotlin のプレイグラウンドで、内容を空の main() 関数に置き換えます。
fun main() {
}
  1. main() 関数の前の行で、本体に // empty body というコメントが入った SmartDevice クラスを定義します。
class SmartDevice {
    // empty body
}

fun main() {
}

3. クラスのインスタンスを作成する

すでに学習したとおり、クラスはオブジェクトの設計図です。Kotlin ランタイムは、クラス(設計図)を使用して、その型のオブジェクトを作成します。この SmartDevice クラスを、スマート デバイスの設計図として使用できます。プログラムで実際のスマート デバイスを使うには、SmartDevice のオブジェクト インスタンスを作成する必要があります。インスタンス化構文は、次の図に示すように、クラス名で始まり、その後に括弧の組が続きます。

1d25bc4f71c31fc9.png

オブジェクトを使用するには、変数の定義と同様に、オブジェクトを作成して変数に代入します。不変変数は val キーワードを使用して作成し、可変変数は var キーワードを使用して作成します。val キーワードまたは var キーワードの後に、変数名、= 代入演算子、クラス オブジェクトのインスタンス化と続きます。この構文を図にすると次のようになります。

f58430542f2081a9.png

SmartDevice クラスをオブジェクトとしてインスタンス化しましょう。

  • main() 関数で、val キーワードを使用して smartTvDevice という名前の変数を作成し、SmartDevice クラスのインスタンスとして初期化します。
fun main() {
    val smartTvDevice = SmartDevice()
}

4. クラスメソッドを定義する

ユニット 1 では、以下のことを学びました。

  • 関数の定義には、fun キーワードの後に、括弧の組と中括弧の組を続けたものを使用します。中括弧の中には、タスクを実行するために必要な手順であるコードが含まれています。
  • 関数を呼び出すと、その関数に含まれるコードが実行されます。

クラスが実行できるアクションは、クラスの関数として定義されます。たとえば、スマート デバイス、スマートテレビ、スマートライトを所有していて、スマートフォンでオンとオフを切り替えられるとします。スマート デバイスは、プログラミングでは SmartDevice クラスに置き換えられ、オンとオフを切り替えるアクションは、オン / オフ動作を実現する turnOn() 関数と turnOff() 関数で表されます。

クラスで関数を定義する構文は、前に学習したものと同じです。唯一の違いは、関数がクラス本体にあることです。クラス本体で定義された関数は、メンバー関数またはメソッドと呼ばれ、クラスの動作を表します。この Codelab の残りの部分では、クラスの本体にある関数をメソッドと呼びます。

SmartDevice クラスで turnOn() メソッドと turnOff() メソッドを定義しましょう。

  1. SmartDevice クラスの本体で、本体が空の turnOn() メソッドを定義します。
class SmartDevice {
    fun turnOn() {

    }
}
  1. turnOn() メソッドの本体に println() 文を追加し、"Smart device is turned on." という文字列を渡します。
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. turnOn() メソッドの後に、"Smart device is turned off." という文字列を出力する turnOff() メソッドを追加します。
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

オブジェクトのメソッドを呼び出す

ここまで、スマート デバイスの設計図となるクラスを定義し、そのクラスのインスタンスを作成して変数に代入しました。次に、SmartDevice クラスのメソッドを使用して、デバイスの電源をオンにしてからオフにします。

クラス内でのメソッドの呼び出しは、前の Codelab の main() 関数から他の関数を呼び出す方法に似ています。たとえば、turnOn() メソッドから turnOff() メソッドを呼び出す必要がある場合は、次のコード スニペットのように記述します。

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

クラスの外部でクラスメソッドを呼び出すには、クラス オブジェクトの後に、. 演算子、関数名、括弧の組と続けたものを使用します。必要に応じて、メソッドに必要な引数を括弧内に入れます。この構文を図にすると次のようになります。

fc609c15952551ce.png

このオブジェクトの turnOn() メソッドと turnOff() メソッドを呼び出しましょう。

  1. main() 関数の smartTvDevice 変数の後の行で、turnOn() メソッドを呼び出します。
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. turnOn() メソッドの後の行で、turnOff() メソッドを呼び出します。
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. コードを実行します。

次のような出力が表示されます。

Smart device is turned on.
Smart device is turned off.

5. クラスのプロパティを定義する

ユニット 1 では、変数(1 つのデータを納めるコンテナ)について学びました。val キーワードを使用して読み取り専用変数を作成する方法と、var キーワードを使用して可変変数を作成する方法を学びました。

メソッドはクラスが実行できるアクションを定義しますが、プロパティはクラスの特性またはデータ属性を定義します。たとえば、スマート デバイスには次のような特性があります。

  • 名前。デバイスの名前。
  • カテゴリ。スマート デバイスのタイプ(エンターテイメント、設備、調理など)。
  • デバイスのステータス。デバイスがオンかオフか、オンラインかオフラインか。デバイスは、インターネットに接続されているときにオンラインとみなされ、そうでないときにはオフラインとみなされます。

プロパティは基本的に、関数本体ではなくクラス本体で定義される変数だと言えます。定義する構文は変数と同じだということです。不変プロパティは val キーワードで定義し、可変プロパティは var キーワードで定義します。

上で述べた特性を SmartDevice クラスのプロパティとして実装しましょう。

  1. turnOn() メソッドの前の行で、name プロパティを定義して "Android TV" という文字列を代入します。
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. name プロパティの後の行で、category プロパティを定義して "Entertainment" という文字列を代入し、deviceStatus プロパティを定義して "online" という文字列を代入します。
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. smartTvDevice 変数の後の行で、println() 関数を呼び出して、"Device name is: ${smartTvDevice.name}" という文字列を渡します。
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. コードを実行します。

次のような出力が表示されます。

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

プロパティのゲッター関数とセッター関数

プロパティでは、変数よりも多くの処理を行えます。たとえば、スマートテレビを表すクラス構造を作成するとします。一般的な操作の一つに、音量の上げ下げがあります。このアクションをプログラミングで表現するために、speakerVolume という名前のプロパティを作成します。このプロパティには、テレビのスピーカーに設定されている現在の音量レベルが保持されていますが、音量の値には設定可能な範囲があります。設定できる音量の最小値は 0 で、最大値は 100 です。speakerVolume プロパティが 100 を超えたり、0 を下回ったりしないように、セッター関数を作成します。プロパティの値を更新するときに、値が 0 から 100 の範囲内にあるかどうかを確認する必要があります。別の例として、名前を常に大文字で記述する必要がある場合を考えます。ゲッター関数を実装すれば、そこで name プロパティを大文字に変換できます。

これらのプロパティを実装する方法を詳しく確認する前に、宣言するための完全な構文を理解しておく必要があります。可変プロパティを定義する完全な構文は、変数定義から始まり、その後に省略可能な get() 関数と set() 関数が続きます。この構文を図にすると次のようになります。

f2cf50a63485599f.png

プロパティにゲッター関数とセッター関数を定義しない場合は、Kotlin コンパイラが内部的に作成します。たとえば、var キーワードを使用して speakerVolume プロパティを定義し、2 という値を代入すると、コンパイラは次のコード スニペットのようにゲッター関数とセッター関数を自動生成します。

var speakerVolume = 2
    get() = field  
    set(value) {
        field = value    
    }

これらの行は、コンパイラがバックグラウンドで追加するものであり、コードには現れません。

不変プロパティの完全な構文は、次の 2 つの点が異なります。

  • val キーワードで始まります。
  • val 型の変数は読み取り専用であるため、set() 関数がありません。

Kotlin プロパティは、メモリに値を保持するためにバッキング フィールドを使用します。バッキング フィールドは、基本的には、プロパティで内部的に定義されているクラス変数です。バッキング フィールドのスコープはプロパティです。つまり、get() プロパティ関数や set() プロパティ関数からのみアクセスできます。

get() 関数内でのプロパティ値の読み取りと、set() 関数内での値の更新には、プロパティのバッキング フィールドを使用する必要があります。これは Kotlin コンパイラにより自動生成され、field 識別子で参照されます。

たとえば、set() 関数内でプロパティの値を更新する場合は、次のコード スニペットのように、value パラメータとして参照される set() 関数のパラメータを使用し、それを field 変数に代入します。

var speakerVolume = 2
    set(value) {
        field = value    
    }

たとえば、speakerVolume プロパティに割り当てる値を 0 から 100 の範囲にするには、次のコード スニペットに示すようなセッター関数を実装します。

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

set() 関数は、in キーワードと値の範囲を使用して、Int 値が 0 から 100 の範囲内にあるかどうかをチェックします。値が範囲内の場合、field の値が更新されます。それ以外の場合、プロパティの値は変更されません。

このプロパティは、この Codelab の「クラス間の関係を実装する」のクラスに含まれているため、ここでセッター関数をコードに追加する必要はありません。

6. コンストラクタを定義する

コンストラクタの主な目的は、クラスのオブジェクトを作成する方法を定めることです。別の言い方をすると、コンストラクタがオブジェクトを初期化することで、そのオブジェクトが使用可能になるということです。この操作は、オブジェクトをインスタンス化するときに行いました。クラスのオブジェクトがインスタンス化されるときに、コンストラクタ内のコードが実行されます。コンストラクタは、パラメータの有無にかかわらず定義できます。

デフォルト コンストラクタ

デフォルト コンストラクタは、パラメータのないコンストラクタです。デフォルト コンストラクタは、次のコード スニペットに示すように定義します。

class SmartDevice constructor() {
    ...
}

Kotlin では簡潔な表現を目指しており、コンストラクタにアノテーションや可視性修飾子がない場合は、後で学習するように constructor キーワードを省くことができます。次のコード スニペットに示すように、コンストラクタにパラメータがない場合は、括弧も省くことができます。

class SmartDevice {
    ...
}

Kotlin コンパイラはデフォルト コンストラクタを自動生成します。自動生成されるデフォルト コンストラクタは、コンパイラがバックグラウンドで追加するため、コードには現れません。

パラメータ付きコンストラクタを定義する

SmartDevice クラス内では、name プロパティと category プロパティを変更できません。そのため、SmartDevice クラスのすべてのインスタンスで、必ず name プロパティと category プロパティを初期化する必要があります。現在の実装では、name プロパティと category プロパティの値がハードコードされています。これは、すべてのスマート デバイスが "Android TV" という文字列の名前を持ち、"Entertainment" という文字列のカテゴリに分類されることを意味します。

不変性を維持したまま、値のハードコードを避けるために、パラメータ付きコンストラクタを使用して初期化します。

  • SmartDevice クラスで、デフォルト値を指定せずに name プロパティと category プロパティをコンストラクタに移動します。
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

これで、プロパティを設定するためのパラメータをコンストラクタに渡せるようになったので、このクラスのオブジェクトをインスタンス化する方法も変わります。オブジェクトをインスタンス化するための完全な構文を次の図に示します。

bbe674861ec370b6.png

コードで表すと次のようになります。

SmartDevice("Android TV", "Entertainment")

このコンストラクタの引数はどちらも文字列です。どの値がどのパラメータのものなのか判別しにくくなっています。これを解決するには、関数に引数を渡した方法と同様に、次のコード スニペットに示すように名前付き引数を備えたコンストラクタを作成します。

SmartDevice(name = "Android TV", category = "Entertainment")

Kotlin のコンストラクタには、主に 2 つのタイプがあります。

  • プライマリ コンストラクタ。クラスには、プライマリ コンストラクタを 1 つだけ定義でき、これはクラスヘッダーの中で定義します。プライマリ コンストラクタは、デフォルト コンストラクタかパラメータ付きコンストラクタのいずれかです。プライマリ コンストラクタに本体はありません。つまり、コードがありません。
  • セカンダリ コンストラクタ。クラスには、複数のセカンダリ コンストラクタを定義できます。セカンダリ コンストラクタは、パラメータの有無にかかわらず定義できます。セカンダリ コンストラクタは、クラスを初期化することができ、本体を持たせてそこに初期化ロジックを入れることができます。クラスにプライマリ コンストラクタがある場合、各セカンダリ コンストラクタでプライマリ コンストラクタを初期化する必要があります。

プライマリ コンストラクタを使用すると、クラスヘッダー内でプロパティを初期化できます。コンストラクタに渡される引数は、プロパティに代入されます。プライマリ コンストラクタを定義する構文は、クラス名の後に constructor キーワード、括弧のペアと続きます。括弧内にはプライマリ コンストラクタのパラメータが入ります。パラメータが複数ある場合は、パラメータ定義をカンマで区切ります。プライマリ コンストラクタを定義する完全な構文を次の図に示します。

aa05214860533041.png

セカンダリ コンストラクタは、クラスの本体の中にあり、その構文は以下の 3 つの部分で構成されます。

  • セカンダリ コンストラクタの宣言。セカンダリ コンストラクタの定義は、constructor キーワードで始まり、その後に括弧のペアが続きます。セカンダリ コンストラクタに必要なパラメータがあれば、それを括弧内に入れます。
  • プライマリ コンストラクタの初期化。初期化は、コロンで始まり、その後に this キーワード、括弧のペアと続きます。プライマリ コンストラクタに必要なパラメータがある場合は、それを括弧内に入れます。
  • セカンダリ コンストラクタの本体。セカンダリ コンストラクタの本体は、プライマリ コンストラクタの初期化の後ろに、中括弧で囲んで記述します。

この構文を図にすると次のようになります。

2dc13ef136009e98.png

例として、スマート デバイス プロバイダが開発した API を統合する場合を考えます。この API は、初期デバイス ステータスを示す Int タイプのステータス コードを返します。また、この API は、デバイスがオフラインの場合は 0 の値を返し、オンラインの場合は 1 の値を返します。それ以外の整数値の場合、ステータスは不明とみなされます。次のコード スニペットに示すように、SmartDevice クラスにセカンダリ コンストラクタを作成すると、この statusCode パラメータを文字列表現に変換できます。

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. クラス間で関係を作る

継承を使用すると、別のクラスの特性と動作に基づいてクラスを作成できます。これは、再利用可能なコードを記述し、クラス間で関係を作るための有効で強力なメカニズムです。

たとえば、スマートテレビ、スマートライト、スマート スイッチなど、多くのスマート デバイスが販売されています。プログラミングでスマート デバイスを表すとき、名前、カテゴリ、ステータスなどの共通の特性を各デバイスが共有します。また、オンとオフを切り替えられるといった共通の動作もあります。

ただし、スマート デバイスによってオンとオフの切り替え方は異なります。たとえば、テレビの電源をオンにするには、ディスプレイをオンにしてから、最後に設定されていた音量とチャンネルを設定し直す必要があります。一方、ライトをオンにする場合に必要なのは、明るさの増減だけです。

また、各スマート デバイスには、その他にも実行できる機能やアクションがあります。たとえば、テレビの場合は、音量の調節やチャンネルの変更ができます。ライトの場合は、明るさや色を調整できます。

つまり、すべてのスマート デバイスが異なる機能を持っている一方で、共通の特性もあるということです。こういった共通の特性は、各スマート デバイス クラスにコピーすることもできますが、継承してコードを再利用可能にすることもできます。

継承するには、SmartDevice の親クラスを作成し、上記の共通のプロパティと動作を定義する必要があります。さらに、親クラスのプロパティを継承する SmartTvDevice クラスや SmartLightDevice クラスなどの子クラスを作成します。

これをプログラミング用語では、SmartTvDevice クラスと SmartLightDevice クラスが SmartDevice 親クラスを「拡張」していると表現します。親クラスはスーパークラスとも呼ばれ、子クラスはサブクラスとも呼ばれます。これらの関係を次の図に示します。

クラスの継承関係を表す図。

ただし Kotlin では、すべてのクラスがデフォルトで final です。これはつまり、そのようなクラスは拡張できないということなので、クラス間の関係を定義する必要があります。

SmartDevice スーパークラスとそのサブクラスの関係を定義しましょう。

  1. SmartDevice スーパークラスで、class キーワードの前に open キーワードを追加して拡張可能にします。
open class SmartDevice(val name: String, val category: String) {
    ...
}

open キーワードは、このクラスが拡張可能であることをコンパイラに知らせるもので、これによって他のクラスがこのクラスを拡張できるようになります。

サブクラスを作成する構文は、すでに行ったように、クラスヘッダーの作成から始まります。コンストラクタの右括弧の後に、スペース、コロン、もう一つのスペース、スーパークラス名、括弧のペアと続けます。括弧内には、必要に応じて、スーパークラスのコンストラクタで必要なパラメータを入れます。この構文を図にすると次のようになります。

1ac63b66e6b5c224.png

  1. SmartDevice スーパークラスを拡張する SmartTvDevice サブクラスを作成します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

SmartTvDeviceconstructor 定義では、プロパティが可変か不変かを指定しません。つまり、deviceName パラメータと deviceCategory パラメータは、クラス プロパティではなく、単なる constructor パラメータです。このクラスでは使用できず、スーパークラスのコンストラクタに渡すだけです。

  1. SmartTvDevice サブクラスの本体で、ゲッター関数とセッター関数について学習したときに作成した speakerVolume プロパティを追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 0..200 の範囲を設定するセッター関数を備え、1 の値が代入される channelNumber プロパティを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. ボリュームを上げて、"Speaker volume increased to $speakerVolume." という文字列を出力する increaseSpeakerVolume() メソッドを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    } 
}
  1. チャンネル番号を増やし、"Channel number increased to $channelNumber." という文字列を出力する nextChannel() メソッドを追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
    
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}
  1. SmartTvDevice サブクラスの後に、SmartDevice スーパークラスを拡張する SmartLightDevice サブクラスを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. SmartLightDevice サブクラスの本体で、0..100 の範囲を指定するセッター関数を備え、0 の値が代入される brightnessLevel プロパティを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. ライトの明るさを上げ、"Brightness increased to $brightnessLevel." という文字列を出力する increaseBrightness() メソッドを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

クラス間の関係

継承を使用する場合、2 つのクラス間の関係は「IS-A 関係」と呼ばれます。あるクラスのオブジェクトは、そのクラスを継承しているクラスのインスタンスでもあります。「HAS-A 関係」の場合、あるクラスのオブジェクトが別のクラスのインスタンスとなることなく、そのクラスのインスタンスを所有できます。次の図は、この関係をおおまかに示したものです。

HAS-A と IS-A の関係をおおまかに表した図。

IS-A 関係

SmartDevice スーパークラスと SmartTvDevice サブクラスの間に IS-A 関係があるとは、SmartDevice スーパークラスで可能なすべての操作を、SmartTvDevice サブクラスで行えるということです。この関係は一方向の関係です。つまり、すべてのスマートテレビがスマート デバイスであると言えますが、すべてのスマート デバイスがスマートテレビであるとは言えません。IS-A 関係をコードで表すと次のコード スニペットのようになります。

// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}

コードの再利用を可能にするためだけに継承を使用しないでください。その前に、2 つのクラスが互いに関連しているかどうかを確認してください。関係がある場合は、IS-A 関係の条件を満たしているかどうかを確認してください。そのサブクラスはスーパークラスだと言えるのかどうかを自問してください。たとえば、「Android はオペレーティング システムである」と言うことができます。

HAS-A 関係

HAS-A 関係は、2 つのクラスの関係を特定するもう一つの方法です。たとえば、通常、スマートテレビは家で使用します。このとき、スマートテレビと家の間には、なんらかの関係があります。家にスマート デバイスがある、つまり家がスマート デバイスを「持っている」ということです。2 つのクラス間の HAS-A 関係は、コンポジションとも呼ばれます。

ここまでにスマート デバイスをいくつか作成しています。次は、スマート デバイスが入っている SmartHome クラスを作成します。SmartHome クラスを使ってスマート デバイスを操作できます。

HAS-A 関係を使用して SmartHome クラスを定義しましょう。

  1. SmartLightDevice クラスと main() 関数の間で、SmartHome クラスを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

}

class SmartHome {
}

fun main() { 
    ...
}
  1. SmartHome クラスのコンストラクタで、val キーワードを使用して SmartTvDevice 型の smartTvDevice プロパティを作成します。
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. SmartHome クラスの本体で、smartTvDevice プロパティの turnOn() メソッドを呼び出す turnOnTv() メソッドを定義します。
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. turnOnTv() メソッドの後の行で、smartTvDevice プロパティの turnOff() メソッドを呼び出す turnOffTv() メソッドを定義します。
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. turnOffTv() メソッドの後の行で、smartTvDevice プロパティの increaseSpeakerVolume() メソッドを呼び出す increaseTvVolume() メソッドを定義し、smartTvDevice プロパティの nextChannel() メソッドを呼び出す changeTvChannelToNext() メソッドを定義します。
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. SmartHome クラスのコンストラクタで、smartTvDevice プロパティ パラメータを独立した行に移動し、その後にカンマを追加します。
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

    ...

}
  1. smartTvDevice プロパティの後ろの行で、val キーワードを使用して SmartLightDevice タイプの smartLightDevice プロパティを定義します。
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

}
  1. SmartHome の本体で、smartLightDevice オブジェクトの turnOn() メソッドを呼び出す turnOnLight() メソッドと、smartLightDevice オブジェクトの turnOff() メソッドを呼び出す turnOffLight() メソッドを定義します。
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. turnOffLight() メソッドの後ろの行で、smartLightDevice プロパティの increaseBrightness() メソッドを呼び出す increaseLightBrightness() メソッドを定義します。
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }
}
  1. increaseLightBrightness() メソッドの後ろの行で、turnOffTv() メソッドと turnOffLight() メソッドを呼び出す turnOffAllDevices() メソッドを定義します。
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

スーパークラスのメソッドをサブクラスでオーバーライドする

前述のように、オンとオフを切り替える機能はすべてのスマート デバイスでサポートされていますが、その中の実行方法は異なります。このデバイス固有の動作を指定するには、スーパークラスで定義されている turnOn() メソッドと turnOff() メソッドをオーバーライドする必要があります。オーバーライドするとは、操作を横取りすること、典型的には手動で制御することを意味します。メソッドをオーバーライドすると、サブクラスのメソッドがスーパークラスで定義されたメソッドの実行をさえぎって、独自の実行を行います。

SmartDevice クラスの turnOn() メソッドと turnOff() メソッドをオーバーライドしましょう。

  1. SmartDevice スーパークラスの本体で、各メソッドの fun キーワードの前に open キーワードを追加します。
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. SmartLightDevice クラスの本体で、本体が空の turnOn() メソッドを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
    }
}
  1. turnOn() メソッドの本体で、deviceStatus プロパティを文字列「on」に設定し、2 の値に brightnessLevel プロパティを設定し、println() 文を追加して、"$name turned on. The brightness level is $brightnessLevel." という文字列を渡します。
    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }
  1. SmartLightDevice クラスの本体で、本体が空の turnOff() メソッドを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
    }
}
  1. turnOff() メソッドの本体で、deviceStatus プロパティを文字列「off」に設定し、0 の値に brightnessLevel プロパティを設定し、println() 文を追加して、"Smart Light turned off" という文字列を渡します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}
  1. SmartLightDevice サブクラスで、turnOn() メソッドと turnOff() メソッドの fun キーワードの前に override キーワードを追加します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

override キーワードは、サブクラスで定義されたメソッドに入っているコードを実行するように Kotlin ランタイムに指示するものです。

  1. SmartTvDevice クラスの本体で、本体が空の turnOn() メソッドを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
        
    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
        
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }
    
    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    fun turnOn() {
    }
}
  1. turnOn() メソッドの本体で、deviceStatus プロパティを文字列「on」に設定し、println() 文を追加して、"$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber." という文字列を渡します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }
}
  1. SmartTvDevice クラスの本体で、turnOn() メソッドの後に、空の本体を持つ turnOff() メソッドを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
    }
}
  1. turnOff() メソッドの本体で、deviceStatus プロパティを文字列「off」に設定し、println() 文を追加して、"$name turned off" という文字列を渡します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. SmartTvDevice クラスで、turnOn() メソッドと turnOff() メソッドの fun キーワードの前に override キーワードを追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. main() 関数で、var キーワードを使用して SmartDevice 型の smartDevice 変数を定義します。そこで、"Android TV""Entertainment" という引数を渡して SmartTvDevice オブジェクトをインスタンス化します。
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. smartDevice 変数の後ろの行で、smartDevice オブジェクトの turnOn() メソッドを呼び出します。
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. コードを実行します。

次のような出力が表示されます。

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. turnOn() メソッドの呼び出しの後ろの行で、"Google Light""Utility" という引数を渡して SmartLightDevice クラスをインスタンス化し、smartDevice 変数に再代入してから、smartDevice オブジェクト参照の turnOn() メソッドを呼び出します。
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
    
    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. コードを実行します。

次のような出力が表示されます。

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

これはポリモーフィズムの例になっています。このコードでは、SmartDevice 型の変数の turnOn() メソッドを呼び出しますが、変数の実際の値に応じて turnOn() メソッドの異なる実装を実行することができます。

super キーワードを使用してサブクラスでスーパークラスのコードを再利用する

turnOn() メソッドと turnOff() メソッドをよく見ると、SmartTvDevice サブクラスと SmartLightDevice サブクラスでメソッドが呼び出されるときの deviceStatus 変数を更新する仕組みが似ていることに気が付くでしょう。つまり、コードが重複しているのです。SmartDevice クラスでステータスを更新するときのコードは再利用できます。

サブクラスからスーパークラスのオーバーライドされたメソッドを呼び出すには、super キーワードを使用する必要があります。スーパークラスのメソッドを呼び出す方法は、クラスの外部からメソッドを呼び出す場合と同様です。オブジェクトとメソッドの間で . 演算子を使用する代わりに、super キーワードを使用する必要があります。これは、サブクラスではなくスーパークラスのメソッドを呼び出すように Kotlin コンパイラに指示するものです。

スーパークラスのメソッドを呼び出す構文は、super キーワードの後に . 演算子、関数名、括弧のペアと続きます。必要に応じて、括弧内に引数を入れます。この構文を図にすると次のようになります。

18cc94fefe9851e0.png

SmartDevice スーパークラスのコードを再利用しましょう。

  1. turnOn() メソッドと turnOff() メソッドから println() 文を削除し、SmartTvDevice サブクラスと SmartLightDevice サブクラスにある重複するコードを SmartDevice スーパークラスに移動します。
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}
  1. super キーワードを使用して、SmartTvDevice サブクラスと SmartLightDevice サブクラス内で SmartDevice クラスのメソッドを呼び出します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

サブクラスでスーパークラスのプロパティをオーバーライドする

メソッドと同様に、同じ手順でプロパティもオーバーライドできます。

deviceType プロパティをオーバーライドしましょう。

  1. SmartDevice スーパークラスの deviceStatus プロパティの後ろの行で、open キーワードと val キーワードを使用して deviceType プロパティを定義し、"unknown" という文字列を設定します。
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. SmartTvDevice クラスで、override キーワードと val キーワードを使用して、deviceType プロパティを定義し、"Smart TV" という文字列を設定します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    ...
}
  1. SmartLightDevice クラスで、override キーワードと val キーワードを使用して、deviceType プロパティを定義し、"Smart Light" という文字列を設定します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    ...

}

8. 可視性修飾子

可視性修飾子は、カプセル化を実現するうえで重要な役割を果たします。

  • クラスでは、クラスの外部から不正にアクセスできないようにプロパティやメソッドを隠すことができます。
  • パッケージでは、パッケージの外部から不正にアクセスできないようにクラスやインターフェースを隠すことができます。

Kotlin には、以下の 4 つの可視性修飾子が用意されています。

  • public: デフォルトの可視性修飾子。宣言をどこからでもアクセスできるようにします。クラス外で使用するプロパティとメソッドは public とマークします。
  • private: 宣言を同じクラスまたは同じソースファイルでアクセスできるようにします。

多くの場合、プロパティやメソッドの中には、クラス内でのみ使用し、他のクラスでは使用する必要のないものがあります。こういったプロパティやメソッドを private 可視性修飾子でマークすると、別のクラスが誤ってアクセスすることがなくなります。

  • protected: 宣言をサブクラスでアクセスできるようにします。定義したクラスとそのサブクラスで使用するプロパティとメソッドは、protected 可視性修飾子でマークします。
  • internal: 宣言を同じモジュール内でアクセスできるようにします。internal 修飾子は private と似ていますが、internal プロパティと internal メソッドには、同じモジュール内であればクラスの外部からでもアクセスできます。

クラスを定義すると公開となり、それをインポートするパッケージからアクセスできるようになります。つまり、可視性修飾子を指定しない限り、デフォルトで公開になります。同様に、クラス内でプロパティやメソッドを定義または宣言すると、デフォルトではクラスの外部からクラス オブジェクトを使ってアクセスできます。コードに適切な可視性を定義するために、特に他のクラスがアクセスする必要のないプロパティやメソッドを隠す際に不可欠なものです。

例として、運転手が自動車を操作する仕組みについて考えてみましょう。自動車を構成する部品の詳細や自動車内部の仕組みは、デフォルトで隠されています。自動車は、できるだけ直感的に操作できるように作られています。自動車の操作が航空機のように複雑になることが望ましくないように、他の開発者や将来の自分がクラスのプロパティやメソッドの用途がわからなくなることも望ましいことではありません。

可視性修飾子を使用すると、コードの適切な部分をプロジェクト内の他のクラスから見えるようにして、実装が意図せず使用されないようにできるため、コードが理解しやすくなり、バグが発生しにくくなります。

可視性修飾子は、次の図のように、クラス、メソッド、プロパティを宣言するとき、その宣言の構文の前に置く必要があります。

dcc4f6693bf719a9.png

プロパティに可視性修飾子を指定する

プロパティに可視性修飾子を指定する構文は、privateprotected、または internal の修飾子で始まり、その後にプロパティを定義する構文が続きます。この構文を図にすると次のようになります。

47807a890d237744.png

たとえば、次のコード スニペットで、deviceStatus プロパティを非公開にする方法を示しています。

open class SmartDevice(val name: String, val category: String) {

    ...

    private var deviceStatus = "online"

    ...
}

セッター関数に可視性修飾子を設定することもできます。修飾子は set キーワードの前に置きます。この構文を図にすると次のようになります。

cea29a49b7b26786.png

SmartDevice クラスの場合、deviceStatus プロパティの値は、クラス オブジェクトを使ってクラスの外部で読み取ることができる必要があります。しかし、値の更新や書き込みができるのは、そのクラスとその子だけにする必要があります。この要件を実現するには、deviceStatus プロパティの set() 関数に protected 修飾子を使用する必要があります。

deviceStatus プロパティの set() 関数に protected 修飾子を使用しましょう。

  1. SmartDevice スーパークラスの deviceStatus プロパティで、protected 修飾子を set() 関数に追加します。
open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set(value) {
           field = value
       }

    ...
}

set() 関数ではアクションやチェックを実行していません。単に value パラメータを field 変数に代入しているだけです。すでに学習したとおり、これはプロパティ セッターのデフォルト実装に似ています。この場合、set() 関数の括弧と本体を省略できます。

open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set

    ...
}
  1. SmartHome クラスで、private のセッター関数を使用して 0 の値に設定された deviceTurnOnCount プロパティを定義します。
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. turnOnTv() メソッドと turnOnLight() メソッドに、deviceTurnOnCount プロパティとそれに続けて ++ 算術演算子を追加します。さらに、turnOffTv() メソッドと turnOffLight() メソッドに deviceTurnOnCount プロパティとそれに続けて -- 算術演算子を追加します。
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }
    
    ...

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    ...

}

メソッドの可視性修飾子

メソッドに可視性修飾子を指定する構文は、privateprotected、または internal の修飾子で始まり、その後にメソッドを定義する構文が続きます。この構文を図にすると次のようになります。

e0a60ddc26b841de.png

例として、SmartTvDevice クラスで、nextChannel() メソッドに protected 修飾子を指定する方法を、次のコード スニペットに示します。

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    protected fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }      

    ...
}

コンストラクタの可視性修飾子

コンストラクタの可視性修飾子を指定する構文は、いくつかの点を除き、プライマリ コンストラクタの定義に似ています。

  • 修飾子は、クラス名の後、constructor キーワードの前に指定します。
  • プライマリ コンストラクタに修飾子を指定する必要がある場合は、パラメータがない場合でも constructor キーワードと括弧を残す必要があります。

この構文を図にすると次のようになります。

6832575eba67f059.png

例として、protected 修飾子を SmartDevice コンストラクタに追加する方法を、次のコード スニペットに示します。

open class SmartDevice protected constructor (val name: String, val category: String) {

    ...

}

クラスの可視性修飾子

クラスに可視性修飾子を指定する構文は、privateprotected、または internal の修飾子で始まり、その後にクラスを定義する構文が続きます。この構文を図にすると次のようになります。

3ab4aa1c94a24a69.png

例として、SmartDevice クラスに internal 修飾子を指定する方法を、次のコード スニペットに示します。

internal open class SmartDevice(val name: String, val category: String) {

    ...

}

理想的には、プロパティとメソッドに指定する可視性を厳格なものにするよう努力すべきです。そのために、可能な限り private 修飾子を使って宣言してください。private にできない場合は、protected 修飾子を使用してください。protected にできない場合は、internal 修飾子を使用してください。internal にできない場合は、public 修飾子を使用してください。

適切な可視性修飾子を指定する

次の表を参考にすれば、クラスやコンストラクタのプロパティやメソッドにアクセスできる場所に応じて、適切な可視性修飾子を決めることができます。

修飾子

同じクラスでアクセス可能

サブクラスでアクセス可能

同じモジュールでアクセス可能

モジュール外からアクセス可能

private

𝗫

𝗫

𝗫

protected

𝗫

𝗫

internal

𝗫

public

SmartTvDevice サブクラスでは、speakerVolume プロパティと channelNumber プロパティをクラスの外部から制御可能にすべきではありません。これらのプロパティは、increaseSpeakerVolume() メソッドと nextChannel() メソッドのみで制御すべきです。

同様に、SmartLightDevice サブクラスでは、brightnessLevel プロパティは increaseLightBrightness() メソッドのみで制御すべきです。

SmartTvDevice サブクラスと SmartLightDevice サブクラスに適切な可視性修飾子を追加しましょう。

  1. SmartTvDevice クラスで、private 可視性修飾子を speakerVolume プロパティと channelNumber プロパティに追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    private var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    private var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    ...
}
  1. SmartLightDevice クラスで、brightnessLevel プロパティに private 修飾子を追加します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    private var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    ...
}

9. プロパティ委譲を定義する

前のセクションで説明したように、Kotlin のプロパティはバッキング フィールドを使用して、その値をメモリに保持します。これは、field 識別子を使用して参照します。

ここまでのコードを見ると、SmartTvDevice クラスと SmartLightDevice クラスの speakerVolume プロパティ、channelNumber プロパティ、brightnessLevel プロパティで値が範囲内にあるかどうかをチェックするコードが重複しています。委譲を使用すれば、セッター関数で範囲チェックのコードを再利用できます。値の管理にフィールド、ゲッター関数、セッター関数を使用するのではなく、委譲で値を管理します。

プロパティ委譲を作成するための構文は、変数の宣言で始まり、by キーワード、プロパティのゲッター関数とセッター関数を処理する委譲オブジェクトと続きます。この構文を図にすると次のようになります。

928547ad52768115.png

実装を委譲できるクラスを実装する前に、インターフェースについて理解しておく必要があります。インターフェースとは、それを実装するクラスが守る必要のある決まり事のことです。インターフェースでは、アクションを「どう行うか」ではなく「何を行うか」に焦点を当てます。つまり、インターフェースを使えば抽象化することができるということです。

たとえば、家を建てる前には、建築家に、寝室、子供部屋、リビングルーム、キッチン、複数のバスルームが必要だと伝えます。つまり、施主は「何が欲しいか」を指定し、建築家はそれを「どうやって実現するか」を設計します。インターフェースを作成する構文は、次の図のようになります。

bfe3fd1cd8c45b2a.png

クラスを拡張して、その機能をオーバーライドする方法については、すでに学習しました。インターフェースでは、クラスがインターフェースを実装します。そのクラスでは、インターフェースで宣言されたメソッドとプロパティの実装の詳細を提供します。委譲の作成方法は、ReadWriteProperty インターフェースの場合と同様です。インターフェースの詳細については、次のユニットで説明します。

var 型の委譲クラスを作成するには、ReadWriteProperty インターフェースを実装する必要があります。同様に、val 型の場合は ReadOnlyProperty インターフェースを実装する必要があります。

var 型の委譲を作成しましょう。

  1. main() 関数の前に、ReadWriteProperty<Any?, Int> インターフェースを実装する RangeRegulator クラスを作成します。
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main() {
    ...
}

山括弧とその内側は気にしないでください。これらは一般的な型を表しており、次のユニットで学習します。

  1. RangeRegulator クラスのプライマリ コンストラクタに、initialValue パラメータ、private の minValue プロパティ、private の maxValue プロパティ(どれも Int 型)を追加します。
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. RangeRegulator クラスの本体で、getValue() メソッドと setValue() メソッドをオーバーライドします。
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

これらのメソッドは、プロパティのゲッター関数とセッター関数の役目を果たします。

  1. SmartDevice クラスの前の行で、ReadWriteProperty インターフェースと KProperty インターフェースをインポートします。
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {
    ...
}

...

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

...
  1. RangeRegulator クラスの getValue() メソッドの前の行で、fieldData プロパティを定義し、initialValue パラメータで初期化します。
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

このプロパティが変数のバッキング フィールドとなります。

  1. getValue() メソッドの本体で、fieldData プロパティを返します。
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}
  1. setValue() メソッドの本体で、value パラメータが minValue..maxValue の範囲にあるかどうかをチェックしてから、fieldData プロパティに代入します。
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}
  1. SmartTvDevice クラスで、委譲クラスを使用して speakerVolume プロパティと channelNumber プロパティを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    ...

}
  1. SmartLightDevice クラスで、委譲クラスを使用して brightnessLevel プロパティを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    ...

}

10. 解答をテストする

解答コードは、次のコード スニペットのとおりです。

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"
        protected set

    open val deviceType = "unknown"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}

class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

次のような出力が表示されます。

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

11. 課題に挑戦しましょう

  • SmartDevice クラスで、"Device name: $name, category: $category, type: $deviceType" という文字列を出力する printDeviceInfo() メソッドを定義してください。
  • SmartTvDevice クラスで、音量を下げる decreaseVolume() メソッドと、前のチャンネルに切り替える previousChannel() メソッドを定義してください。
  • SmartLightDevice クラスで、明るさを下げる decreaseBrightness() メソッドを定義してください。
  • SmartHome クラスで、すべてのアクションを、各デバイスの deviceStatus プロパティが "on" 文字列に設定されている場合にのみ実行できるようにしてください。また、deviceTurnOnCount プロパティが正しく更新されるようにしてください。

実装が完了したら、次のことを行ってください。

  • SmartHome クラスで、decreaseTvVolume()changeTvChannelToPrevious()printSmartTvInfo()printSmartLightInfo()decreaseLightBrightness() の各メソッドを定義してください。
  • SmartHome クラスで、SmartTvDevice クラスと SmartLightDevice クラスの適当なメソッドを呼び出してください。
  • main() 関数で、追加したメソッドを呼び出してテストしてください。

12. おわりに

これで、クラスを定義する方法と、オブジェクトをインスタンス化する方法の学習が終わりました。また、クラス間で関係を結ぶ方法と、プロパティ委譲を作成する方法についても学習しました。

まとめ

  • OOP には、カプセル化、抽象化、継承、ポリモーフィズムという 4 つの主要な原理があります。
  • クラスは class キーワードで定義され、プロパティとメソッドを含みます。
  • プロパティは変数に似ていますが、プロパティにはカスタムのゲッターとセッターを設けることができます。
  • コンストラクタでは、クラスのオブジェクトをインスタンス化する方法を指定します。
  • プライマリ コンストラクタを定義する際には、constructor キーワードを省略できます。
  • 継承により、コードの再利用が簡単になります。
  • IS-A 関係は継承を表します。
  • HAS-A 関係はコンポジションを表します。
  • 可視性修飾子は、カプセル化を実現するうえで重要な役割を果たします。
  • Kotlin には、publicprivateprotectedinternal の 4 つの可視性修飾子があります。
  • プロパティ委譲を使用すると、ゲッターとセッターのコードを複数のクラスで再利用できます。

さらに詳しく学習するには