ソフトウェア技術研究会
オブジェクト指向について<!-- --> | 東北工業大学ソフトウェア技術研究会

オブジェクト指向について

📅2024-11-19

オブジェクト指向

こんにちわ、K.Y. (HN Rerurate_514)です。 最近、2年生の講義でオブジェクト指向について扱ったので、それについて解説していこうと思います。

前半、初心者向けで(pythonで説明)、後半はオブジェクト指向を知っている方向け(ts, kt, dartなどで説明)です。 今回は考え方を説明するもので、言語ごとのやり方を示すものではありません。 自分自身も初学者なので、間違っていたらごめんなさい。

前半

概要

クラスについて

オブジェクト指向というのは、似たようなデータや処理をまとめたクラスというものを作成して管理しやすいようにしたものです。

例えば音楽再生に関するプログラムを書いたとします。

def main():
	#音楽再生の関数を使用する。

def playMusic():
	#音楽の再生処理

def stopMusic():
	#音楽の停止処理

def moveNextMusic():
	#次の曲に移動する処理

def movePreviousMusic():
	#前の曲に移動する処理

def upVolume():
	#音量上げる処理

def downVolume():
	#音量下げる処理

この状態ではまだわかりやすく見えるかもしれませんが、音楽再生するだけのプログラムはアプリとかでは使えないですよね? アプリなどを作る際には、さらにUIに関するコードや音楽を取得するプログラムも必要なのです。

それらをすべて同じ場所に書いていたらごちゃごちゃして訳が分からなくなってしまいます。 そうなるとバグが発生した箇所を特定できなかったり、一か所変更したらいろんなところでバグが発生してしまったり、、なんてことが起きてしまいます。

そういう時にクラスを使用して同じような処理やデータをまとめるわけです。

上の音楽再生の例でクラスを使用した例を示してみると、

class MusicPlayer:
	def playMusic():
		#音楽の再生処理
	
	def stopMusic():
		#音楽の停止処理
	
	def moveNextMusic():
		#次の曲に移動する処理
	
	def movePreviousMusic():
		#前の曲に移動する処理
	
	def upVolume():
		#音量上げる処理
	
	def downVolume():
		#音量下げる処理

def main():
	player = MusicPlayer()#インスタンス生成

となり、クラスを書くことでコードが見通しやすくなるわけです。

インスタンスについて

上記の音楽プレーヤークラスのメソッドを使用するためにインスタンスというものを使用します。 巷ではクラスが設計書でインスタンスが実体であるなんて言われていますが、これを言われて一発で理解できる人なんているのでしょうか? 私が高校生の時にこのような説明を受けて全くわからなかった記憶があります。

私はインスタンスについてこう考えています。 クラス内の変数やメソッドにアクセスするための道具である。 さらに、この道具には値を保持する機能がついています。

実体なんだか道具なんだかと言っていますが、要するにクラスの中に書いた処理やデータを使用するにはインスタンスを介さないといけないと理解すればよいです。

class MusicPlayer:
	def playMusic():
		#音楽再生の処理

def main():
	playMusic() #エラー:これはplayMusicという関数が、main関数が存在するスコープに存在しないからである。

	player = MusicPlayer() #インスタンス生成
	player.playMusic() #正常に動作する:インスタンスを介してMusicPlayer内のメソッドにアクセスしている。

さらに、値がインスタンスで保持されるので、同じクラスから作成されたインスタンスでも全く違うものを指すことができます。 これはどういうことかというと、

class Person:
	def __init__(self, name, age, gendar, skill):
		self.name = name
		self.age = age
		self.gendar = gendar
		self.skill = skill

	def printName(self):
		print("名前:", self.name)

	def printAge(self):
		print("年齢:", self.age)

	def printGendar(self):
		print("性別:", self.gendar)

	def printSkill(self):
		print("スキル;", self.skill)

というコードがあったとします。 このコードは名前と年齢、性別、スキルのプロパティ(属性)を持つPersonクラスを作成しています。 このクラスを使用すると、

tarou = Person("太郎", 18, "男性", ["プログラミング", "運動"])
hanako = Person("花子", 18, "女性", ["料理", "数学"])

といったように、同じクラスでも太郎さんと花子さんといった、全く違うものを指すオブジェクトを作成することができました。

この時、二人の性別を取得したい場合、クラス内のプロパティにインスタンスを介してアクセスします。

print(tarou.gendar) #男性と出力される
print(hanako.gendar) #女性と出力される

