0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlinの抽象クラス・openクラス・インターフェースをMinecraftプラグイン開発で学ぶ(自分用メモ)

Posted at

はじめに

Kotlin で共通処理をまとめる方法について、
「abstract class」「open class」「interface」それぞれの使いどころをMinecraftを例に出し自分なりに整理し、メモを兼ねて記事にしました。
同じような悩みを持つ方の参考になれば嬉しいです。

目次

1.それぞれについて

抽象クラス(abstract class)

[特徴]

  • 共通処理をまとめつつ、必ずサブクラスで実装してほしい関数や変数を定義できる (未実装の場合はコンパイルエラーになる)
  • インスタンス化できない
  • 複数のabstract classを継承することはできない

使用例:Mob の基底クラス

abstract class MobBase {
    abstract fun attack(player: Player) // サブクラスでの実装が必須
    fun move() {
        println("Mobが動いた!")
    }
}

class Zombie : MobBase() {
    override fun attack(player: Player) {
        println("ゾンビが噛みついた!")
    }
}

class Skeleton : MobBase() {
    override fun attack(player: Player) {
        println("スケルトンが矢を撃った!")
    }
}


/*
    [実行結果]
    Zombie().attack(player) -> ゾンビが噛みついた!
    Zombie().move() -> Mobが動いた!
    
    Skeleton().attack(player) -> スケルトンが矢を撃った!
    Skeleton().move() -> Mobが動いた!
*/

open クラス(open class)

特徴

  • 共通処理のデフォルト実装を持ち、必要に応じてサブクラスで上書き(オーバーライド)できます。
  • オーバーライドは必須ではない(実装しない場合は open クラスの処理が実行される)
  • 共通処理のデフォルト実装を持たせたい場合に便利(特定のサブクラスだけ挙動を変えたいときに便利)

使用例:Mob の基底クラス

open class MobOpen {
    open fun approached(player: Player) { // オーバーライド可能
        println("攻撃!")
    }
}

class Zombie : MobOpen() {
     // デフォルト実装を使用
}

class Skeleton : MobOpen() {
     // デフォルト実装を使用
}

class Creeper : MobOpen() {
    override fun approached(player: Player) {
        println("自爆!")
    }
}

/*
    [実行結果]
    Zombie().approached(player)   -> 攻撃!
    Skeleton().approached(player) -> 攻撃!
    Creeper().approached(player)  -> 自爆!
*/

インターフェース(interface)

特徴

  • 「この能力を持つ」という契約を表現できる
  • 抽象メソッドは必ず実装が必要(未実装の場合はコンパイルエラー)
  • デフォルト実装も書ける
  • 複数のインターフェースを同時に実装可能

使用例:モブの能力と敵モブ

// 敵モブの共通能力
interface EnemyMob {
    fun attack(player: Player) { // デフォルト実装
        println("プレイヤーに敵対攻撃!")
    }
}

// 能力ごとのインターフェース
interface DoorOpener {
    fun openDoor() // 実装はサブクラスに任せる
}

interface ArrowShooter {
    fun shootArrow()
}

// 複数の能力を持つクラス
class Zombie : EnemyMob, DoorOpener {
    override fun openDoor() {
        println("ゾンビがドアを開けた!")
    }
}

class Skeleton : EnemyMob, ArrowShooter {
    override fun shootArrow() {
        println("スケルトンが矢を撃った!")
    }
}

// 敵モブではないが、同じ能力を持つクラス
class Villager : DoorOpener {
    override fun openDoor() {
        println("村人がドアを開けた!")
    }
}

/*
    [実行例]
    Zombie().attack(player)       -> プレイヤーに敵対攻撃!
    Zombie().openDoor()           -> ゾンビがドアを開けた!
    
    Skeleton().attack(player)     -> プレイヤーに敵対攻撃!
    Skeleton().shootArrow()       -> スケルトンが矢を撃った!
    
    Stray().attack(player)        -> プレイヤーに敵対攻撃!
    Stray().shootArrow()          -> ストレイが氷の矢を撃った!
    
    Villager().openDoor()         -> 村人がドアを開けた!
*/

