1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2023で記事投稿!

テンプレートパターンをGo言語で再現するぞ!

Last updated at Posted at 2023-06-25

記事を書くきっかけ

最近Java言語で学ぶデザインパターン入門を参考にデザインパターンの勉強をしているのですが、
本の中で紹介されていたTemplate Patternを、私が普段使っている「Goで実装するならどうやるのか」というのが気になって書き始めたところ、今まで私が気づいていなかったGoの仕様について勉強することが出来たので、シェアしたいと思い記事にしました。

コードを書き始めて気づいたある違和感...

そんなわけで本を読みながらGoでの実装を進めていたのですが、あるところで違和感を感じ始めました。

本ではFactoryという抽象クラスにcreate()というインスタンス生成のメソッドを実装しています。
一方でcreate()の中で使われるcreateProduct()とregisterProduct()に関しては実装をサブクラスに委譲しています。

Pythonで書くとしたらこんな感じです。

from abc import ABCMeta, abstractmethod

class Product:
    def __init__(self, owner: str):
        self.owner = owner

class Factory(metaclass=ABCMeta):
    def create(self, owner: str):
        product = self.create_product(owner)
        self.register_product(product)
        return product

    @abstractmethod
    def create_product(owner: str) -> Product:
        raise NotImplementedError()

    @abstractmethod
    def register_product(product: Product):
        raise NotImplementedError()

class IDCardFactory(Factory):
    def create_product(owner: str) -> Product:
        return Product(owner)
    
    def register_product(product: Product):
        print(f'register product: {product}')

こんな感じのことをGoでも書きたかったのですが、なぜかうまく同じように書けずに違和感を感じていたところググってみたらこんな記事を見つけました。

そして読み進めている内に気づいたのですが、Goの基本的な仕様として抽象メソッドと具象メソッドをひとつのクラスの中に定義することができないのです。

個人的に改めてこのことを知るのは衝撃的でしたが、確かにGoでそんな実装見たこともなかったです。
考えてみれば抽象メソッドだけならインターフェースがあるし、具象メソッドが欲しければ構造体を定義してそれを委譲する気がします。

どうやって本の中で紹介されているFactory Methodを実装しようか数時間ほど悩みましたが自分なりに書いてみたので、以下でコードの解説をしていきます。

コード解説

実際のコードはこちらに上げているので良ければ見てみてください。

前述したとおり、GoではJavaのように抽象メソッドと具象メソッドを混ぜたクラスを継承するというのが簡単にはできないようになっています。

悩んだ末、今回の実装方針としては下記のようにしました。
(createsの向きが上下で入れ替わっていて見づらいですがご容赦ください...)

uml.png

工夫したポイントとして、Factoryインターフェースを満たすAbstractFactory構造体のメンバ変数として createProductとregisterProductの関数を持たせるようにしています。

抽象クラス

まず抽象クラスの実装としてはこんな感じです。

type (
	Product interface {
		Owner() string
		Use()
	}
	Factory interface {
		Create(owner string) Product
	}
	AbstractFactory struct {
		// AbstractFactoryのメソッドを直接叩かれたくないのでPrivate
		// あくまでCreate()の中で使われるメソッドのため直接叩かれる想定ではない
		createProduct   func(owner string) Product
		registerProduct func(p Product)
	}
)

func NewAbstractFactory(
	createProduct func(owner string) Product,
	registerProduct func(p Product),
) *AbstractFactory {
	return &AbstractFactory{
		createProduct:   createProduct,
		registerProduct: registerProduct,
	}
}

func (f *AbstractFactory) Create(owner string) Product {
	p := f.createProduct(owner)
	f.registerProduct(p)
	return p
}

Product構造体に関してはインターフェースを一つ定義するだけで終わりましたが、本で紹介されていたFactoryスーパークラスは、FactoryインターフェースとAbstractFactory構造体の2つに切り分けることで実現しました。

この時点でAbstractFactory構造体がFactoryインターフェースを満たしているため、サブクラスとなる構造体では、AbstractFactoryを委譲して、AbstractFactory内のcreateProduct()とregisterProduct()に具象メソッドを注入してあげれば、Factoryインターフェースとして振る舞うことができるようになります。