もしこれをクラスなしで作成したらどうなるでしょうか?一例として挙げてみます。

tarouName = "太郎"
tarouAge = 18
tarouGendar = "男性"
tarouSkill = ["プログラミング", "運動"]

hanakoName = "花子"
hanakoAge = 18
hanakoGendar = "女性"
hanakoSkill = ["料理", "数学"]

これをクラス全員分の変数名と値を書くのはとても大変です。 さらに値を表示する関数もそれぞれの人ごとに作成しなければなりません。

インスタンスを使用することでコードを綺麗に効率よく書くことができるのです。

スコープ

アクセス制限

さらにクラスにはスコープ(変数やメソッドにアクセスできる範囲)を設定するという役割もあります。

例えばゲームのプログラムを書いたとします。 ゲームは様々な処理があります。 UIの処理、ステージの処理、HPの処理、アイテムの処理、敵の処理、攻撃などです。 その中でもプレイヤーに関する処理だけを例としてクラスなしで書いてみます。

playerHP = 100
playerMana = 200

def damegedPlayerHP():
	#プレイヤーのHPを減らす

def healedPlayerHP():
	#プレイヤーのHPを回復する

def attackToEnemy():
	#攻撃
	
def defence()
	#防御

def useItem():
	#アイテムを使用する

def escapeFromEnemy():
	#逃げる

def fire():
	#炎魔法

def ice():
	#氷魔法

#など。

このようにしたとき、HPに関する処理はdamagedPlayerHPhealedPlayerHPのみです。 しかし、別の担当者が間違ってプレイヤーに全く関係がない関数の中にHPを変更する処理を書いてしまったとします。

#敵がプレイヤーから逃げる関数
def escapeFromPlayer():
	playerHP += 100;
	#逃げる処理

このとき、HPに全く関係がない場面でHPが変動してしまうというバグが発生してしまいます。 さらにこのバグはエラーが出ない(コード的には正常、ゲーム的にはバグ)ので、バグが発生した箇所を特定するのが非常に困難です。 上に示したコードはたかだか20行程度ですが、実際のゲームやアプリのコードは何千何万何十万行とあるのでその中からバグを特定するのは、かなり厳しいです。

では、なぜこれが発生したのかを考えてみます。

これは、プログラミング的には、その値にいつでもアクセスできるからが理由になります。 もし、playerHPにアクセスできない場合、このようなバグも発生しないのです。 では、これを解決しようとするとスコープを設定する必要があります。

それを実現出来るのがクラスになります。

class Player:
	def __init__(self):
		self.playerHP = 100
		self.playerMana = 200

	def damegedPlayerHP():
		#プレイヤーのHPを減らす
	
	def healedPlayerHP():
		#プレイヤーのHPを回復する
	
	def attack():
		#プレイヤーが攻撃する処理
		
	def defence()
		#プレイヤーが防御する処理
	
	def useItem():
		#アイテムを使用する
	
	def escapeFromEnemy():
		#敵から逃げる
	
	def fire():
		#炎魔法
	
	def ice():
		#氷魔法

クラスを使用すると、上記に示したコード

#敵がプレイヤーから逃げる関数
def escapeFromPlayer():
	playerHP += 100; #playerHPが存在しないというエラーが発生する。
	#逃げる処理

を書いたとしても実行段階でエラーが発生するのでバグが発生した箇所が特定でき、修正も楽になります。

名前空間

さらにスコープを設定するのは名前空間の衝突を防ぐ役割もあります。 名前空間の衝突とは、同じ名前の変数や関数を書いたときにエラーが発生することです。 同じスコープ内では例外を除いて同じ名前のオブジェクトは存在できません。

上の例で言うと

playerHP = 100
playerMana = 200

def damegedPlayerHP():
	#プレイヤーのHPを減らす

def healedPlayerHP():
	#プレイヤーのHPを回復する

def attack():
	#プレイヤーが攻撃する処理
	
def defence()
	#プレイヤーが防御する処理

def useItem():
	#アイテムを使用する

def escapeFromEnemy():
	#敵から逃げる

def fire():
	#炎魔法

def ice():
	#氷魔法

#など。

ですが、例えばこのコードに敵に関する処理を入れようとします。 敵も攻撃を行うのでattack関数を作りたいです。 しかし、このコードでは既にプレイヤー側でattack関数を作成しているので、敵側のattack関数を実装することができません。

