📖

オブジェクト指向とオブジェクト指向エクササイズ

2023/01/15に公開約5,000字

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

プログラム全体がオブジェクトだけで設計されます。オブジェクトが持つ状態と挙動のセットを定義し、設計図とインターフェースを使って抽象化し、オブジェクト間で状態と挙動のやりとりを行い、抽象化したものを再利用したり合成することで、コードの再利用性を高めることに焦点を当てています。

クラス

クラスの役割はインスタンスの作成と再利用の単位の2つがある

カプセル化

あらゆるものの詳細を隠蔽することをカプセル化という。詳細を隠すことにより、外部に公開されたインターフェイスだけを使ってプログラムすること。

継承

継承とは、一方のクラスが他方のクラスの既存の定義を使用して自分自身を定義できるようにする、2 つのクラス間の関係のことを指します。継承を使うことによって、以前に作成した他のクラスを再利用して新しいクラスを作成することができます。
継承では、データ構造の一般化を行い、ほとんどの場合、is-a 関係に従うことになります。

ポリモーフィズム

抽象クラスやインターフェイス経由でオブジェクトに指示すると、そのオブジェクトのクラスの実装に応じて異なった動作が行われること。多態性という。ポリモーフィズムは呼び出す側のロジックを一本化する仕組み。既存コードを修正することなく機能追加できる。
抽象に対してプログラムするので、具象に依存せず、クラス間のつながりを疎結合にでき、柔軟性が向上する

インターフェース

メソッドの実装を定義せずに、クラスが実装する必要があるメソッドを指定するための仕組み。インスタンスを生成とメソッドの実装は定義しない。メソッドの実装の定義を強制できる。
定数と抽象メソッドのみを定義できる。インターフェースを実装したクラスでは、すべてのメソッドの処理を定義する必要がある。インターフェースは複数実装できる。

オーバーロード

クラス内に同じ名前で引数の型や数が違うメソッドを複数定義することをオーバーロードという。
呼び出すメソッドは名前と引数の組み合わせで決めている。
メリットは、メソッド名が変わらないので使用する側が使いやすい。出力メソッドなどもオーバーロードされて提供されている。(オーバーロードが無い言語もある、Goは無い)

オーバーライド

サブクラスでスーパークラスのメソッドを再定義することをオーバーライドという。親クラスのメソッドを子クラスで使用するときに上書きして使用できる。条件として、メソッド名、引数の型と数、戻り値は親クラスと同じでないといけない。

抽象クラス

実装内容を持たないメソッドを抽象メソッドという。抽象メソッドを持つクラスを抽象クラスという。抽象クラスはインスタンス化はできない。

コンストラクタ

オブジェクトの初期化のために使われる特殊なメソッドをコンストラクタという。コンストラクタを戻り値を持たない。コンストラクタを定義していない場合は、自動でデフォルトコンストラクタ(引数・処理なし)が生成される。
オブジェクトの初期状態がどのように構築されるべきか(コンストラクタ)を定義します。

static

クラスを初期化した場合は、基本的には別々のインスタンスとなり、別々の値を持つことになる。しかし、各インスタンスで共有したい情報も出てくる可能性がある。その場合に、静的フィールド(static)を使用する。
メンバ変数がインスタンスごとではなく、クラスごとに用意される。なので、静的フィールドは、クラス変数と呼ばれる。

オブジェクト指向エクササイズ

オブジェクト指向のコツはいくつかの変数をまとめて名前をつけて抽象化することだと考えています。

メソッド内のインデントは1段まで

なぜ: 複雑すぎるメソッドは凝集度が低い
どうする:メソッドを分割する

// インデント1段なのでOK
func exampleFunction() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
}

// インデント2段なのでNG
func exampleFunction() {
	for i := 0; i < 10; i++ {
		if i%2 == 0 {
			fmt.Println(i)
		}
	}
}

// インデント2段なのでNGなので、2段目の処理を外に出して別の関数として定義する
func exampleFunction() {
	for i := 0; i < 10; i++ {
		ifFunction(i)
	}
}

func ifFunction(i int) {
	if i%2 == 0 {
		fmt.Println(i)
	}
}

else禁止

なぜ:複雑すぎるメソッドは凝集度が低い。if-elseは、拡張されるにつれて肥大化する
どうする:ガード節(ある条件を満たしていない時にリターンするか例外を投げる)早期リターン、ポリモーフィズムを使用する

// elseを使用してるのでNG
func exampleFunction(someCondition bool) {
	if someCondition {
		doSomething()
	} else {
		doSomethinsElse()
	}
}

// 早期リターンなどを使用してelse句を無くす
func exampleFunction(someCondition bool) {
	if someCondition {
		doSomething()
		return
	}
	doSomethinsElse()
}

プリミティブ型と文字列型はラップする

