はじめに
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 |