この時、クラスを分けてスコープを定義すると同じ関数名でプログラムを書くことができます。

class Player:
	def attack():
		#プレイヤーが敵に攻撃する処理

class Slime:
	def attack():
		#敵がプレイヤーに攻撃する処理

このように書くと、これらのコードを使用するときに、

player = Player()
slime = Slime()

player.attack()
slime.attack()

のように、攻撃に関する処理がattackで統一されて見やすくなります。

まとめ

極論を言うとオブジェクト指向を使用せずとも様々なコードやゲームを書くことはできます。 ただ、同じアプリを一人で作成するのは、仕事ではほぼあり得ません。 この時、皆が思い思いにプログラムを作成すると、プログラムがごちゃごちゃになってメンテナンスもバグの修正もできなくなってしまいます。 これを効率よく綺麗にコードを書いてメンテナンスを容易にする仕組みがオブジェクト指向です。 ※人により様々な主張があります。

今回の記事の前半では後半で書いているような継承や委譲といった難しい仕組みについては解説していません。 初心者に一気にこれらを説明すると訳が分からなくなってしまうかもしれないと考えたからです。

後半

ここからは自分のwikiに入れていたノートなので、書き方がちょっぴり違いますがご容赦ください。 ただ、ポリモーフィズムに関する記述、間違っていたらごめんなさい。自分の中の認識を書きました。

概要

オブジェクト指向とは、共通の性質をもったオブジェクトをクラスとしてまとめるものです。 またあるデータの集合体でもあります。

このクラスを具現化したものをインスタンスといいます。 以下に #lang/Dart での例を示すと

class SampleClass{
	final String name;
	final int age;
	
	SampleClass(this.name, this.age)	
}

//インスタンス化
final instance = SampleClass("rerurate", 20);

となります。 インスタンス化を行うと、インスタンスを介してそのクラスにあるメンバを使用することができます。

用語

データを属性(プロパティ、インスタンス変数)、クラス内の関数をメソッド、これらクラス内に定義されたオブジェクトをクラスメンバといいます。

プロパティに対して、ほかのモジュールからのアクセスに対してデータを隠ぺいする(=変更されないようにする)ことをカプセル化といいます。

以下に #lang/TypeScript での例を示します。

class Person{
	private name: string
	private age: int
	
	constructor(name: string, age: int){
		this.name = name;
		this.age = age;	
	}
}

この言語ではprivate修飾子によってメンバを隠ぺいすることができ、アクセスを制限することができます。

クラスの関係

is-a関係

is-a関係は特化関係、汎化関係ともいわれる。

複数のクラスに共通した部分を抽出して上位クラスとして定義するもの。

part-of関係

part-of関係は集約関係、分解関係ともいう。

複数の下位クラスが集合して、一つの上位クラスを構成するといった関係

特性

継承(インヘリタンス)

継承とは上位クラスで定義された性質が、下位クラスへと継承されることです。

以下に #lang/Dart での例を示します。

//スーパークラス
class Animal{
	void voice(){
		print("がおー");
	}
	
	void walk(){
		//歩く処理
	}
}

//サブクラス
class Cat extends Animal{
	
	void voice(){
		print("にゃー");
	}
}

//サブクラス
class Dog extends Animal{
	
	void voice(){
		print("わん");
	}
}

Dart言語では継承にextendsキーワードを使用します。

このコードでは、動物についての動作を定義するAnimalクラスを作成しました。 さらにそのクラスの中に、鳴き声を出力するメソッド(voice)と歩くメソッド(walk)を定義しました。 このAnimalクラスを継承すると、そこに書いてある処理を使用することができます。

ここで、猫のクラス(Cat)を作りたいとなったとして、動物を表すAnimalクラスを継承しました。

しかし、Animalクラスのvoiceメソッドを見てみると、がおーという鳴き声が定義されています。 試しにAnimalクラスを継承しただけの状態でCatクラスを使用してみます。

class Animal{
	void voice(){
		print("がおー");
	}
	
	void walk(){
		//歩く処理
	}
}

class Cat extends Animal { }

void main(){
	var cat = Cat()
	
	cat.voice()//がおー と出力される
	cat.walk()//正常に、歩く処理が実行された
}

歩く処理であるwalkメソッドは正常にAnimalクラスに定義された動作を実行しましたが、鳴き声は変更していないのでがおーと出力されてしまいました。 しかし、猫の鳴き声はがおーではなくにゃーです。

