saeki’s blog

The limits of my code mean the limits of my world.

GKEでGoアプリを動かすためのCI/CDパイプラインを構築する

Kubernetes入門がてらGKEでGoのアプリを動かすためのCI/CDパイプラインを組んでみたのでその作業ログ。これはあくまで入門記事なので本番で運用するレベルではない。

GCPの設定

まずはGCPの設定から。準備としてやることは大体こんな感じ。

- GCPプロジェクトの作成
- 使用するAPIの有効化
- CI用のサービスアカウント作成、権限付与

※以下gcloudコマンドではターミナルのクライアントにて$ gcloud auth loginで認証済みであることを前提とする

GCPプロジェクト作成

コマンドでさくっと作る

$ gcloud projects create <project_name> --set-as-default

<project_name>は任意のプロジェクト名に置き換える。グローバル?で一意な名前じゃないとプロジェクトIDが少し変わるので面倒。できれば一意なプロジェクト名にしたい。

--set-as-defaultを指定することでこれ以降叩くコマンドにいちいちプロジェクトを指定する必要がなくなる。

設定を確認

$ gcloud config list

あと念のため^で作ったプロジェクトが請求先アカウントと接続済みであることを確認しておく

gcr apiを有効にする

CircleCIでビルドしたコンテナイメージをGCRにpushする必要があるので、gcr apiを有効にする

$ gcloud services enable containerregistry.googleapis.com

有効になったか確認

$ gcloud services list

サービスアカウントを作成する

CircleCIからGCRへコンテナイメージをpushするためのサービスアカウントを用意

$ gcloud iam service-accounts create <service_account_name> --display-name "Account for upload docker image from circleci"

<service_account_name>はそれっぽいアカウント名に置き換える。circleciとかで一旦はよさそう

サービスアカウントに権限を付与する

$ gcloud projects add-iam-policy-binding <project_id> --member serviceAccount:<service_account_name>@gke-go-sample.iam.gserviceaccount.com --role roles/storage.admin

<project_id>は先ほど作成したプロジェクトのプロジェクトIDに置き換える。プロジェクトIDはGCPコンソール画面で確認できる

Goアプリケーション

単純にHello, worldを返すだけのAPIを用意する

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, world!")
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Dockerfileとかは以下 github.com

CircleCI

CIrcleCIではlintとtest, イメージのbuildとpushを行う 以下.circleci/config.yml

version: 2.1
executors:
  default:
    docker:
      - image: circleci/golang:1.13
    working_directory: /go/src/github.com/saekis/gke-go-sample
    environment:
      - GO111MODULE: "on"
  gcloud:
    docker:
      - image: google/cloud-sdk
    working_directory: /go/src/github.com/saekis/gke-go-sample
    environment:
      - GO111MODULE: "on"

commands:
  restore_module:
    steps:
      - restore_cache:
          name: Restore go modules cache
          key: go-mod-{{ .Branch }}-{{ checksum "go.mod" }}

  save_module:
    steps:
      - save_cache:
          name: Save go modules cache
          key: go-mod-{{ .Branch }}-{{ checksum "go.mod" }}
          paths:
            - /go/pkg/mod/cache

jobs:
  setup:
    executor:
      name: default
    steps:
      - checkout
      - restore_module
      - run: go mod download
      - save_module

  lint:
    executor:
      name: default
    steps:
      - checkout
      - restore_module
      - run:
          name: Download lint tool
          command: go get -u golang.org/x/lint/golint
      - run:
          name: golint
          command: golint -set_exit_status -min_confidence=1.1 ./...
      - run:
          name: go vet
          command: go vet ./...

  test:
    executor:
      name: default
    steps:
      - checkout
      - restore_module
      - run:
          name: test
          command: go test -v ./...

  build:
    parameters:
      env:
        type: string
    executor:
      name: gcloud
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Authenticate gcloud service account
          command: |
            echo $GCP_SERVICE_KEY > gcloud-service-key.json
            gcloud auth activate-service-account --key-file gcloud-service-key.json
            gcloud auth configure-docker --quiet
      - run:
          name: Build golang app image
          command: |
            docker build -t asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_BUILD_NUM} -f docker/golang/Dockerfile .
            docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:latest
            if [ -n "${CIRCLE_TAG}" ]; then
              docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_TAG}
            fi
      - run:
          name: Build nginx image
          command: |
            docker build -t asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_BUILD_NUM} -f docker/nginx/Dockerfile .
            docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:latest
            if [ -n "${CIRCLE_TAG}" ]; then
              docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_TAG}
            fi
      - run:
          name: Push golang image to GCR
          command: docker push asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app
      - run:
          name: Push nginx image to GCR
          command: docker push asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx

workflows:
  build_and_deploy:
    jobs:
      - setup
      - lint:
          requires:
            - setup
      - test:
          requires:
            - setup
      - build:
          name: build_dev
          env: dev
          requires:
            - lint
            - test
          filters:
            branches:
              ignore:
                - stage
                - master
      - build:
          name: build_stg
          env: stg
          requires:
            - lint
            - test
          filters:
            branches:
              only: stage
      - build:
          name: build_prod
          env: prod
          requires:
            - lint
            - test
          filters:
            branches:
              only: master

プロジェクト名とサービスアカウントのアクセスキーはCircleCIの環境変数にセットする

一応ここまででGitHubにpushされるとCircleCIが動いてGCRにコンテナイメージがpushされるようになる。

GKEの設定

クラスタ作成

$ gcloud container clusters create <cluster_name>

<cluster_name>は任意のクラスタ名を指定する
クラスタ作成は結構時間かかるので少し待つ
確認

$ kubectl config get-contexts

マニフェスト作成

deplayment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gke-go-sample-backend
  labels:
    app: gke-go-sample-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gke-go-sample-backend
  template:
    metadata:
      labels:
        app: gke-go-sample-backend
    spec:
      containers:
        - name: nginx
          image: asia.gcr.io/gke-go-sample/prod-nginx:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 80
        - name: golang
          image: asia.gcr.io/gke-go-sample/prod-app:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080

nginxをリバースプロキシに設定してるので、nginxのportを80、コンテナ間通信するためにgolangのコンテナのポートを8080にする

またコンテナを公開するのでingressも設定する ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gke-go-sample-backend-ingress
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName: gke-go-sample-backend
              servicePort: 80

全てのマニフェストファイルは以下

github.com

GKEへデプロイ

マニフェストファイルが用意できたらGKEにデプロイする

$ kubectl apply -f ./k8s

これでマニフェストファイルで設定した項目がGKEに反映される。 コンソールでingressのexternal ipにアクセスしてHello, worldが表示されれば成功。

GoFデザインパターン ~Compositeパターン~

Compositeパターンとは

ファイルシステムのような、中身(ファイル)とその入れ子(ディレクトリ)を再帰的に処理するためのパターン。容器と中身を同一視し、再帰的な構造を作る

Compositeパターンにおける役割

name description
Leaf 容器の中身を表すもの。ファイルシステムの例ではファイルがこの役割。
Composite 容器を表すもの。ファイルシステムの例ではディレクトリ。
Component LeafとCompositeを同一視するためのもの。今回書いたサンプルコードにおけるEntryインターフェースがこれ。
Client Compositeパターンの利用者。

コード例

Entryインターフェース

package main

type Entry interface {
    GetName() string
    GetSize() int64
    Add(Entry) error
}

ファイルとディレクトリを同一のものとして扱えるようにするためのインターフェース。ファイルクラスとディレクトリクラスがそれぞれこのインターフェースを実装するが、この方法のデメリットは、ファイルクラスでもAdd(Entry) errorのようなディレクトリクラスのためのメソッドを実装しなくてはいけなくなる(ファイルクラスにおけるAdd(Entry) errorはただエラーを返すためだけのメソッド)。

File

package main

import "github.com/pkg/errors"

type file struct {
    name string
    size int64
}

func NewFile(name string, size int64) Entry {
    return &file{
        name: name,
        size: size,
    }
}

func (f *file) GetName() string {
    return f.name
}

func (f *file) GetSize() int64 {
    return f.size
}

func (f *file) Add(Entry) error {
    return errors.New("This is not directory")
}

ファイルを表す構造体。Add() はディレクトリにおいてのみ必要だが、Entryインターフェースを満たすためにここでも実装する

Directory

package main

type directory struct {
    name    string
    size    int64
    entries []Entry
}

func NewDirectory(name string, size int64) Entry {
    return &directory{
        name: name,
        size: size,
    }
}

func (d *directory) GetName() string {
    return d.name
}

func (d *directory) GetSize() int64 {
    return d.size
}

func (d *directory) Add(entry Entry) error {
    d.entries = append(d.entries, entry)
    return nil
}