具象クラス

サブクラス側の実装はこんな感じです。

type idCardFactory struct {
	*domain.AbstractFactory
}

var _ domain.Factory = &idCardFactory{}

// 本の内容に従ってPrivateにしている
func (f *idCardFactory) createProduct(owner string) domain.Product {
	return &IDCard{owner: owner}
}

// 本の内容に従って同じくPrivateにしている
func (f *idCardFactory) registerProduct(p domain.Product) {
	fmt.Printf("card registered for %s\n", p.Owner())
}


func NewIDCardFactory() domain.Factory {
	f := &idCardFactory{
		// 抽象の構造体に具象の構造体に定義したメソッドを注入
		// これをやらないと&idCardFactory.Create()実行時にnil pointerエラーになる
		AbstractFactory: domain.NewAbstractFactory(
			(&idCardFactory{}).createProduct,
			(&idCardFactory{}).registerProduct,
		),
	}
	return domain.Factory(f)
}

留意点として、AbstractFactory: domain.NewAbstractFactory(の部分において、抽象クラスAbstractFactoryに対して具象メソッドを渡しているのですが、(&idCardFactory{}).createProductではなく、createProductだけを定義するのでもコードは動きます。

つまりこれを

func (f *idCardFactory) createProduct(owner string) domain.Product {
	return &IDCard{owner: owner}
}

こうしたうえで

func createProduct(owner string) domain.Product {
	return &IDCard{owner: owner}
}

(&idCardFactory{}).createProductの代わりにcreateProductをNewAbstractFactory()に渡しても動きますという意味です。

ただ、idCardFactoryに紐づく形でメソッドを定義しておいた方が、今後新しいFactoryの実装が出てきた時にも意図しない場面での利用を避けたり、コードを読みやすく出来ると思ったのでこのような書き方としました。
つまり例えばですが、securityCardFactoryという構造体が新しく定義された時に、securityCardFactoryのCreate()内でidCardFactoryのメソッドが意図せず呼ばれることを防ぐことができると考えています。

また、NewIDCardFactory()の最後でインスタンス化したidCardFactoryをFactoryにキャストしているため、前述のように新しいFactoryの実装が追加されても、同じようにFactory型で返す実装にすればポリモーフィズムを実現することが出来ます。
ただ、そのままidCardFactoryを返してしまって、必要があればクライアント側でFactoryにAssertすることの方が多いイメージではあるので、実際の開発の状況によって使い分けるべきだと思います。

Main関数

最後にmain関数はこんな感じになっています。

ちゃんとFactory.Create()が呼ばれた時のregisterProduct()による出力と、idCard.Use()による出力が確認できていますね。

func main() {
	idCardFactory := impl.NewIDCardFactory()

	idcard1 := idCardFactory.Create("Matisse")
	idcard1.Use()
	// id card registered for Matisse
	// id card used: owner name: Matisse

	idcard2 := idCardFactory.Create("Sadko")
	idcard2.Use()
	// id card registered for Sadko
	// id card used: owner name: Sadko
}

まとめ

というわけで今回の記事では、Java言語で学ぶデザインパターン入門で紹介されているFactory Methodの実装をGoで再現してみました。

抽象メソッドと具象メソッドが混ざったクラスの継承はJavaやPythonだと簡単にできるのですが、Goでは同様なことが簡単にてきないことが分かったので自分なりに実装してみました。

また、他の方のテンプレートパターンのGoでの実装の記事も見かけましたが、ほとんどが同パッケージ内で書かれた場合のみで、抽象的な振る舞いと具体的な実装が別パッケージに分かれている場合の実装として今回このようなコードを書いてみました。

実現の方法としては、スーパークラスとなる構造体に具象メソッドを定義し、抽象メソッドをメンバ変数として持っておいて、サブクラスをインスタンス化をする時に抽象メソッドに対してサブクラス側で定義した具象メソッドを注入するという手法を選択しました。

まだまだJava言語で学ぶデザインパターン入門の内容をGoで実装する時に「!?」となる部分もありそうなので、その際には第2弾を書きたいと思います!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?