このように動作を変更したい場合に使用するのがオーバーライド(override)です。 オーバーライドを使用したコードに書き直してみます。

class Animal{
	void voice(){
		print("がおー");
	}
	
	void walk(){
		//歩く処理
	}
}

class Cat extends Animal {
	
	void voice(){
		print("にゃー")
	}
}

void main(){
	var cat = Cat()
	
	cat.voice()//にゃー と出力される
	cat.walk()//正常に歩く処理が実行された
}

鳴き声を変更したのみで新たにCatクラスを作成することができました。 移動に関するwalkメソッドは、そのままオーバーライドせずに使用しています。

Catクラスを作りたいだけならこう書けばいいじゃん!と思う方もいるかもしれません。

class Cat{
	void voice(){
		print("にゃー");
	}
	
	void walk(){
		//歩く処理
	}
}

しかし、さらにほかの動物を作りたいとなった場合はどうでしょう。

class Cat{
	void voice(){
		print("にゃー");
	}
	
	void walk(){
		//歩く処理(共通)
	}
}

class Dog{
	void voice(){
		print("わん");
	}
	
	void walk(){
		//歩く処理(共通)
	}
}

ここでwalkメソッドを見てみると、二回同じコードが書かれていることになります。 ここからさらに、動物を実装しようとすると毎回、歩く処理を何度も何度もコピペしなければなりません。 さらに、walkメソッド内の動作を変更したい場合、すべての動物に対して、変更後のコードを適用させるのはとても大変です。そこで変更の見落としがあった場合、それは意図しない動作、つまりバグにつながります

そんなことは嫌なので、継承を使用してコードを共通化させるのです。

//スーパークラス
class Animal{
	void voice(){
		print("がおー");
	}
	
	void walk(){
		//歩く処理
	}
}

//サブクラス
class Cat extends Animal{
	
	void voice(){
		print("にゃー");
	}
}

//サブクラス
class Dog extends Animal{
	
	void voice(){
		print("わん");
	}
}

このコードを使用すると、

void main(){
	var cat = Cat()
	cat.voice()//にゃー と出力される
	cat.walk()//歩く処理

	var dog = Dog()
	dog.voice()//わん と出力される
	dog.walk()//歩く処理
}

となり、わざわざ歩く処理をコピペせずともCatクラスとDogクラスで歩く処理を実装することができます。 さらにAnimalクラスのwalkメソッドを変更するだけで、CatクラスとDogクラスの歩く処理も変更され、見落としが減ることになります。

ここまで書いておいてなんですが、実際にはこの例のAnimalクラスでCatDogを作成してはいけません。なぜかというと、例えば鳥も動物ですよね。シャチとか。これらをAnimalクラスで作成しようとするとAnimalクラスでとてもたくさんの処理(責務)を実装しなければならず、SOLID原則(後述)を守ることができないからです。

ポリモーフィズム

ポリモーフィズムとは、同一メッセージに対する振る舞いがクラスごとに異ならなければいけないという考え方のことです。

コード例を示します

class Animal {
  final String name;
  const Animal(this.name);

  void voice() {
    print("がおー");
  }

  void walk() {
    //歩く処理
  }

  void printName() {
    print("名前:$name");
  }
}

//サブクラス
class Cat extends Animal {
  Cat(String name) : super(name);

  
  void voice() {
    print("にゃー");
  }
}

//サブクラス
class Dog extends Animal {
  Dog(String name) : super(name);
  
  
  void voice() {
    print("わん");
  }
}

この時、コードを使用すると、

void main(){
	var cat = Cat("みけねこ")
	cat.voice()//にゃー と出力される
	cat.walk()//歩く処理
	cat.printName()//みけねこ と出力される

	var dog = Dog("ちわわ")
	dog.voice()//わん と出力される
	dog.walk()//歩く処理
	dog.printName()//ちわわ と出力される
}

となり、CatクラスとDogクラスで、同じ名前であるvoiceメソッドとprintNameメソッドで違う動作をすることができました。 同じメソッド(オーバーライドによって上書き済み)で呼び出したとしても異なる振る舞いをすることをポリモーフィズムといいます。

ポリモーフィズム、かなり難しいという印象を持ちがちです。 しかし、実際には同じスーパークラスAを継承したサブクラスBとサブクラスCが存在するとき、オーバーライドしたメソッドでそれぞれBとCで振る舞いが変わらなければならないというものです。