ディレクトリを表す構造体。

Client

package main

import (
    "flag"
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "strings"
)

func main() {
    os.Exit(run())
}

func run() int {
    flag.Parse()

    for _, v := range walk(getPath()) {
        fmt.Printf("%#v", v)
    }
    return 0
}

func walk(path string) []Entry {
    files, err := ioutil.ReadDir(path)
    if err != nil {
        panic(err)
    }

    var entries []Entry
    for _, file := range files {
        var entry Entry
        if file.IsDir() {
            entry := NewDirectory(file.Name(), file.Size())
            entries = append(entries, walk(filepath.Join(path, file.Name()))...)
            for _, entryInDir := range entries {
                err := entry.Add(entryInDir)
                if err != nil {
                    panic("panic!")
                }
            }
        } else {
            entry = NewFile(file.Name(), file.Size())
        }

        entries = append(entries, entry)
    }
    return entries
}

func getPath() string {
    var path string
    if args := flag.Args(); len(args) > 0 {
        path = args[0]
        if !strings.HasSuffix(path, "/") {
            path = path + "/"
        }
    } else {
        path = "./"
    }
    return path
}

Compositeを利用するClientの実装。walk() はディレクトリを探索するための関数で、ディレクトリに対しては walk() の中で walk()を再帰的に呼び出す。

github.com

GoFデザインパターン ~Strategyパターン~

Strategyパターンとは

ある問題に対して複数の解法が存在する場合、コンテキストに合わせてアルゴリズムのスイッチングを簡単に行えるようにするためのパターン。Strategyは「戦略」という意味。

Strategyパターンにおける役割

name description
Strategy アルゴリズムを利用するためのインターフェース。
ConcreteStrategy Strategyインターフェースを継承し、アルゴリズムの具体的な実装を行う。
Context 文脈に沿ってStrategyを呼び出す役。Strategyは外部からContextに注入し、ContextとStrategyは疎結合になるようにする。

GoでStrategyパターンの簡単な枠組みを実装してみる

$ tree ./
├── context.go
├── main.go
└── strategy
    ├── strategy.go
    ├── strategy_alpha.go
    └── strategy_beta.go

Strategyパッケージ

package strategy

type Strategy interface {
    Call()
}

type AlphaStrategy struct{}

func (*AlphaStrategy) Call() {
    // Do something
}

type BetaStrategy struct{}

func (*BetaStrategy) Call() {
    // Do something
}

Strategy interface がアルゴリズムを実行するための窓口になる。アルゴリズムを利用する側は Strategy interfaceにのみ依存する。
AlphaStrategy BetaStrategy はそれぞれ Strategy interface を継承していて、アルゴリズムの具体的な実装を行う。

Mainパッケージ

package main

import (
    "os"

    "github.com/saekis/go-design-pattern/8_strategy/strategy"
)

func main() {
    var s strategy.Strategy

    switch os.Args[1] {
    case "alpha":
        s = &strategy.AlphaStrategy{}
    case "beta":
        s = &strategy.BetaStrategy{}
    default:
        return
    }

    Context{s}.Exec()
}

type Context struct {
    strategy strategy.Strategy
}

func (c *Context) Exec() {
    c.strategy.Call()
}

Context.Exec() はアルゴリズムを利用するが、実際のアルゴリズムの処理は注入されたStrategyに委譲する。こうすることで、アルゴリズムを利用する文脈とアルゴリズムの実装を疎結合にすることができるので、アルゴリズムのスイッチングが容易になり、テスタビリティの向上が期待できる。

github.com

GoFデザインパターン ~Builderパターン~

こちらを読みながら進める。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

Builderパターンとは

複雑なインスタンスなどの生成において、骨組みと部品の生成を分離するパターン。 Builderが各部品を作るインターフェースを持ち、監督者であるDirectorがBuilderのインターフェースを使って部品を組み立てていく。
ここでのポイントは、DirectorはBuilderのインターフェースのみ着目するためBuilderの具体的な実装には依存せず、Builderが交換可能であるということ。

f:id:t_saeki:20190821203613p:plain

クラス図あまり綺麗に書けなかった..

実装

増補改訂版Java言語で学ぶデザインパターン入門で使われてる例をGoで実装してみる。

ディレクトリ構成は以下

./7_builder
├── builder
│   ├── builder.go
│   ├── html_builder.go
│   └── text_builder.go
├── director
    ├── director.go
