はじめに
業務でLaravelを使ってMVCでアプリケーションを作成したことはありますが、どうやらGoは「Clean Architecture(クリーンアーキテクチャ)」で開発するのが主流なようです。
今回はコードを書いたりAIに聞いたりしながらGoとクリーンアーキテクチャについて学んでみました!
思考整理のためのアウトプットですので、情報の正確性などは保証できません!ご容赦ください
インターフェース指向
クリーンアーキテクチャで重要になってくるのがインターフェースです。
ChatGPTによると、Goは「インターフェース指向」の言語と言われているようです。
オブジェクト指向はよく耳にしますが、インターフェース指向という言葉もあるんですね。
インターフェースという概念はどの言語にも存在すると思いますが、GoのインターフェースはPHPなどのそれとは多少異なるらしく、わかりやすくChatGPTに教えてもらいました。
例えばJavaやC#では、あるクラスがインターフェースを「implements」や「: InterfaceName」として明示的に実装する必要があります。
でもGoでは…
対応するメソッドを「たまたま」持っていれば、そのインターフェースを満たしているとGoは判断します。
うーん、よくわかりません笑
ということで、いったんmain.goにインターフェースを含むコードを書いてみて、インターフェースについてわかってきたらクリーンアーキテクチャについても深掘りしていこうと思います。
Goのインターフェースを理解する
例としてAnimalインターフェースを定義してみます。
最初にコード全体を貼っておきます。
package main
import (
"fmt"
)
// インターフェースの定義
type Animal interface {
Speak() string
}
type Dog struct {
Name string
}
// Dog に Speak() メソッドを実装(インターフェースを満たす)
func (d Dog) Speak() string {
return d.Name + " says: わん!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return c.Name + " says: にゃー!"
}
// Animal インターフェースを受け取る関数
func MakeItSpeak(a Animal) {
fmt.Println(a.Speak())
}
func main() {
dog := Dog{Name: "ポチ"}
cat := Cat{Name: "ミケ"}
MakeItSpeak(dog)
MakeItSpeak(cat)
}
ターミナルでgo run main.go
を実行することで、ポチsays:わん! ミケsays:にゃー!
が出力されます。
では順番にコードを追っていきましょう。
type Animal interface {
Speak() string
}
type Dog struct {
Name string
}
type Cat struct {
Name string
}
Animalインターフェースでは「Speak()」という、戻り値の型が文字列のメソッドを定義しています。
また、 Dog ・ Cat それぞれの構造体が定義されています。
func (d Dog) Speak() string {
return d.Name + " says: わん!"
}
func (c Cat) Speak() string {
return c.Name + " says: にゃー!"
}
次に Dog・Cat 構造体をレシーバーとして、自分の鳴き声を返すSpeak()メソッドを実装します。
Goでは、構造体がSpeak()メソッドを持っていれば、それだけでAnimalインターフェースを満たしている、と暗黙的に判断されます。
この実装は構造体とインターフェースの間に明示的な結びつきを持たないため、疎結合な設計を自然に実現できるようです。
レシーバーについて補足
「値レシーバー」と「ポインタレシーバー」仮にメソッド内の処理として、構造体のフィールド(他の言語でいうクラスのメンバー・プロパティ)の値を更新したい場合
func (*d Dog) Speak() string {}
のように構造体の変数の前にアスタリスクを記述します。
こうすると、メソッド内でフィールドを変更すると参照元の構造体にもその変更が反映されます!
アスタリスクが付く方はポインタレシーバー、付かない方は値レシーバーと言います。
func MakeItSpeak(a Animal) {
fmt.Println(a.Speak())
}
そして、MakeItSpeak()関数は引数にAnimal型をとっており、DogやCatはSpeak()メソッドを実装しているため、Animalインターフェースを満たしているとみなされます。
※この後の章で触れますが、他の言語のように明示的に「Animal型を実装します」という宣言が不要なのがGoの特徴です。(例:implementsキーワードなど)
そのため、上記の説明も満たしていると"みなされる"というニュアンスになります。
より厳密にいうとGoのコンパイラが自動で判断する、ということです。
そのため、Animal型の引数をとるMakeitSpeak()関数に渡すことができるということです。
func main() {
dog := Dog{Name: "ポチ"}
cat := Cat{Name: "ミケ"}
MakeItSpeak(dog)
MakeItSpeak(cat)
}
ということで、最終的にmain関数内でMakeItSpeak()に Dog・Cat(=Animalインターフェース)を渡して実行することで、それぞれの動物の鳴き声が出力されました!
PHPでAnimalインターフェースを書いてみる
試しにPHP(オブジェクト指向言語)でAnimalインターフェースがどのようなコードになるか比較すると、よりGo言語の特徴がわかりやすいかと思ったので、AIにお願いして書いてもらいました。
<?php
interface Animal {
public function speak(): string;
}
class Dog implements Animal {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function speak(): string {
return $this->name . " says: わん!";
}
}
class Cat implements Animal {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function speak(): string {
return $this->name . " says: にゃー!";
}
}
function makeItSpeak(Animal $animal) {
echo $animal->speak() . PHP_EOL;
}
$dog = new Dog("ポチ");
$cat = new Cat("ミケ");
makeItSpeak($dog);
makeItSpeak($cat);
こうして見比べてみると、割と異なる点が多いように見えます。
PHPではimplements
というキーワードを使ってインターフェースを明示的に実装する必要があります。
class Dog implements Animal {...}
class Cat implements Animal {...}
また、Goにはコンストラクタ関数が用意されていないため、dog := Dog{Name: "ポチ"}
のように直接初期化する必要がありますが、$this変数を用いて__constructor関数で初期化しています。
public function __construct(string $name) {
$this->name = $name;
}
また、メソッドについてですが
Go
・・・・構造体が持つ機能・振る舞い
PHP
・・・クラスが持つ機能・振る舞い
このような捉え方をすると同じもののように思えますが、実際には少し意味合いが異なるようです。
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " says: わん!"
}
Dogという構造体にSpeak()という振る舞いを「結びつけている」
class Dog {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function speak(): string {
return $this->name . " says: わん!";
}
}
Dogというクラスにspeak()という振る舞いを「定義している」
仮に犬・猫に続いて「猿」を追加するケースを考えてみます。
猿は鳴きますし、噛み付くし、木に登ります。
この特徴の中で、鳴く・噛み付くは犬にも猫にも当てはまりますよね。
その場合、必要に応じてそれらの振る舞い=メソッドを Dog・Cat 構造体に結びつけるイメージでしょうか。
クラスにメソッドを定義するのとはちょっと感覚が違いますよね。
type Speaker interface {
Speak() string // 鳴く
}
type Biter interface {
Bite() string // 噛み付く
}
type Climber interface {
Climb() string // 木に登る
}
猿
type Monkey struct {
Name string
}
func (m Monkey) Speak() string {
return m.Name + " says: ウキキー!"
}
func (m Monkey) Bite() string {
return m.Name + " が噛みついた!"
}
func (m Monkey) Climb() string {
return m.Name + " が木に登った!"
}
犬
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " says: わん!"
}
func (d Dog) Bite() string {
return d.Name + " が噛みついた!"
}
Goのインターフェースの理解を深める
また、異なる例でPHPとGoのインターフェースの比較ができるコードを出してもらいました。
以下のPHPのコードは「渡された動物が Speaker や Climber を実装してるかチェックして、できる行動だけ実行する」という判定をしています。
function act($animal) {
if ($animal instanceof Speaker) {
echo $animal->speak();
}
if ($animal instanceof Climber) {
echo $animal->climb();
}
}
ポイントは、instanceof Speaker を使うには、そのクラスがあらかじめ implements Speaker を書いておかないとダメという点です。
interface Speaker {
public function speak(): string;
}
class Dog implements Speaker {
public function speak(): string {
return "わん!";
}
}
一方、Goはインターフェースを実装する際の宣言が不要です。
以下のコードで Dog に「Speaker を実装する」とは一言も書いていませんが、Act() に Dog を渡すと、Dog は Speak() メソッドを持っているので Speaker インターフェースを満たしている(とGoが判断する)。
よって、a.(Speaker) の型アサーションも true になり、問題なく動作します。
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "ワンワン!"
}
func Act(a interface{}) {
if s, ok := a.(Speaker); ok {
fmt.Println(s.Speak())
}
if c, ok := a.(Climber); ok {
fmt.Println(c.Climb())
}
}
つまり、型アサーションで引数の値を調べるときに、インターフェースを実装しているか知る必要はないということになります。
確かにこれは「疎結合」な設計だな、ということがわかりました。
おわりに
だいぶ内容がGoのインターフェースに関する話に偏ってしまいました。
ですがGoでクリーンアーキテクチャを使って開発するならインターフェースは重要な要素になるので、少しボリュームを割いて理解に努めました。
これで大体インターフェースについてはわかったので、次の記事ではmain.goのコードを責務ごとにファイル分割して、クリーンアーキテクチャをやっていこうと思います