オーバーロード

別名を多重定義とも言います。 同一名で関数のシグネチャが異なるメソッドを複数定義することができるものです。

以下に #lang/Kotlin で例を示します。

class Printer(){
	fun print(name: String) {
		print("名前は" + name)
	}
	
	fun print(age: int){
		print("年齢は" + age)
	}
}

この例では、Printerクラスでprintという名前のメソッドが二つ定義されています。 通常、同一名の変数やクラスは同じスコープ内で定義することはできません。 しかし、オーバーロードを使用すると、同一名でもシグネチャが異なると定義することができます。

呼び出す際もstringを引数に入れると引数がstring型printメソッドが呼び出されますし、int型を引数に入れると引数がint型printメソッドが呼び出されます。 言語側で引数に入れられた型によってどちらを呼び出すか判定しているので、このような書き方ができます。

これをオブジェクト指向の機能かといわれると微妙ですが一応入れました。

デリゲーション

別名を委譲ともいいます。 別のクラスに処理を委託するという考え方です。 この委譲では継承を使用せずに処理を共通化して効率よくコーディングする際に使用されます。 ではどうして、継承を使用しない場面が出てくるかというとhas-a関係にあるクラス間の場合です。 基本的に継承を使用するのはis-a関係にあるクラスのみです。 例えば上のAnimalクラスとCatクラスでいうならばCat is a Animalであるので継承を使用してもいい場合です。

しかし、このような場合を考えてみます。

class CPU {
	string register = ""

	void enzan(){
		//演算処理
	}
	
	void decode(){
		...
	}
}

class PC extends CPU{
	void login(){
		...

		enzan();

		...
	}
}

このコードは、PCクラスでCPUクラスのenzanメソッドを使用したいがために継承を使用しています。これは正しい継承の使用方法ではありません。このように使用してしまうとPCクラスをインスタンス化したとき、decodeなどのPCクラスにはしてほしくないメソッドも使用できてしまいます。

pc = PC()

pc.decode()//?

この時、PCクラスはenzanメソッドだけを使用したいです。 その時、委譲を使用すると無駄なくコードを使用することができます。

class CPU {
	string register = ""

	void enzan(){
		//演算処理
	}
	
	void decode(){
		...
	}
}

class PC {
	final CPU cpu = CPU();
	
	void login(){
		...
		
		cpu.enzan();
		
		...
	}
}

PCにとって必要ないCPUクラスのメソッドをそぎ落とすことができました。

個人的な話になりますが、継承よりも委譲を使用したほうがコードが見通しやすくなると感じています。

オブジェクト指向原則

SOLID原則

概要

オブジェクト指向にはSOLID原則というものが存在します。 これは限りなくクラスを綺麗にバグなく書くことができるようにするために作られたルールのことです。 もちろんこのような原則がなくともオブジェクト指向を使用してコーディングすることはできますが、綺麗に書くならこれらの原則を意識したほうが良いです。 ただ、これらを常にすべて厳密に守らないといけないということでもありません。

ちなみにSOLIDとは五つの原則の頭文字をとったものです。

S:単一責任の原則

これはクラスが沢山の機能(責任)を持たせないようにするものです。 例えば、この記事の前半で書いたPlayerクラスがあったとします。

class Player:
	def __init__(self):
		self.playerHP = 100
		self.playerMana = 200

	def damegedPlayerHP():
		#プレイヤーのHPを減らす
	
	def healedPlayerHP():
		#プレイヤーのHPを回復する
	
	def attack():
		#プレイヤーが攻撃する処理
		
	def defence()
		#プレイヤーが防御する処理
	
	def useItem():
		#アイテムを使用する
	
	def escapeFromEnemy():
		#敵から逃げる
	
	def fire():
		#炎魔法
	
	def ice():
		#氷魔法

実はこのコードは、単一責任の原則に思いっきり反しています。 なぜかというとこのプレイヤークラスにはHP管理や攻撃処理、アイテムの処理、逃げる処理、果ては魔法の処理までこのクラスで行おうとしているからです。これらを解消するためにはそれらの処理を分離しなくてはいけません。

HPに関する処理を分離したいなら、それらに関する処理を分離します。

class HP:
	def __init__(self, hp):
		self.hp = hp
	
	def heal(self, healValue):
		self.hp += healValue

	def damaged(self, damageValue):
		self.hp -= damageValue

これはHP管理の単一の責任を持ったクラスになります。