│   └── document_builder.go
└── main.go

Director

構造の生成過程で監督者となるDirector。Builderインターフェスを使って構造を作っていく。コードが複雑になると複数のDirectorが必要になりそうなので、Director-ConcreteDirectorの関係を作りConcreteDirectorはDirectorインターフェースを実装するようにした。こうすることでClientにおいてDirectorも交換可能となる。

package director

import "github.com/saekis/go-design-pattern/7_builder/builder"

type Director interface {
    Build() string
}

type documentDirector struct {
    builder builder.Builder
}

func NewDocumentDirector(builder builder.Builder) Director {
    return &documentDirector{builder: builder}
}

func (director *documentDirector) Build() string {
    director.builder.MakeTitle("title")
    director.builder.MakeString("string")
    director.builder.MakeItems([]string{
        "item1",
        "item2",
    })
    director.builder.Close()
    return director.builder.GetResult()
}

Builder

Builderで各部品を生成する具体的な実装をおこなう。

Builderインターフェース
package builder

type Builder interface {
    MakeTitle(string)
    MakeString(string)
    MakeItems([]string)
    Close()
    GetResult() string
}
ConcreteBuilder

TextBuilder

package builder

import (
    "bytes"
    "fmt"
)

type textBuilderImple struct {
    buf bytes.Buffer
}

func NewTextBuilder() Builder {
    return &textBuilderImple{buf: bytes.Buffer{}}
}

func (b *textBuilderImple) MakeTitle(title string) {
    b.buf.WriteString(fmt.Sprintf("「%s」\n\n", title))
}

func (b *textBuilderImple) MakeString(str string) {
    b.buf.WriteString(fmt.Sprintf("%s\n", str))
}

func (b *textBuilderImple) MakeItems(items []string) {
    for _, item := range items {
        b.buf.WriteString(fmt.Sprintf("・%s\n", item))
    }
}

func (b *textBuilderImple) Close() {
    // Do nothing
}

func (b *textBuilderImple) GetResult() string {
    return b.buf.String()
}

HtmlBuilder

package builder

import (
    "bytes"
    "fmt"
)

type htmlBuilderImple struct {
    buf bytes.Buffer
}

func NewHtmlBuilder() Builder {
    return &htmlBuilderImple{buf: bytes.Buffer{}}
}

func (b *htmlBuilderImple) MakeTitle(title string) {
    b.buf.WriteString(fmt.Sprintf("<html><head><title>%s</title></head><body>", title))
    b.buf.WriteString(fmt.Sprintf("<h1>%s</h1>", title))
}

func (b *htmlBuilderImple) MakeString(str string) {
    b.buf.WriteString(fmt.Sprintf("<p>%s</p>", str))
}

func (b *htmlBuilderImple) MakeItems(items []string) {
    b.buf.WriteString("<ul>")
    for _, item := range items {
        b.buf.WriteString(fmt.Sprintf("<li>%s</li>", item))
    }
    b.buf.WriteString("</ul>")
}

func (b *htmlBuilderImple) Close() {
    b.buf.WriteString("</body></html>")
}

func (b *htmlBuilderImple) GetResult() string {
    return b.buf.String()
}

Client

ClientがBuilderを初期化してそれをDirectorに食わせる。
ClientではDirectorインターフェースのみ着目するため、DirectorとBuilderの具体的な処理は知らない。

package main

import (
    "fmt"

    "github.com/saekis/go-design-pattern/7_builder/director"

    "github.com/saekis/go-design-pattern/7_builder/builder"
)

func main() {
    // Build text
    b := builder.NewTextBuilder()
    d := director.NewDocumentDirector(b)
    result := d.Build()
    fmt.Println(result)

    // Build html
    b = builder.NewHtmlBuilder()
    d = director.NewDocumentDirector(b)
    result = d.Build()
    fmt.Println(result)
}

実行結果

$ go run main.go 
# text
「title」

string
・item1
・item2

# html
<html><head><title>title</title></head><body><h1>title</h1><p>string</p><ul><li>item1</li><li>item2</li></ul></body></html>

実装したもの

github.com

GoFデザインパターン ~Prototypeパターン~

こちらを読みながら進める。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

Prototypeパターンとは

クラスからインスタンスを生成せず、既存のインスタンスをコピーすることで新しいインスタンスを生成するというのがPrototypeパターン。本書によると、使い所はインスタンス生成が複雑な場合やインスタンスの内容がユーザーによる入力依存でコードによる再現が難しい場合とのこと。それだけだといまいち理解しづらいので本書の例題を試してみる。

