継承・カプセル化・ポリモーフィズムはオブジェクト指向の3大要素と呼ばれています。そのうち今回の記事で扱うポリモーフィズムは日本語では多様性などと訳されますが、どのような概念なのかがいまいち分かりずらく、実装例を見ても何の役に立つのか自分には長らくピンと来ませんでした。
本記事では、ポリモーフィズムとは何か、何が便利なのかをGo言語での実装例と共に紹介し、最後にはSOLID原則の一つであるOCPにも触れたいと思います。
##ポリモーフィズムとは?
ポリモーフィズムを一言で言うと、**「同じインターフェイスを操作しても、それを実装するインスタンスによって違う動きをさせる仕組み」**であると自分は理解しています。
オブジェクト指向の本として有名な「オブジェクト指向でなぜ作るのか」では
類似したクラスに対するメッセージの送り方を共通化する仕組み
と説明されています。Javaで言うと、同じクラスのメソッドを呼び出しても、そのメソッドを持つ親クラスを継承する子クラスによって実際の動きは異なってくるというところでしょうか。ポリモーフィズムはメソッドの呼び出し側で楽をするための仕掛けです。
※言葉で聞いただけでピンと来なくても、実装例を見ればポリモーフィズムの良さがしっかりと分かるのでまずは読み進めて、この記事の最後の実装例を読み終わったらまた戻って読んでみてください。最初はよく分からなかった説明も、すんなりと理解できると思います。
##ポリモーフィズムの良さ
ここまででポリモーフィズムという仕組みについて言葉で説明してきました。言葉でどういうものか分かったら、次はその利用価値まで理解しておきましょう。
よくポリモーフィズムのメリットとして、ifやswitch文による条件分岐が消えるということが挙げられます。ポリモーフィズムによって消すことができるifやswitch文とは、どのクラスのインスタンスなのかを判断するために使われるものです。以下に実装例を示します。
type worker struct {
age, workingYear, baseSalary int
performance int
company string
}
func main() {
taro := worker{
age: 33,
workingYear: 10,
baseSalary: 250000,
performance: 80,
company: "toyota",
}
hanako := worker{
age: 28,
workingYear: 5,
baseSalary: 100000,
performance: 190,
company: "google",
}
ichiro := worker{
age: 40,
workingYear: 15,
baseSalary: 300000,
performance: 130,
company: "sony",
}
workers := []worker{taro, hanako, ichiro}
fmt.Printf("Total income: %d\n", calculateIncome(workers))
}
func calculateIncome(workers []worker) int {
sum := 0
for _, worker := range workers {
switch worker.company {
case "toyota":
sum += worker.baseSalary + (1100 + worker.performance) + (worker.workingYear * 10)
case "google":
sum += worker.baseSalary + (1000 * worker.performance)
case "sony":
sum += worker.baseSalary + (10 * worker.performance) + (worker.workingYear * 100)
default:
sum += 0
}
}
return sum
}
このように、呼び出す側でswitch文による条件分岐が発生してしまいました。この例では3つだけですが、これがどんどん増えていくと複雑で長いコードになってしまうためあまり良いとは言えません。これをポリモーフィズムを利用して、インスタンスによって違う動きをさせることですっきりとさせてみましょう。
##ポリモーフィズムのGo言語での実装例
上で示したコードをポリモーフィズムを使って改良すると、以下のようになるでしょう。
type income interface {
calculate() int
}
type toyota struct {
age, workingYear, baseSalary int
performance int
}
type google struct {
age, workingYear, baseSalary int
performance int
}
type sony struct {
age, workingYear, baseSalary int
performance int
}
func (t toyota) calculate() int {
return t.baseSalary + (1100 + t.performance) + (t.workingYear * 10)
}
func (g google) calculate() int {
return g.baseSalary + (1000 * g.performance)
}
func (s sony) calculate() int {
return s.baseSalary + (10 * s.performance) + (s.workingYear * 100)
}
func main() {
taro := toyota{
age: 33,
workingYear: 10,
baseSalary: 250000,
performance: 80,
}
hanako := google{
age: 28,
workingYear: 5,
baseSalary: 100000,
performance: 190,
}
ichiro := sony{
age: 40,
workingYear: 15,
baseSalary: 300000,
performance: 130,
}
workers := []income{taro, hanako, ichiro}
fmt.Printf("Total income: %d\n", calculateIncome(workers))
}
func calculateIncome(ic []income) int {
sum := 0
for _, worker := range ic {
sum += worker.calculate()
}
return sum
}
calculateIncome
メソッドからswitchが消え去り、とてもすっきりしています。冒頭で述べた通り、ポリモーフィズを使用したことで同じインターフェイス(ここではincomeインターフェース)を操作しても、それを実装するインスタンス(taro, hanako, ichiro)によって違う動き(calculate
メソッドによる会社ごとの給料計算方法)をさせることができており、類似したクラス(Goでは構造体)に対するメッセージの送り方を共通化することができています。
冒頭で述べた、
- 同じインターフェイスを操作しても、それを実装するインスタンスによって違う動きをさせる仕組み
- 類似したクラスに対するメッセージの送り方を共通化する仕組み
- ポリモーフィズムはメソッドの呼び出し側で楽をするための仕掛け
という文章の意味がより明確に理解できるようになったのではないかと思います。
incomeというインターフェースを定義し、そのincomeインターフェースを満たすようにcalculateメソッドをそれぞれの型に定義し、同じインターフェース型としてまとめて扱えるようにしているところがポイントです。
##変更を加えるときにも、ポリモーフィズムが役に立つ
ではここで、トヨタ・Google・SONYに加え、Yahooの社員の給料を計算する必要が生じ、コードに変更を加える必要が出てきた時のことを考えてみましょう。
type income interface {
calculate() int
}
type toyota struct {
workingYear, baseSalary, performance int
}
type google struct {
workingYear, baseSalary, performance int
}
type sony struct {
workingYear, baseSalary, profit int
}
type yahoo struct {
age, workingYear, baseSalary int
}
func (t toyota) calculate() int {
return t.baseSalary + (1100 + t.performance) + (t.workingYear * 10)
}
func (g google) calculate() int {
return g.baseSalary + (1000 * g.performance)
}
func (s sony) calculate() int {
return s.baseSalary + (500 * s.profit) + (s.workingYear * 100)
}
func (y yahoo) calculate() int {
return y.baseSalary + (20000 * y.workingYear)
}
func main() {
taro := toyota{
workingYear: 10,
baseSalary: 250000,
performance: 80,
}
hanako := google{
workingYear: 5,
baseSalary: 100000,
performance: 190,
}
ichiro := sony{
workingYear: 15,
baseSalary: 300000,
profit: 100,
}
motoko := yahoo{
baseSalary: 40000,
workingYear: 25,
}
workers := []income{taro, hanako, ichiro, motoko}
fmt.Printf("Total income: %d\n", calculateIncome(workers))
}
func calculateIncome(ic []income) int {
sum := 0
for _, worker := range ic {
sum += worker.calculate()
}
return sum
}
ポリモーフィズムを利用しているおかげで、calculateIncome
メソッドを改修する必要がないことに注目してください。
※もしポリモーフィズムを利用していなかったら、また新しくswitch文にcaseを足さなければならなくなってしまい、switch文が更に長くなってしまいます。
新しくyahooの型と給料計算メソッドを定義し、income型のスライスに追加するだけで改修がすみました。これならいくら追加があってもcalculateIncomeメソッドは肥大せずにいつでもすっきりとさせておくことができます。
##オープンクローズドの原則
このように、変更が発生した場合に既存のコードには手を加えず、新しく変更用のコードを加えるだけで対応できている時、そのプログラムはオープンクローズドの原則*(OCP : The Open Closed Principle)*を満たしていると言います。これはSOLID原則というオブジェクト指向原則のOの部分にあたるものです。
ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張のために開いていて、修正のために閉じていなければならない。
calculateIncome
メソッドは、修正をすることなく(修正のために閉じていて)新しい会社の社員の給料計算という変更(拡張)に対応することができている(拡張に開いている)ため、まさにオープンクローズドの原則を満たしていると言えます。
本記事で扱ったコードはこちらに載せてあります。
やたらと長いif文やswitch文が散見されるコードを担当しているならば、ポリモーフィズムを利用したリファクタリングを行ってみるのも良いでしょう。
それでは、最後までお読みいただきありがとうございました。