O:オープン・クローズドの原則

この原則は機能の追加は簡単で、修正箇所は閉鎖的である、という原則です。 上の例での、魔法に関してこの原則を適用してみます。 ここは #lang/Kotlin で書きます。

interface Magic{
	val name: String,
	val consumedMana: Int,
	val damage: Int
}

class Fire extends Magic{
	override val name: String = "ファイア"
	override val  consumedMana: Int = 10
	override val damage: Int = 20
}

ここでは炎魔法を作成してみました。 interfaceやスーパークラスを使用することで変更が容易に、修正しても影響範囲が小さいコードになります。 仮に上のコードに氷魔法を追加する場合は簡単で、

class Ice extends Magic{
	override val name: String = "アイス"
	override val  consumedMana: Int = 4
	override val damage: Int = 9
}

といったように変更が容易であるのが原則です。 ダメージや名前を修正しても、他に影響する範囲は限りなく小さいです。

L:リスコフの置換原則

この原則は全て継承関係にあるクラスは同じに扱えるべきであるという原則です。

例えば、型キャストでよくasto型名とか、(double)なんて使用するかと思います。このようなプリミティブ型に使用しているならば、まだいいのですが、これを継承関係にあるクラスで使用してはいけないです。

#lang/Dart です。

class SuperClass{
	void printMsg(){
		print("super!");
	}
}

class SubClass extends SuperClass{
	
	void printMsg(){
		print("sub!");
	}
}

class SubSubClass extends SubClass{
	
	void printMsg(){
		print("subsub!");
	}
}

このようなコードが存在したとき、

void func(SuperClass superClass){
	var subClass = superClass as SubClass
}

と勝手にキャストしたり、

class SubClass2 extends SuperClass{
	
	void print(){
		print("subsub!");
	}
	
	void printSpecial(){
		print("special SUBSUB!")
	}
}

スーパークラスになく、サブクラスにしかないメソッドを追加したりなどすると、この原則に違反しているといえます。

ちなみにこのようなコードを書くとこのような結果になります。

void main() {
  SuperClass subClass1 = SubClass();
  subClass1.printMsg();//実行成功
  
  SuperClass subClass2 = SubClass2();
  subClass2.printSpecial();//実行失敗
}

SuperClassにはprintSpacialメソッドが実装されていないため、インスタンスがSubClass2だとしてもエラーが発生してこの原則に違反しているといえます。

I:インターフェース分離の原則

この原則はインターフェースや抽象クラスにおいて、それを継承したサブクラスにいらないメソッドを用意しないというものです。

#lang/Kotlin で書きます。 例えば、このような敵全般を司るEnemyクラスがあったとします。

interface Enemy{
	fun attack();
	fun jump();
	fun defence();
	fun heal();	
}

ここで敵たちのクラスを作成します。

class Slime() : Enemy {
	override fun attack(){ 
		//攻撃処理
	}
	override fun junp(){ 
		//ジャンプする処理
	}

	override fun defence(){}
	override fun heal(){}
}

class Zombie() : Enemy {
	override fun attack(){ 
		//攻撃処理
	}
	override fun defence(){ 
		//防御処理
	}

	override fun jump(){}
	override fun heal(){}
}

スライムには防御と回復をしてほしくないので実装しないことにしました。 ゾンビにはジャンプと回復をしてほしくないので実装しないことにしました。

しかし、このような継承を使用してしまうと、それぞれのクラスで使用しないはずのメソッドも実装しなければいけません。結果的に 、コードがとても汚く、見通しの悪いコードになってしまいます。

D:依存性逆転の原則

まずこの原則は、上位モジュールが下位モジュールに依存してはならないというものです。 具体的は以下のようなコードです。

class Animal{
	final String color;

	Animal(this.color);
	
	void printHinshu(){
		if(color == "白"){
			print("白猫です");
		}
		else if(color == "黒"){
			print("黒猫です");
		}
	}
}

class Cat extends Animal{
	Cat(String color): super(color);
}

このコードでは、上位モジュールであるAnimalクラスが、下位モジュールであるCatクラスに依存してしまっている状態です。 いちいち、Animalクラス側で条件分岐を使用して判定してしまっています。

というか、else ifswitchwhenなどは使用した段階で設計ミスを疑った方がいいです。例外として、未来永劫変わらないと断言できる事項については使用していいと思っています。曜日とか。(さらに、その曜日ごとの判定もswitchなどを使用せずとも、デザインパターンなどで対処できます。)

