はじめに
こんにちはやまねです。アクシスに入社して8ヶ月が経ちました。毎日戸惑いながらも働いています。
今回は8ヶ月前にあまり理解できなかったポリモーフィズムについて取り上げたいと思います。
ポリモーフィズム
ポリモーフィズムとはオブジェクト指向プログラミングの概念の1つであり、日本語で多態性
、多様性
と訳されます。
シンタックスシュガーが糖衣構文
と訳されるくらいぱっと見よく分かりにくいですが、オブジェクト指向の文脈で言うと、
同じ名前のメソッドを複数のクラスで使用でき、かつ異なる振る舞いを実現することができる仕組み
だと説明されます。
ただ、これには二つ疑問点がありました。
- それより以前のプログラミングにはポリモーフィズムのような仕組みは存在しなかったのか?
- ポリモーフィズムって結局何が利点なのか?
です。
オブジェクト指向プログラミング以前にはポリモーフィズムのような仕組みは存在しなかったのか?
調べたところポリモーフィズム自体がオブジェクト指向以前のプログラムになかったのかと問われれば、あったようです。(仕組みとしてはないみたい)
例えばこの記事では、C言語でポリモーフィズムを実現しようとしています。
しかしこれにはデメリットがあります。この場合、関数ポインタ
に頼らなくてはいけません。
関数ポインタに依ることの危険性はこの本では以下のように述べられています。
関数へのポインタを明示的に使用して、ポリモーフィズムの振る舞いを生み出す時の問題は、関数へのポインタが危険であることだ。
ポインタを初期化するときは「ポインタを経由して関数を呼び出す」という規則を覚えて置く必要がある。プログラマが規則を覚えておかないと、バグの追跡と排除が相当難しくなるだろう。
オブジェクト指向プログラミングは、そのような規則を排除し、また関数ポインタを使用することでポリモーフィズムを実現することへの危険性を回避したものであると捉えることができます。
ただ、なぜバグの温床になりうるポリモーフィズムを生み出したかったのかがいまいち掴めませんでしたが、
後述するように、ポリフォーフィズムは、依存関係と制御の流れに大きな影響
を及ぼしているからです。
依存関係の逆転
典型的なプログラムでは、main関数が上位の関数を呼び出し、その上記の関数が中間の関数を呼び出し、そして中間の関数が下位の関数を呼び出すような構造を取っていたみたいです。
そうなると、ソースコードの依存関係と制御の流れが同じになります。ここで述べているソースコードの依存関係とは、c言語であれば、#include
を記述することを指します。
この場合、下位の部分の変更がその上位に波及し、変更する箇所が多くなります。
しかし、ポリモーフィズムを使用すると、この構造とは異なる構造になります。
ここでは、インタフェース経由で関数を呼び出しています。Aはインタフェースには依存をしますが、制御の流れは別の方向に向いています。
ポリモーフィズムの大きな利点は、この依存関係と制御の流れを別にすることで全てのソースコードの依存関係を絶対的に制御することが可能
になることです。
そして、この依存関係を安全に制御できるという特徴は、「依存関係の逆転の原則」
として原則づけられています。SOLID原則のDの部分です。
依存関係逆転の原則(DIP)
上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。
実際に使ってみる
go言語で、ポリモーフィズムに触れてみたいと思います。go言語はオブジェクト指向プログラミングではありませんが(話を流れをぶったぎってすみません)、ポリモーフィズムの仕組みを提供してくれています。
- version : go1.15
この記事を参考に、APIサーバーを立て、CRUD
を行う想定で実装していきます。
まず、ルートから入ってきたリクエストはcontroller
側で処理されます。
package controller
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/qiita/usecase"
)
type UserController struct {
usecase usecase.UserInteractor
}
// 初期化関数
func NewUserController(usecase usecase.UserInteractor) UserController {
return UserController{
usecase: usecase,
}
}
func (uc *UserController) GetUsers(w http.ResponseWriter, r *http.Request) {
users := uc.usecase.GetAll()
json.NewEncoder(w).Encode(users)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (uc *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
// クエリストリングを受け取る
vars := mux.Vars(r)
users := uc.usecase.Get(vars["id"])
json.NewEncoder(w).Encode(users)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (uc *UserController) PostUser(w http.ResponseWriter, r *http.Request) {
age, _ := strconv.Atoi(r.PostFormValue("age"))
name := r.PostFormValue("name")
users := uc.usecase.Create(age, name)
json.NewEncoder(w).Encode(users)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
}
func (uc *UserController) UpdateUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
age, _ := strconv.Atoi(r.PostFormValue("age"))
name := r.PostFormValue("name")
users := uc.usecase.Edit(vars["id"], age, name)
json.NewEncoder(w).Encode(users)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (uc *UserController) DeleteUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
uc.usecase.Delete(vars["id"])
w.WriteHeader(http.StatusNoContent)
}
次にcontroller
はusecase
に処理を移します。
package usecase
import (
"github.com/qiita/domain"
)
type UserInteractor struct {
// ここでインターフェスを呼び出す
userRepository domain.UserRepository
}
// 初期化関数
func NewUserInteractor(userRepository domain.UserRepository) UserInteractor {
return UserInteractor{
userRepository: userRepository,
}
}
func (ui *UserInteractor) GetAll() []domain.User {
users := ui.userRepository.GetUsers()
return users
}
func (ui *UserInteractor) Get(id string) []domain.User {
users := ui.userRepository.GetUser(id)
return users
}
func (ui *UserInteractor) Create(age int, name string) []domain.User {
users := ui.userRepository.CreateUser(age, name)
return users
}
func (ui *UserInteractor) Edit(id string, age int, name string) []domain.User {
users := ui.userRepository.EditUser(id, age, name)
return users
}
func (ui *UserInteractor) Delete(id string) {
ui.userRepository.DeleteUser(id)
}
Usecase
はRepository
を介してデータの永続化を行いますが、その際に依存関係を制御します。
実際には、Usecase
はRepository
のインターフェースに依存します。
package domain
type UserRepository interface {
GetUsers() []User
GetUser(id string) []User
CreateUser(age int, name string) []User
EditUser(id string, age int, name string) []User
DeleteUser(id string)
}
しかし実際の制御は、infrastructure/user_repository.go
を呼び出しています。
package infrastructure
import (
"github.com/qiita/domain"
"github.com/qiita/infrastructure/database"
)
type UserRepository struct {
db database.DbHandler
}
func NewUserRepository(db database.DbHandler) UserRepository {
return UserRepository{
db: db,
}
}
var users []domain.User
func (ur UserRepository) GetUsers() []domain.User {
db := ur.db.ConnectDB()
db.Find(&users)
return users
}
func (ur UserRepository) GetUser(id string) []domain.User {
db := ur.db.ConnectDB()
db.Where("id = ?", id).Find(&users)
return users
}
func (ur UserRepository) CreateUser(age int, name string) []domain.User {
db := ur.db.ConnectDB()
db.Create(&domain.User{Name: name, Age: age})
return users
}
func (ur UserRepository) EditUser(id string, age int, name string) []domain.User {
db := ur.db.ConnectDB()
db.Model(&users).Where("id = ?", id).Update("name", name)
db.Model(&users).Where("id = ?", id).Update("age", age)
return users
}
func (ur UserRepository) DeleteUser(id string) {
db := ur.db.ConnectDB()
db.Where("id = ?", id).Delete(&users)
}
上記のように、レイヤー毎に依存したくない、けど制御したいというときにポリモーフィズムは活躍するのだと思いました。
まとめ
最後の方は駆け足になりました。すみません。🙇♂️
全てを理解したわけではもちろんありませんが、日々過ごしている中で少しでも新たな発見があることは、とても素晴らしいことだと思います。go言語にも興味を持ったので、もう少し業務が早く終わらせられるようになったら、きちんと触れてみたいです。
参考
Clean Architecture 達人に学ぶソフトウェアの構造と設計
Clean ArchitectureでAPI Serverを構築してみる
C言語でポリモーフィズム