🦆

ちょうぜつソフトウェア設計入門 三章

2023/03/11に公開

オブジェクト指向プログラミング

クーリンアーキテクチャを実現する際に、抽象の扱いが重要になってくる

その抽象を扱うにあたり便利なのがオブジェクト指向プログラミング(OOP)

OOPは明確な定義がない

「ものが物体として存在するイメージでプログラミングをうまくやる」

これ以上のことは何も言っていない。

モデリングをするので現実をそのまま表したものでもない。

classというプログラミングの文法でもない。

状態(関数の呼び出しの度に結果を変えてしまうもの)管理とは関係ない

バイト列のやりとりというプログッラミングの実態に、人が認識しやすいものの概念を与え理解しやすくできないか?という試みをOOPと呼んでいる

OOPのメリットは3つある

OOPのメリット1 カプセル化

関連度の高い知識を一つのオブジェクトにまとめること

関連知識達は互いに作用したり、制御条件があったりする。

こう言ったものをまとめることで、オブジェクトという認識単位を得ることができる。

たとえば車を例に取ると

class Car(
	val body: Body,
	val engine: Engine,
	val wheels: List<Wheel>
) {
	public fun startEngine() {
		this.enginge.start()
	}

	public fun adjustHandle() {
		this.wheels[0].adjust()
		this.wheels[1].adjust()
}

val car = new Car(body, engine, wheels)
car.startEngine()
car.adjustHandle()

のように車を構成するパーツをまとめ、それらの作用や条件などにも意味を付与することができた

(やはりKotlinはボイラープレートが少なくて素晴らしい✨

このようにOOPを用いることで、全再利用の原則や閉鎖性共通の原則に沿うことができる。(凝縮度を高めることができる)

詳細の隠蔽

また、カプセル化されたオブジェクトは詳細を隠蔽することができる。

車を修理するとき。運転するときで意識すべき粒度や観点は変わってくる。

エンストした際などであれば、エンジンの各パーツが正しく動作しているか意識する

しかし、運転するときには鍵を回してエンジンを動かすくらいの認知(エンジンを構成するパーツまで意識することはない)

関心に沿った形でカプセル化が行われていれば、カプセル化されたオブジェクトは関心を分離する抽象度の境界線となる。

(車の運転の場面ではエンジンの中身までは意識しないから、エンジンインターフェースを満たすカプセル化された何かを動かすとだけ表現する)

責務の委譲

自動車はエンジンにエンジン内部の処理を委譲している

責務を委譲したことにより自動車は、エンジンを動かしハンドルを調整するという自身の責務に集中できる。

そしてエンジンも車体やハンドルといったことは意識せず、エネルギーを効率よく生み出し供給するという自身の責務だけに集中ができる

このように良いプログラミングは適切な境界線が引かれる

(データ移行などで取り込み処理にCSVのパース処理が載っている。これは適切な境界線が引かれていない例)

デメテルの法則

カプセル化した内部をほじくるな、という法則

似た格言として「Tell, Don’t Ask.」(使用者は使用対象に目的を伝えよ。使用対象にいちいち内部プロパティのことを聞くな)というものがある

要するに、カプセル化で関心を分離したのにそのオブジェクトを掘り返さなければならない実装になっていたら密結合になってしまう。(アーキテクチャの意味をなさない)

// 掘り返している実装 NG
class Driver {
	public fun drive() {
			car.getEngine().start()
	}
}

このNGのケースからも分かるように、getterで得たプロパティのメソッドを呼び出すことは避けるべき

これを戒めるために、デメテルの法則はメソッドチェーンを禁じている

いちいちドライバーが車に「エンジンってある?あれを動かして欲しい」などと掘り返さなくてよくなるように設計せよ。とデメテルの法則は読み替えられる。よって上記のコードは下のような形になる。

// 掘り返していない実装 OK
class Driver {
	public fun drive() {
			car.startEngine()
	}
}

メソッドチェーンでも良いパターン

抽象度が同じレベルのオブジェクトを返すのであれば問題ない。(ほじくっていないから)

//OKな例
class HTTPResponse {
	fun withStatus(status: Int, message: String) {
		this.status = status
		this.message = message
		return this
	}

	fun withHeader(mediaType, MIMEType) {
		this.mediaType = mediaType
		this.MIMEType = MIMEType
		return this
	}
}

val baseResponse = HTTPResponse()
val errorResponse = baseResponse
	.withStatus(404, 'Not Found')
	.withHeader('Content-Type', 'application/json')

OOPのメリット2  ポリモーフィズム

使い手からは同じもののように見える。ただ、実は中身が異なる実態が数多くある。

この構造のことをポリモーフィズムと呼ぶ。

オートマの免許を持っていればオートマ車ならなんでも乗れるようなもの

下記はペットとしてのがわ(インターフェース)を満たしていれば、置き換えることが可能というサンプル

interface Pet {
	fun reaction() 
}

class PetshopCustomer {
	fun touch(pet: Pet) {
		pet.reaction()
	}
}

class Dog: Pet {
	override fun reaction() {
		print('ワン')
	}
}

class Cat: Pet {
	override fun reaction() {
		print('ニャン')
	}
}

val customer = PetshopCustomer();
customer.touch(Dog())
customer.touch(Cat())

また、customerが直接各ペットという具象に依存していないので安定度・抽象度等価の原則にも反しない。

ポリモーフィズムのメリット

  1. 条件分岐を減らし、安定度を高めることができる。

同じようなif文がプログラミング中に散らばっていたら、改修漏れも発生して不便。見通しも悪い。

上の例だったら新しいペットが追加されたり、各ペットの振る舞いが増える毎にプログラム中の箇所を修正しなければならなくなる

2.Tell, Don’t Askを守れる

// NGコード
class Petshop(val app: App, val bool: withoutLogging = false) {
private fun paycheck() {
		if(!withoutLogging) {
			app.getLogger().log('begin')// カプセル化したオブジェクトを掘り返している❌
		}
		// 支払いのトランザクションなど
		if(!withoutLogging) {
			app.getLogger().log('end')
		}
	}

これはフラグを用いて処理を変更させたサンプル。

これだとデメテルの法則に違反しており、密結合も起こしてしまっている
ポリモーフィズムを活用すれば下記のように改善できる

// 改善後のコード
class Petshop(val logger: LoggerInterface) {
	private fun paycheck() {
		this.logger.log('start')
		// 支払いのトランザクションなど
		this.logger.log('end')
	}
}

class MessageLogger: LoggerInterface {
	override fun log(message: String) {
		print(message)
	}
}

class NullLogger: LoggerInterface {
	override fun log(message: String) {}
}

// ログをオフに
val shop = PetShop(NullLogger());
//ログをオンに
// val shop = PetShop(MessageLogger());

このように機能のオンオフを管理したかったら、その関心のオブジェクトによって振る舞いを切り替えれば良い。

ポリモーフィズムで行われたクラスの派生
Pet → Dog, Cat

OOPのメリット3  継承(実装)/汎化

継承の主たる旨味は、共通処理を基底クラスに実装できる(差分プログラミング)ということではない。

概念の一般化と、その実装の分離ができるのが醍醐味

(OOPのこの醍醐味から考えると、抽象クラスよりもインターフェースを用いた方がよさそう)

先ほどのPetクラスのサンプルコードを思い出してほしい。

あのコードにはPetインターフェースを実装した、Dog, Catクラスがあった

このPetとDog, Catの関係がis-aの関係である

DogもCatもPetの条件(インターフェース)を満たしているからPetとして問題ないわけだ

これは、PetインターフェースがDog, Catという具体的な実装(多態)に共通している概念を抽出し一般化したといえる

具体から抽象を作りだされ、その抽象に依存することで安定度を高めることができるということだ

たとえ具体が一つしかなかったとしても、依存先が抽象になることで安定依存の原則にも従うことができる。

概念の抽出のよって行われたクラスの派生
Dog, Cat → Pet

汎化

この具体から抽象を抽出していく作業を汎化と呼ぶ。

良い抽象を作るには、顧客や利用者にヒアリングを行い無限にある可能性を絞っていくことが必要

万能の抽象を作ろうするとかえって、拡張性は落ちてしまう。

ペットショップのシステムを開発しているのに、野生動物も想定したAnimalインターフェースは無駄。

そもそもシステムの用途に則していない

(もっと細かい例や話も書籍に載っているので、気になる方は是非買ってみよう)

オブジェクト指向プログラミング ≠ 構造化プログラミング

OOPはこれだ!、とはいえないが構造化プログラミングではないとは言える

💡 構造化プログラミング = go toを避けifやforなどのブロックを用いるもの。
内外を構造で区切ることで、無関係なブロック階層から切り離すことができる。

構造化とは

ブロックを用いることで区切ることを構造化と呼ぶ。

意味合いの強い変数をまとめて、意味のある名前をつけることも広義の構造化と呼べる。

そしてこういった構造化は良い意味でのブラックボックス化といえる。

名前が適切なものがついていれば、中の実装を気にしなくて済む。

抽象は構造化プログラミングとOOPの違いではない

そしてこのブラックボックス化された構造のことを抽象と呼んでいた。(OOP登場以前から抽象という概念は存在していた)そしてこの抽象にユニークな型名をつけたものを抽象データ型と呼ぶ。

→具体を隠蔽し抽象を利用させるという発想自体は、OOP独自のものではない。

(この話はClean Architectureのはじめの方にも記載があった)

ただ、OOPにおける抽象とイコールのものではなく実態はOOPの具象

そのため下記の問題が起きる

構造化プログラミングの問題点

複雑な機能を作る時に、機能分解という手法が構造化プログラミングでは用いられる

💡 機能分解・・・複雑な全体を機能ブロックごとに分割し、それを積み上げる形で組むこと

この機能分解のデメリットが構造化プログラミングの問題点である

そのデメリットとは、上位構造は下位構造に依存してしまう(=一つの大きな責務を上位構造と下位構造で共有してしまう)

構造化プログラミングには抽象の概念はあったが、実態は具象なのでこのような問題が生じる。

このようなデメリットがあったのでは、下位構造のどこかに問題があった場合上位構造も壊れてしまい改修の難易度はグッと高まってしまう

OOPと構造化プログラミングの違い

違いは抽象という発想ではなく、上位構造が下位構造の正しさに関係なく独立して存在できること。

OOPを利用して目指す状態のおさらい

閉じたパッケージの中では、密な上下関係がつくれていて(閉鎖性共通の原則)パッケージの独立性は担保されている状態

Discussion