実装

ディレクトリ構成は以下

6_prototype
├── framework
│   ├── manager.go
│   ├── null_product.go
│   └── product.go
├── main.go
└── product
    ├── message_box.go
    └── underline_pen.go

frameworkパッケージのmanager.goがproductインターフェースを使いproductの複製処理を行う。productパッケージにはproductインターフェースを実装した実際のproductを置く。

frameworkパッケージ

Productインターフェース

コピーの対象であるProductのインターフェース。インスタンスのクローン生成の詳細は各Productで実装される。

package framework

type Product interface {
    Use(string)
    CreateClone() Product
}

Manager

Productのコピーを生成する責務を持つManager。ClientはManagerのCreateClone() を呼ぶことで実際の複製処理が隠蔽された状態でインスタンスのコピーを生成することができる。
またmap型のshowcaseフィールドでクローンの原型となるProductを保持する。インスタンスの複製で呼ばれる Create() では要求されたProductが存在しない場合にNullProductを返すことでManagerインターフェースを満たすようにする。NullProductは実行されても何も処理が走らないようにする。

package framework

type Manager interface {
    Register(string, Product)
    Create(string) Product
}

type managerImple struct {
    showcase map[string]Product
}

func NewManager() Manager {
    return &managerImple{showcase: map[string]Product{}}
}

func (m *managerImple) Register(name string, proto Product) {
    m.showcase[name] = proto
}

func (m *managerImple) Create(protoname string) Product {
    if p, ok := m.showcase[protoname]; ok {
        return p.CreateClone()
    }

    return NewNullProduct()
}

productパッケージ

Goで構造体のクローンをシュッと生成する方法がわからなかったので愚直に原型となる構造体のフィールド値を受け継いだ構造体を生成するようにした。

package product

import (
    "fmt"

    "github.com/saekis/go-design-pattern/6_prototype/framework"
)

type messageBox struct {
    charDecorated string
}

func NewMessageBox(str string) framework.Product {
    return &messageBox{str}
}

func (mb *messageBox) Use(s string) {
    len := len(s)
    for i := 0; i < len+4; i++ {
        fmt.Print(mb.charDecorated)
    }
    fmt.Println("")
    fmt.Println(fmt.Sprintf("%s %s %s", mb.charDecorated, s, mb.charDecorated))
    for i := 0; i < len+4; i++ {
        fmt.Print(mb.charDecorated)
    }
    fmt.Println("")
}

func (mb *messageBox) CreateClone() framework.Product {
    return &messageBox{mb.charDecorated}
}

Main

Managerを利用するClient

package main

import (
    "github.com/saekis/go-design-pattern/6_prototype/framework"
    "github.com/saekis/go-design-pattern/6_prototype/product"
)

func main() {
    manager := framework.NewManager()
    up := product.NewUnderlinePen("~")
    mb1 := product.NewMessageBox("*")
    mb2 := product.NewMessageBox("/")
    manager.Register("Strong message", up)
    manager.Register("Warning box", mb1)
    manager.Register("Slash box", mb2)

    p1 := manager.Create("Strong message")
    p1.Use("Hello World.")
    p2 := manager.Create("Warning box")
    p2.Use("Hello World.")
    p3 := manager.Create("Slash box")
    p3.Use("Hello World.")
}

実行結果

$ go run main.go
"Hello World."
~~~~~~~~~~~~

****************
* Hello World. *
****************

////////////////
/ Hello World. /
////////////////

うーん、なんかいまいちPrototypeパターンのメリットが実感できない。もうちょっとProductが複雑であればメリットが実感できるのだろうか。
ただ良いなと思ったのが、インスタンスのクローン生成処理がProductインターフェースにより抽象化されたことで、それに依存するClientと具体的なProductクラスが分離され疎結合になったこと。もしProductのクローン生成の仕様が変わったとしても、変更はproductパッケージだけになるし、新しい種類のProductを追加したくなってもproductパッケージにProductを追加するだけで済む。のでオープン・クローズドの原則にも準拠できるようになる。

実装したもの

github.com

GoFデザインパターン ~Singletonパターン~

こちらを読みながら進める。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

Singletonパターンとは

