前記事はこちら。
まえおき
前回の投稿から少し時間が経ち、その間にいろいろと反応をいただけました。
引き続きRPGの世界観を踏襲しつつ、かつ現実世界に還元しすぎないような「プログラミングのイチ技術」としてのオブジェクト指向解説を目指したいと思います。
内容
- クラスの作成、インスタンス化、抽象クラスと継承の解説 ← 前回
- インターフェース、
ダックタイピングポリモーフィズムの解説 ← 今回
今回はインターフェース、ポリモーフィズム、そして少しだけデザインパターンについて記述します。
前回ダックタイピングと書いてましたが、ポリモーフィズムに差し替えます1。
前回の問題点と課題
第1回の最後に継承の問題点について記述しました。要約すると次のとおりです。
- 基底クラスは拡張がしづらい
- 基底クラス変更の影響は、派生クラスすべてに及ぶ
- 基底クラスの抽象メソッドは派生クラスにて必ず実装しなければならないため
今回の課題は「特殊攻撃(skillAttack)」を実装し、かつ影響(コード変修正)を限定的にすることです。これを、インターフェースを使って解決してみます。
前回に引き続き、次の基底クラスMonster.ktを使っていきます。
abstract class Monster {
abstract fun attack(target: Human)
abstract fun protect()
}
インターフェースとは
とりあえずインターフェースの定義から。ここは「ほーん?」と流していただければ大丈夫です。
interface ISkillAttack { //←インターフェース名
fun skillAttack() //←実装なしのメソッド
}
クラス名のようなモノ(ISkillAttack)2と、実装のないメソッド名(skillAttack)がひとつ記述されています。
スライムに特殊攻撃実装するよ! - ISkillAttackの実装
さてこれをどう使うかというと、前回のSlimeクラスを思い出しましょう。Slimeに特殊攻撃「溶解液」的なものを実装します。
Kotlinにおいて、インターフェースの実装は「: ..., ISkillAttack」とカンマ区切りで記述します。Javaにおいてはimplementsです。
class Slime : Monster(), ISkillAttack { //←ISkillAttackを実装
override fun attack() {...}
override fun protect() {...}
//追加。skillAttackを必ずoverrideしなければならない
override fun skillAttack() {/*服だけを溶かす溶解えk*/}
}
これで基底のMonsterクラスをいじること無しに、Slimeに特殊攻撃を実装することができました!ワーイ!
「何がそんなに嬉しいの?」という声が聞こえそうなので、じゃあKingSlimeとRedDragonにも特殊攻撃を実装してみます。炎吐かせたりしてみたいですしね。
複数のモンスターに実装するよ!
class KingSlime : Monster(), ISkillAttack {...}
class KillerMachine : Monster() {...}
class RedDragon : Monster(), ISkillAttack {...}
...
これであなたのもつモンスターには、Slime、KingSlime、RedDragonの三体にのみ、特殊攻撃が実装されました。
それでは前回と同様に、勇者に一斉攻撃を仕掛けてみようと思います。ただし、上記の三体には特殊攻撃を指示します。
val monsters = listOf<Monster>(Slime(), KingSlime(), ...) //←前と同じ
for (monster in monsters){
//forでリストに格納した全モンスターに指示をする
if (monster is ISkillAttack){
//ISkillAttack#skillAttackを実装している
//Slime, KingSlime, RedDragonのみがこのブロックに入る
monster.skillAttack()
} else {
//残りはattackを指示
monster.attack(yuusya)
}
}
これで勇者は服が溶け、炎で焼かれ、残りのモンスターからフルボッコにされます。
何が起こったのか?
if (monster is ISkillAttack)
は、ISkillAttack型にキャスト可能かどうかを判別しています。
Kotlinのis演算子はJavaのinstanceofと同じで、そのインスタンスが目的の型にキャスト可能かどうかを調べられます。キャスト可能ということは、ISkillAttack#skillAttackがコールできるということを示します3。
ISkillAttack型を実装している
↓
skillAttackが必ず実装(override)されている
↓
(中身は知らんが)skillAttackを必ずコールできる!
しれっとISkillAttack「型」と言っていますが、インターフェースはクラスと同じように型として使うことができます。
これがインターフェースの強みで、変数の中身がよく分からなくてもISkillAttack型と言えればskillAttackをコールできます。また、実際にどう動くかはクラス自身の実装に委ねられます。
重ねて言えば、あなた自身が
「お前はRedDragonだな?(if monster is RedDragon)炎吐けー!」
「お前はSlimeだな?(if monster is Slime)溶解液を出せー!」
など、いちいち"君の名は"と聞いて指示を与えるまでもなく、
「特殊攻撃ができるやつは(if monster is ISkillAttack)特殊攻撃しろー!」
と言えば済んでしまうようになりました。
現実問題どう使うの? - Observerパターン
オブジェクト指向を初めて勉強していたころ、クラスの継承は(個人的には)理解しやすかったですが、インターフェースはいまいち利点が分かりづらい印象がありました。
そこで、デザインパターンのひとつ「Observerパターン」を取り上げて、継承とインターフェースの使用法を見ていきます。Observerパターンは先ほどの一斉攻撃にも近いパターンになります。
Observerパターンとは
「Subject#notifyメソッドが呼ばれると、Subjectに集約されたオブジェクトのupdateメソッドを順次叩きに行く」
シーケンス図は次のようになります。
オレンジ枠と赤枠で囲まれた部分がObserverパターンに必要な部分となります。
カラーピッカーを考える
次のようなカラーピッカーを考えます。
これは筆者が技術書典5の執筆用にAndroidで自作したもので、実際にObserverパターンを採用しています。
四角い領域をタップすると、タップ座標から色を取得し、右側にRGB値を表示します。ここで、RGB値を表示するテキスト部分がupdateメソッドを実装したクラスになります(詳細は次節)。
Observerパターンで出てくるクラス
Observerパターンでは次のクラスを作成します。
- 抽象Subjectクラス
- IObserverを集約するリストを保持する
- パターンの構築に不可欠な「notify, attach, detachメソッド」等を定義する
- 「不可欠」=基底クラスで定義すべき事項
- 具象Subjectクラス
- 抽象Subjectを継承。IObserverの集約と更新の一斉通知をする
- IObserverインターフェース
- updateメソッドを定義
- ISkillAttackがこれにあたる
- IObserverを実装したクラスたち
- Subjectで更新が通知されたとき、updateの実装に従っていろいろする
- 「実装に従っていろいろする」=派生クラスで定義すべき(基底クラスに実装しない)
- カラーピッカーでは、RGB値を表示するTextViewに実装する
- RPGでは、Slime, KingSlime, RedDragonがこれにあたる
以下、参考にカラーピッカー用のコードを掲載しますが、ここは読み飛ばしてもらっても結構です。動作はシーケンス図とGIF画像の通りなので端折らせていただきます。
//使い回ししやすいように、ジェネリクス型で定義
abstract class Subject<T> {
//通知先をリストにて集約
protected val observers = mutableListOf<T>()
//notifyを定義
abstract fun notify()
//通知先のオブジェクトを登録
fun attach(observer: T){ observers.add(observer) }
//通知先を全てクリア
fun detachAll(){ observers.clear() }
...
}
class ColorSubject: Subject<IColorObserver>() {
//登録されているIColorObserverオブジェクトに更新を一斉通知
override fun notify() {
//observersは抽象Subjectクラスに定義
for (observer in observers){
observer.update()
}
}
...
}
interface IColorObserver {
//ISkillAttackに相当
//何をするかは実装されたクラスで記述
fun colorUpdate()
}
//RGB値を表示するTextView
class ObservableTextView: TextView, IColorObserver {
override fun colorUpdate(){
val color = getSubject().getColor() //←適当デス
//RGB値に変換してテキストセットなど
...
}
...
}
//updateを実装させたい適当なクラス
class Hoge: IColorObserver {
override fun colorUpdate(){ /*なんやかんや実装*/ }
...
}
Observerパターンを通して言いたいこと
ちょっと書き散らかしてしまいましたが、この例を通して言いたいことは次のとおりです。
- 全派生クラスに共通する事項については、基底クラスにて定義し継承する
- 派生クラスの一部に対し機能を追加・拡張する場合、インターフェースで実装する
- 基底クラスで決めるべきことの選別ってムズカシイ(拡張性の検討)
またデザインパターンを勉強すると、
やりたいこと
↓
パターンを探す・適用
↓
基底クラスで定義すべき事項、インターフェースで実装すべき事項の見通しがつく
ので、デザパは知っておいたほうが人生得すると思います。4
ポリモーフィズムとまとめ
正直ここまで書いてきたことがポリモーフィズムについての解説になります。
- 基底クラス型(Monster)の変数に派生クラスを格納すると、overrideされたメソッドが実行される5
- 変数の中身が実際になんのオブジェクトが知らなくても、型を調べればどんなメソッドが実装されているのか分かる
- 呼び出す側はオブジェクトの中身について知らなくていいし、知っているべきではない(疎結合)
書きながら思ったのですが、現実世界で考えると、ひとつの「物(オブジェクト)」をさまざまな「角度(型)」から見ることで「違う解釈(override?)」ができる、そんな感じかなと思いました。ちょっと無理がありますかね。
あとがき
2回に渡り、私的オブジェクト指向の解説を書かせていただきましたがいかがでしたでしょうか。個人的にはオブジェクト指向というよりも、型システムの解説みたいな感じになってしまったのはちょっと残念でしたが、今の自分に書ける限界はこんなところだと思います。
最後までお読みいただき、ありがとうございました。