デザインパターン

私が良く使用するもののみ紹介します。全部で結構な種類がありますが。。。

大体、デザインパターンなんて、結局わかりやすく作ろうとして、最終的に他の人が見たら難解な場合が多いんですよ。あと結構無駄に複雑になるときも多いです。

真にオブジェクト指向を理解している人だけの特権ですね。私はほぼ使えません。

Singleton

Singleton(シングルトン)は、そのクラスのインスタンスが一つしか存在して欲しくない場合に使用します。さらに、そのインスタンスはメモリ的に静的なので、どこで呼び出しても単一のインスタンスが使用できます。

はい、みなさんお分かりですね。 これは使用しない方がいいです。なぜならいわゆるグローバル変数(=グローバルインスタンス)だからです。どこで値が変更されているかが分かりずらくなります。

しかし、このパターンを使用してもいい時があります。それはデータベースの操作クラスです。トランザクション中に他のトランザクションが起こるとダメなので使用することができます。

class DBController{
	DBController._();
	
	static DBController? _instance;
	
	factory DBController(){
		_instance ??= DBController._();
		return _instance;
	}
	
	void create(){ ... }
	void read(){ ... }
	void update(){ ... }
	void delete(){ ... }
}

Strategy

Strategy(ストラテジー)パターンは条件分岐を減らしたい場合に有効です。

このような場合、

class Creature{
	void voice(String animal){
		if(animal == "cat"){
			print("にゃー");
		}	
		else if(animal == "dog"){
			print("わん");
		}
	}	
	
	void move(String animal){
		if(animal == "cat"){
			print("ぴょん");
		}	
		else if(animal == "dog"){
			print("てくてく");
		}
	}	

	void eat(String animal){
		if(animal == "cat"){
			print("ぱくぱく");
		}	
		else if(animal == "dog"){
			print("ばくばく");
		}
	}	
}

void main(){
	var creature = Creature();

	creature.voice("cat");//にゃー
}

Creatureクラスで条件分岐して鳴き声を切り替えています。 これもほかの事例と同様にたくさんの動物を実装する場合はどうでしょうか。 動物を追加するたびにif文が増えて、コードがとても見づらくなってしまいます。

class Creature{
	void voice(String animal){
		if(animal == "cat"){
			print("にゃー");
		}	
		else if(animal == "dog"){
			print("わん");
		}
		else if(animal == "mouse"){
			print("ちゅー");
		}
		else if(animal == "lion"){
			print("がおー");
		}
		else if(animal == "frog"){
			print("けろけろ");
		}
		else if(animal == "niwatori"){
			print("こけこっこー");
		}
		else if(animal == "hukurou"){
			print("ほー");
		}
	}	

	//省略
}

これを解決するのがストラテジパターンです。 ストラテジパターンはifswitchを使用せず、interfaceなどを使用して処理の切り替えを一斉に行うものです。

まず以下のような動作を定義したインターフェースを用意します。

今回使用する言語(Dart)には、interface構文が実装されていないので、代わりに抽象クラスを使用します。

abstruct class AnimalInterface{
	void voice();
	void move();
	void eat();
}

そして、インターフェースを継承して各動物のクラスを作成します。

class Cat extends AnimalInterface {
	
	void voice() {
		print("にゃー");
	}

	
	void move() {
		print("ぴょん");
	}

	
	void eat() {
		print("ぱくぱく");
	}
}

class Dog extends AnimalInterface {
	
	void voice() {
		print("わん");
	}

	
	void move() {
		print("てくてく");
	}

	
	void eat() {
		print("ばくばく");
	}
}

//省略

ほかに動物を追加する際にも、このAnimalInterfaceクラスを継承することで簡単に追加することができます。

これらを使用するコンテキストクラスを用意します。

class Animal{
	void voice(AnimalInterface animal) {
		animal.voice();
	}

	void move() {
		animal.move();
	}

	void eat() {
		animal.eat();
	}
}

この時、各種動物はAnimalInterfaceクラスを継承しているので、引数には継承したクラスのインスタンスがすべて代入可となります。

これらを合わせて使用すると、

void main(){
	var animal = Animal();
	
	animal.voice(Cat())//にゃー
	animal.voice(Dog())//わん
}

といったように、ifswitchを使用せずとも処理を切り替えることができました。 これがストラテジパターンです。

参考文献

© 2024 ソフトウェア技術研究会