アプリケーションのスコープにおいてインスタンスがたった一つであることを保証するパターン。直接的なインスタンス生成を制限し、インスタンス生成を一箇所で管理することでSingletonパターンを実現する。

実装

Singletonクラス

増補改訂版Java言語で学ぶデザインパターン入門 のJavaの例ではSingletonクラスのコンストラクタのスコープをprivateにすることで直接的なインスタンス生成を制限してるが、Goでは構造体のスコープをパッケージに限定することでこれを実現する。 インスタンスの取得は GetInstance() でのみ可能となる。
またインスタンス生成の関数を外部に公開せず、パッケージの変数初期化時にインスタンスを生成するようにしている。これはGoにおいて複数スレッドからSingletonクラスを生成される可能性を防ぐためで、他にはinit関数でインスタンスを生成してこれを実現することもできる。

package singleton

type singleton struct{}

var instance = newInstance()

func newInstance() *singleton {
    return &singleton{}
}

func GetInstance() *singleton {
    return instance
}

main

package main

import (
    "fmt"

    "github.com/saekis/go-design-pattern/5_singleton/singleton"
)

func main() {
    for i := 1; i <= 5; i++ {
        go func() {
            singleton.GetInstance()
        }()
    }

    fmt.Scanln()
}

実行結果

$ go run main.go 
Create new instance
Exist instance
Exist instance
Exist instance
Exist instance
Exist instance

実装したもの

github.com

GoFデザインパターン ~Factory Methodパターン~

こちらを読みながら進める。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

Factory Methodパターンとは

Template Methodパターンではスーパークラスで処理の枠組みを決め具体的な処理は子クラスで実装したが、それをインスタンス生成に適用したのがFactory Methodパターン。Factory Methodパターンによって、インスタンス生成の枠組みとインスタンス生成の実装を分けることができる。

f:id:t_saeki:20190712190500p:plain

実装

増補改訂版Java言語で学ぶデザインパターン入門で使われてる例をGoで実装してみる。

ディレクトリ構成は以下

./4_factory_method
├── framework
│   ├── creater.go
│   ├── factory.go
│   └── product.go
├── idcard
│   ├── idcard.go
│   └── idcard_factory.go
└── main.go

frameworkパッケージ

package framework

type Creater interface {
    CreateProduct(string) Product
    RegisterProduct(Product)
}

type Product interface {
    Use() string
    GetOwner() string
}

type Factory struct {}

func (*Factory) Create(creater Creater, owner string) Product {
    p := creater.CreateProduct(owner)
    creater.RegisterProduct(p)
    return p
}

Goで実装しようとするとImplementFactoryにFactoryを継承させることは難しいため、Factory.Create() にCreaterインターフェースを実装したImplementFactoryを渡すことでポリモーフィズムを実現するようにした

idcardパッケージ

このパッケージがインスタンス生成の具体的な処理を担う

package idcard

import (
    "fmt"

    "github.com/saekis/go-design-pattern/4_factory_method/framework"
)

type IdCard struct {
    owner string
}

func NewIdCard(owner string) framework.Product {
    return &IdCard{owner: owner}
}

func (ic *IdCard) Use() string {
    return fmt.Sprintf("Use %s's card", ic.owner)
}

func (ic *IdCard) GetOwner() string {
    return ic.owner
}

type IdCardFactory struct {
    owners []string
}

func NewIdCardFactory() framework.Creater {
    return &IdCardFactory{owners: []string{}}
}

func (*IdCardFactory) CreateProduct(owner string) framework.Product {
    return NewIdCard(owner)
}

func (factory *IdCardFactory) RegisterProduct(p framework.Product) {
    factory.owners = append(factory.owners, p.GetOwner())
}

IdCardFactory構造体がFactory Methodパターンにおけるインスタンス生成の具体的な処理を実装するクラス

Main

Main実装

package main

import (
    "fmt"

    "github.com/saekis/go-design-pattern/4_factory_method/framework"

    "github.com/saekis/go-design-pattern/4_factory_method/idcard"
)

func main() {
    factory := framework.Factory{}
    creater := idcard.NewIdCardFactory()
    card1 := factory.Create(creater, "たろう")
    card2 := factory.Create(creater, "はなこ")
    card3 := factory.Create(creater, "たかし")

    fmt.Println(card1.Use())
    fmt.Println(card2.Use())
    fmt.Println(card3.Use())
}

実行結果

$ go run main.go 
Use たろう's card
Use はなこ's card
Use たかし's card

実装したもの

github.com