2.それぞれの違い

自分が疑問に思ったことをQ&A方式でそれぞれの違いを説明していきます

open classとabstract classの使い分け

Q.「abstract class」のように処理をまとめられるなら、「open class」でも問題ないんじゃないの?

A. open classとabstract classの最も大きな違いは、処理の実装を強制するかどうかにあります。

**open classは、もしサブクラスで処理を書かなかったとしても、コンパイルエラーにはならず、親クラスに書かれたデフォルトの処理がそのまま実行されます。
これは、実装の「強制力がない」ということです。

**abstract classは、「必ず処理を完成させる」**というルールを課します。abstractと書かれたメソッドは、親クラスでは中身が空っぽで、サブクラスが必ずその処理を完成させる必要があります。
もし実装を忘れるとコンパイルエラーになるため、実装漏れを防ぐことができます。

これは、実装の「強制力がある」ということです。

// abstract class で実装する場合
abstract class MobBase {
    abstract fun attack(player: Player) // サブクラスでの実装が必須
    fun move() {
        println("Mobが動いた!")
    }
}

class Zombie : MobBase() {
    // 処理の書き忘れ
}

/*
    [実行結果]
    コンパイルエラー
*/

// open classで実装する場合
open class MobBase {
    open fun attack(player: Player) {}// サブクラスでの実装は任意(未実装なら空処理)
    fun move() {
        println("Mobが動いた!")
    }
}

class Zombie : MobBase() {
    // 処理の書き忘れ
}

/*
    [実行結果]
    Zombie().attack(player) -> (何も起きず)
    Zombie().move() -> Mobが動いた!
*/

open classとinterfaceの使い分け

Q. open classとinterfaceはどちらも共通処理を書けるけど、どう使い分けるの?

A.open classとinterfaceの最も大きな違いは、「関係性」をどう表現したいかです。

open class は 「〜は〜の一種である」 という親子の関係性を表します。
例えば、 「ゾンビはMobの一種である」 といった関係です。
この関係は 一つしか持つことができません。

open class Mob() {}
open class EnemyMob() {}
class Zombie():Mob(),EnemyMob() {}

/*
    [実行結果]
    コンパイルエラー
*/

interface は 「〜は〜の能力を持つ」 という役割や能力を表します。
例えば、「ゾンビはドアを開ける能力を持つ」といった関係です。
キャラクターは複数の能力を持つことができるため、 複数のinterfaceを同時に実装できます。

abstract classとinterfaceの使い分け

Q. abstract classもinterfaceも、処理を強制的に実装させる機能があるけど、どう使い分ければいいの?

A. どちらも実装を強制することはできますが、その目的が異なります。abstract classは 「親としての土台」 を定義し、interfaceは **「能力や役割」**を定義します。

abstract class は、 「〜は〜の一種である」 という親子関係を表すのに使います。これは共通の土台を作るためのものです。
例えば、SkeletonはMobBaseの一種です。
クラスは一つしか親を持てないため、一つのabstract classしか継承できません。

abstract class MobBase {}
abstract class UseBowMob {}

class Skeleton():MobBase(),UseBowMob() {}

/*
    [実行結果]
    コンパイルエラー
*/

interface は、「〜は〜の能力を持つ」という役割を表すのに使います。
これは能力の組み合わせを表現するためのものです。
例えば、ZombieはEnemyMobという能力とDoorOpenerという能力を両方持ちます。
複数の能力を同時に持てるため、複数のinterfaceを実装できます。

3.選び方まとめ表 |

種類 実装の強制力 継承の数 使いどころ
abstract class あり 1つまで 親としての土台、必ず実装してほしい処理 MobBase
open class なし(任意) 1つまで 共通処理をデフォルト化、必要な時だけオーバーライド MobOpen
interface あり/なし 複数可 能力や役割の付与 DoorOpener
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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?