なぜ:ラップすることによって値に対して型を定義することで、コードの可読性を高め、変更に強く、拡張しやすくなる。値オブジェクトと呼ばれるテクニックで以下の特徴を持つ。イミュータブルオブジェクト、変更可能、振る舞いに副作用がない
どうする:格納するにようを表すクラスを抽出する

// プリミティブ型を指定してるのでNG
type User struct {
	name string
	age  int
}

// プリミティブ型をラップしてるのでOK
type User struct {
	name Name
	age  Age
}

type Age struct {
	value int
}

// このようにAgeクラスでバリデートして初期化することで、Userクラスが持つageに負の値が混入することがなくなる
func (a *Age) newAge(value int) *Age {
	if value < 0 {
		panic("マイナスの年齢はだめです!!")
	}
	return &Age{value: value}
}

type Name struct {
	value string
}

名前は省略しない

コードを他の開発者が読む時に、可読性に欠けるから。

1行につきドットは一つまで

なぜ: オブジェクトの内部構造が見える。依存するオブジェクトの種類が増える
どうする:委譲メソッドを作る

// メソッドチェインはドットが2つ以上はNG
firstName := user.getName().split("")[0]

1クラスは50行、1ディレクトリ10ファイルまで

なぜ: 大きいクラス、パッケージは、責務を過剰に負ってしまっているから
どうする:1つの責務に分割するべし

1つのクラスにつき、インスタンス変数は2つまで

なぜ: 多くのメンバ変数を持つクラスは凝集度が低い
どうする:クラスを分割する

// メンバ変数が2つを超えたのでNG
type User struct {
	name        Name
	phoneNumber PhoneNumber
	email       Email
}

// メンバ変数が2つを超えた場合は、分割する
type User struct {
	name    Name
	contact Contact
}

type Contact struct {
	phoneNumber PhoneNumber
	email       Email
}

ファーストクラスコレクションを使用する

なぜ: コレクション(リスト、マップなど)をラップしたクラスのこと。コレクションは、いわばプリミティブ型のようなもの。
どうする:コレクションをメンバ変数に持つクラスを作る

// NG
var books []Book = getBooks()

// OK
var bookShelf BookShelf = getBookShelf()

type BookShelf struct {
	books []Book
}

Getter,Setter,publicプロパティの禁止

なぜ: カプセル化が破られる。カプセル化したメソッドは、外部からの不当な攻撃を防ぐ必要があるので使用しない。
どうする:尋ねるな、命じろ!。情報をゲットして何かするのではなく、してほしいことを依頼する。Getterを呼びたくなったときは、getした後で何をするのかを考えて、その処理を呼び出し元に返してあげる。

// NG
var book Book = dao.findBook(bookId)
totalPrice := book.getPrice() * quantity

// OK
var book Book = dao.findBook(bookId)
totalPrice := book.calculateTotal(quantity)

OKとNGで何が変わるのか?
・Bookの内部情報がより隠される=>前者では少なくとも「本の単価」についてが外部に引き出されていました。しかし後者ではより本来の目的に近い「合計額」のみが引き出されています
・処理の重複が起きにくくなる=>このように情報の本来の持ち主がデータの加工を担当すると、あちらこちらで同じような加工処理が行われることがなくなります。つまり全体としてのコード量や複雑さが減り、メンテナンス性が向上します
・変更に強くなる=>1つ目や2つ目と強く関係しますが、合計額算出の具体的な処理の内容はBookの中に隠蔽されています。従って、算出方法が変わったとしてもBook#calcurateTotalが変更されるだけで済みます

「getter, setterを使うな」というのはより正しく言うならば、「そのgetter, setter呼び出しは呼び出される側への責務の割り当てに置換できないか検討しろ」ということになります
今回の内容を1行ずつにまとめると
・データの加工は、そのデータの持ち主がやるのがベスト。その結果、カプセル化が進み、コードの重複を排除できる
・カプセル化の目的は、変更による影響範囲を極力狭くすること

Tell, Don't Ask

「Tell, Don't Ask.」とは、オブジェクト指向プログラミングにおいて"良い"とされる考え方のひとつ。
日本語だと大体「求めるな、命じよ」と訳されることが多い。
もうちょっと具体的にすると、ある処理をする際、その処理に必要な情報をオブジェクトから引き出さないで、情報を持ったオブジェクトにその処理をさせろということ。

getterというのはまさにオブジェクトから情報を引き出すメソッドである。
つまり、あるクラスで他クラスのgetterを呼び出すような処理を実装している場合、その処理は本来呼び出されるクラス側で実装されるべきだということ。
setterも同様に、フィールドの中身を変えるような処理はそもそもそのフィールドをもつクラス内で完結させるべきである、という考え方。

Discussion

ログインするとコメントできます