101010

とあるアプリ開発者のブログです。KotlinやSwiftが好き。たまにポえむってます。

抽象クラスとインターフェース

f:id:araemonz:20190201180627j:plain
抽象クラスからインタフェースを実装してインスタンス生成するまでのイメージ

Kotlinスタートブック -新しいAndroidプログラミング を読んだことで、Kotlinへの理解がだいぶ深まった。 おかげでKotlinリファレンスをスラスラ読めるようになってきたので嬉しい。

そんなリファレンスの中で、良きサンプルコードを見つけたので取り上げてみる。

Basic Syntax - Creating basic classes and their instances

上記URLのサンプルコードには、抽象クラスやインタフェースの技術が詰め込まれている。今回は、そのサンプルコードを自分なりにまとめてみることにした。

何を作るの?

四角形(長方形)と三角形のクラスを作り、面積や周囲の長さ求めるメソッドを実装していく。 また長方形のクラスには、正方形であるかどうかの判定できるプロパティを持たせる。

次のように使用することを、最終目標とする。

fun main() {

    val rectangle = Rectangle(5.0, 2.0)
    val square = Rectangle(2.0, 2.0)
    val triangle = Triangle(3.0, 4.0, 5.0)

    println("長方形の面積:${rectangle.calculateArea()} / 周囲の長さ:${rectangle.perimeter} / 正方形判定:${rectangle.isSquare}")
    println("長方形の面積:${square.calculateArea()} / 周囲の長さ:${square.perimeter} / 正方形判定:${square.isSquare}")
    println("三角形の面積は${triangle.calculateArea()}、周囲の長さは${triangle.perimeter}です。")
}

長方形と三角形のクラスを作る

まずは辺の長さだけをプロパティに持たせた、シンプルなクラスを作ろう。 Rectangleは長方形、Triangleは三角形だ。

class Rectangle(val height: Double, var length: Double)
class Triangle(var sideA: Double, var sideB: Double, var sideC: Double)

図形に共通なメソッドを持たせるための抽象クラスを作る

周囲の合計の長さや、面積の計算は、長方形と三角形のどちらの図形にも必要なメソッドだ。 このような共通するメソッドは、抽象クラス化して継承させるのがスマートだろう。

abstract class Shape(val sides: List<Double>) {
    val perimeter: Double get() = sides.sum() // 周囲の合計の長さ
    abstract fun calculateArea(): Double // 実装を強制
}

abstract class は抽象クラスとしての宣言だ。また、メンバ変数やメソッドに abstract をつけると、抽象クラスを継承したクラスはそのメンバをオーバーライドする義務が発生する。こうすることでメソッドの重複や書き忘れなく、規則に沿った、統一感のある設計ができるようになる。

抽象クラスを継承する

それでは先程のShapeクラスを継承してみよう。最初に書いたRectangleとTriangleに継承すると次のようになる。

class Rectangle(
    val height: Double,
    var length: Double
) : Shape(listOf(height, length, height, length)) {
    override fun calculateArea(): Double {}
}

class Triangle(
    var sideA: Double,
    var sideB: Double,
    var sideC: Double
) : Shape(listOf(sideA, sideB, sideC)) {
    override fun calculateArea(): Double {}
}

関数calculateAreaの実装は後ほど書くとして、抽象クラスの雰囲気はつかめただろう。 Shape(listOf(sideA, sideB, sideC))のように、抽象クラスのコンストラクタにlistを渡している。RectangleやTriangleのコンストラクタで受け取った引数をlistにして、抽象クラスへ渡しているのだ。

それでは、関数calculateAreaを実装していこう。長方形の面積は 底辺 x 高さ なので、次のようになる。

override fun calculateArea(): Double = height * length

また、三角形の面積は ヘロンの公式 を使うと、次のようになる。

override fun calculateArea(): Double {
    val s = perimeter / 2
    return Math.sqrt(s * (s - sideA) * (s - sideB) * (s - sideC))
}

インタフェースの登場

抽象クラスができたところで、次に正方形かどうか判定するフラグを、クラスRectangleもたせよう。 長方形だけにというのがポイントだ。つまり、三角形には正方形判定のプロパティをもたせたくない。だから当然、抽象クラスには実装しなかった。ここではインタフェースを通して、正方形判定のプロパティを実装していく。

インタフェースRectanglePropertiesの内容だ。

interface RectangleProperties {
    val isSquare: Boolean
}

インタフェースでは、プロパティやメソッドの宣言をするだけで中身の処理は書けない。あくまでもインタフェースであって、実装は具象クラスに任せるのだ。

RectangleクラスとTriangleクラスとの違いが、インタフェースの実装によって簡単に判断できることがわかるだろう。

class Rectangle(
    val height: Double,
    var length: Double
) : Shape(listOf(height, length, height, length)), RectangleProperties 

class Triangle(
    var sideA: Double,
    var sideB: Double,
    var sideC: Double
) : Shape(listOf(sideA, sideB, sideC))

それでは最後に、Rectangleクラスにインタフェースの内容を実装しよう。 横の長さlengthと高さheightが同じ値であれば、正方形であるので次のように書ける。

// class Rectangle
override val isSquare: Boolean get() = length == height

ここまでのプログラミングのまとめ

ここまでのプログラミングコードをまとめておく。

abstract class Shape(val sides: List<Double>) {
    val perimeter: Double get() = sides.sum() // 周囲の合計の長さ
    abstract fun calculateArea(): Double // 実装を強制
}

interface RectangleProperties {
    val isSquare: Boolean
}

class Rectangle(
    val height: Double,
    var length: Double
) : Shape(listOf(height, length, height, length)), RectangleProperties {
    override val isSquare: Boolean get() = length == height
    override fun calculateArea(): Double = height * length
}

class Triangle(
    var sideA: Double,
    var sideB: Double,
    var sideC: Double
) : Shape(listOf(sideA, sideB, sideC)) {
    override fun calculateArea(): Double {
        // 3辺の長さから面積を求める、ヘロンの公式
        val s = perimeter / 2
        return Math.sqrt(s * (s - sideA) * (s - sideB) * (s - sideC))
    }
}

これで目的のクラスはすべて揃った。

作ったクラスの使用例

最後に、作ったクラスからインスタンスを生成して、メソッドやプロパティへアクセスしてみよう。

fun main() {

    val rectangle = Rectangle(5.0, 2.0)
    val square = Rectangle(2.0, 2.0)
    val triangle = Triangle(3.0, 4.0, 5.0)

    println("長方形の面積:${rectangle.calculateArea()} / 周囲の長さ:${rectangle.perimeter} / 正方形判定:${rectangle.isSquare}")
    println("長方形の面積:${square.calculateArea()} / 周囲の長さ:${square.perimeter} / 正方形判定:${square.isSquare}")
    println("三角形の面積は${triangle.calculateArea()}、周囲の長さは${triangle.perimeter}です。")
}

次のように、期待通りの出力結果となった。

長方形の面積:10.0 / 周囲の長さ:14.0 / 正方形判定:false
長方形の面積:4.0 / 周囲の長さ:8.0 / 正方形判定:true
三角形の面積は6.0、周囲の長さは12.0です。