20200402のGoに関する記事は7件です。

Go言語でOTP

二段階認証で必要になったのでGolangでOTPクライアントを実装してみました。
以下をかなり非常に参考させてもらいました。

package otp

import (
    "crypto/hmac"
    "crypto/sha1"
    "encoding/base32"
    "encoding/binary"
    "time"
)

func TRUNCATE(hs []byte) int {
    offset := int(hs[len(hs)-1] & 0x0F)
    p := hs[offset : offset+4]
    return (int(binary.BigEndian.Uint32(p)) & 0x7FFFFFFF) % 1000000
}

func HMACSHA1(k []byte, c uint64) []byte {
    cb := make([]byte, 8)
    binary.BigEndian.PutUint64(cb, c)

    mac := hmac.New(sha1.New, k)
    mac.Write(cb)

    return mac.Sum(nil)
}

func TOTP(k string, x uint64) int {
    key, err := base32.StdEncoding.DecodeString(k)
    if err != nil {
        return 0
    }

    return HOTP(key, T(0, x))
}

func HOTP(k []byte, c uint64) int {
    return TRUNCATE(HMACSHA1(k, c))
}

func T(t0, x uint64) uint64 {
    return (uint64(time.Now().Unix()) - t0) / x
}

// func main() {
//  fmt.Println(TOTP("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEF", 30)) // 30秒
// }

雰囲気で関数が全部大文字なってるけど HOTP/TOTP だけでよかった感。

本記事とは関係ないですが、Golang素晴らしいですね。
Javaで作っていたいくつかのAppEngineアプリを2nd Genにアップグレードしたら、Spring Bootで環境変数の一覧返すだけのWeb APIをF1インスタンス(メモリ256MB)にデプロイしたら、2回に1回メモリが溢れて落ちてしまい、全く使い物にならなかった...Google公式サイトにもJavaはオーバーヘッドが大きいとの記載があってJavaをやめる決断をしました。
勉強時間と教科書代と移行時間は結構かかってしまいましたが、代わりに全部Goに書き換えたら、メモリの使用量は1/10、実行速度はゴルーチンのおかげで3倍になり、Javaより非常に安く運用できそうです。
I/O待ちを軽量スレッド(goroutine)の仕組みを簡単に用いて潰せるのは非常にスケールさせやすいですね。GOMAXPROCSが1のGAE環境でも他サービスとの連携、DB/Storage/Datasotre/Firestore等への読み書き等を行っている間に処理を進めることでJavaよりかなり高速に実装し直せたのでとても驚きました。
とりあえず、Javaを今後使うことはないでしょう...さらばJava...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マイクロサービスとモノレポ

コロナの影響で在宅勤務(テレワーク)も一ヶ月以上も続き環境の変化に慣れてきましたが、BBCパパのように急な家族の割り込みによる対応をどうかわすかを考えている streampack の Tana です。

IMAGE ALT TEXT HERE
(在宅疲れした際に見るとリラックスできますw)

マイクロサービスとモノレポ(Monorepo)の取り組みについて

streampack は動画配信プラットフォームをお客様の要望にカスタマイズしながら、AWSなどのクラウド上に提供しているサービスです。既存のシステムを最大限に活かしつつ、効率的に適用するためのマイクロサービスとモノレポについての経験談のお話になります。

以前は案件やレイヤー毎にリポジトリを切って開発してました。いわゆる Polyrepo(Multi-Repo) と呼ばれる方法です。

例えば、
- project1_frontend
- project1_cms
- project2_api
- project3_dev
などなど。

当時はまだ案件が少なかったり、固有な機能やシステムがあり、案件の影響を避けるという理由もあり、別のリポジトリで管理した方がいいだろうと、当時は判断してました。

管理・保守の問題が発生・・・

しかし、案件が増えるたびにリポジトリの管理が複雑になり、セキュリティアップデート、機能廃止、バグの修正が入る場合、全てのリポジトリで下記の作業が必要した。

  • Project-1レポジトリのチェックアウト、アプリ起動、修正、テスト、レビュー、デプロイ
  • Project-2レポジトリのチェックアウト、アプリ起動、修正、テスト、レビュー、デプロイ
  • Project-3レポジトリのチェックアウト、アプリ起動、修正、テスト、レビュー、デプロイ

単純作業かもしれませんが、上記の作業やタスクの切り替えはどうしても時間がかかります。

なぜモノレポの採用に至ったのか?

そこで、調査していく上で、影響を受けたのが、なぜ Google は全てのコードをシングルレポジトリを管理しているのかのYouTubeになります。Facebook, Twitter などの大規模システムでも採用されている手法です。

この手法で重要なのが、Trunk-based Development であり、常に Trunk に開発されたコードがマージされるということになります。そうすれば、常に最新なコードで動作確認できます。リリースはリリースブランチがあり、リリースしたいコードのみを Cherry Pick して、リリースブランチに反映してリリースを行います。特別な機能やベータ段階のものは、フィーチャーフラグで制御します。必要に応じて ON/OFF にすることにより、コードを修正することなく制御できます。また、シングルリポジトリなので、コード、ライブラリの共有ができるのもメリットの一つになります。

streampackとしても今後の保守・運用を考慮すると、RailsGolangNEXT.js(NodeJS)などのアプリケーション毎のバージョン・セキュリティアップデートがあることを想定すると、それぞれの一つのモノレポで運用した方がいいと判断し進めることにしました。また、再利用することで開発の効率や無駄な開発を減らすのも目的の一つです。

調査とコード移植

まずは、現状調査ですが、案件ごとに特別な機能やシステムを洗い出しをしました。その特別な機能の移植を モノレポ に反映し、フィーチャーフラグとして提供するようにしました。また、設定情報やサービスの固有な情報は環境変数で管理し、ローカル開発の際は、.envの参照、また本番環境では、ECS(タスク定義)上で汎用的に管理できるようにしてます。例え、ホスト名、AWS環境、API情報が変わったとしても簡単に変更できます。Docker上で開発する上では重要な方法です。

Config - The TWELVE-FACTOR APP

DBマイグレーション

モノレポによる移行を考慮する際に重要だったのが、Backwards Compatibilityです。移行前と移行後でも同様に正常にダウンタイムなく動く必要がありました。ある案件ではある機能不要だからといって、テーブルやカラムを削除したりすると、DBマイグレーション時に移行前の状態だと動かなくなるので、基本は削除しないようにしております。将来的に不要と判断したタイミングで精査して削除するという流れです。また、一部データ移行・更新が必要だったものに関しては、DBマイグレーションで実行のタイミングで、タスク(Script)により、制御し手動対応させず自動化させてます。

マイクロサービス

ライブ配信制御機能やライブチャットなど一部のサービスや機能に応じてマイクロサービスとして管理されております。シングルなモノレポなので、ディレクトリを変え環境変数を変えることで、開発環境を立ち上げたり、異なる環境にデプロイし再現したり再利用することができるようにしてます。一部のお客様の要望により、固有な機能なものを幅広く使えるように共通仕様に持っていき再利用できるように心がけてはいますが、共有ライブラリ化できていないのもあるので、今後の課題です。

ビルド&デプロイ

マイクロサービスでのアプリケーションごとに、インストール方法や実行コマンドが違って混乱することがあります。どんな人でも同じコマンドで動かせることで、ハードルを下げ、さらにドキュメント(README.md) などもシンプルになります。以前こちらのブログでも紹介させていただきましたが、make コマンドを使ってアプリケーションの立ち上げ、ビルド、デプロイまでできるようにしてます。

アプリを立ち上げたい場合:

$ make run

ビルドしリリースしたい場合:

$ make release

というイメージです。Code Deploy, Circle CIなどのCI/CDを導入するのも可能ですが、開発・リリース頻度の兼ね合いや社内用のgitではCI/CDに対応していないため見送り状態です。またBazelなどのビルドツールだと並列処理などメリットはありそうですが要検討が必要そうです。

新規・追加開発でのメリット

モノレポ対応できたことで、マイクロサービス間の修正が一貫性の変更がコードレビューで確認できます。それぞれのレポジトリの切り替えが必要なく、コードレビューをそれぞれ出す必要もありません。一つのリポジトリさえクローンして修正すればいいのです。マイクロサービス化では重要な手法かと思っております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モノレポとマイクロサービス

コロナの影響で在宅勤務(テレワーク)も一ヶ月以上も続き環境の変化に慣れてきましたが、BBCパパのように急な家族の割り込みによる対応で頭を悩まされている streampack の Tana です。

IMAGE ALT TEXT HERE
(在宅疲れした際に見るとリラックスできますw)

モノレポ(Monorepo)とマイクロサービスへの取り組みについて

streampack は動画配信プラットフォームをお客様の要望にカスタマイズしながら、AWSなどのクラウド上に提供しているサービスです。既存のシステムとマイクロサービスを最大限に活かすために、案件ごとに管理していた複数のレポジトリ管理からモノレポへ移管したかについての経験談のお話になります。

以前は案件やレイヤー毎にリポジトリを切って開発してました。いわゆる Polyrepo(Multi-Repo) と呼ばれる方法です。

例えば、
- project1_frontend
- project1_cms
- project2_api
- project3_dev
などなど。

当時はまだ管理している案件が少なくまた固有な機能やシステムがあり、
別のリポジトリで管理した方がいいだろうと、当時は短期的な目線で判断してました。

がしかし・・・

管理・保守の問題が発生

案件が増えるたびにリポジトリの管理が複雑になり、セキュリティアップデート、機能廃止、バグの修正によって、全てのリポジトリで下記の作業が必要した。

  • Project-1レポジトリのチェックアウト、アプリ起動、コード修正、テスト、レビュー、デプロイ
  • Project-2レポジトリのチェックアウト、アプリ起動、コード修正、テスト、レビュー、デプロイ
  • Project-3レポジトリのチェックアウト、アプリ起動、コード修正、テスト、レビュー、デプロイ

単純作業のかもしれませんが、数が多いとどうしても時間がかかり非効率です。
また案件によっては、開発環境がないものがあったりなど、それをどう考慮するか、本番直反映でいけるかなど考慮すべき点が多々ありました。

なぜモノレポの採用に至ったのか?

そこで、調査していく上で、影響を受けたのが、なぜ Google は全てのコードをシングルレポジトリを管理しているのかのYouTubeになります。Facebook, Twitter などの大規模システムでも採用されている手法です。

この手法で重要なのが、Trunk-based Development であり、常に Trunk に開発されたコードがマージされるということになります。そうすれば、常に最新なコードで動作確認できます。リリースはリリースブランチがあり、リリースしたいコードのみを Cherry Pick して、リリースブランチに反映してリリースを行います。特別な機能やベータ段階のものは、フィーチャーフラグで制御します。必要に応じて ON/OFF にすることにより、コードを修正することなく制御できます。また、シングルリポジトリなので、コード、ライブラリの共有ができるのもメリットの一つになります。

streampackとしても今後の保守・運用を考慮すると、RailsGolangNEXT.js(NodeJS)などのアプリケーション毎のバージョン・セキュリティアップデートがあることを想定すると、それぞれの一つのモノレポで運用した方がいいと判断し進めることにしました。また、再利用することで開発の効率や無駄な開発を減らすのも目的の一つです。

調査とコード移植

まずは、現状調査ですが、案件ごとに特別な機能やシステムを洗い出しをしました。その特別な機能の移植を モノレポ に反映し、フィーチャーフラグとして提供するようにしました。また、設定情報やサービスの固有な情報は環境変数で管理し、ローカル開発の際は、.envの参照、また本番環境では、ECS(タスク定義)上で汎用的に管理できるようにしてます。例え、ホスト名、AWS環境、API情報が変わったとしても簡単に変更できます。Docker上で開発する上では12factorの概念は重要です。

Config - The TWELVE-FACTOR APP

DBマイグレーション

モノレポによる移行を考慮する際に重要だったのが、Backwards Compatibilityです。移行前と移行後でも同様に正常にダウンタイムなく動く必要がありました。ある案件ではある機能不要だからといって、テーブルやカラムを削除したりすると、DBマイグレーション時に移行前の状態だと動かなくなるので、基本は削除しないようにしております。将来的に不要と判断したタイミングで精査して削除するという流れです。また、一部データ移行・更新が必要だったものに関しては、DBマイグレーションで実行のタイミングで、タスク(Script)により、制御し手動対応させず自動化させてます。

マイクロサービス

ライブ配信制御機能やライブチャットなど一部のサービスや機能に応じてマイクロサービスとして管理されております。シングルなモノレポなので、ディレクトリを変え環境変数を変えることで、開発環境を立ち上げたり、異なる環境にデプロイし再現したり再利用することができるようにしてます。一部のお客様の要望により、固有な機能なものを幅広く使えるように共通仕様に持っていき再利用できるように心がけてはいますが、共有できない、固有な仕様になっているのが現状で、改善の余地があります。

ビルド&デプロイ

マイクロサービスでのアプリケーションごとに、インストール方法や実行コマンドが違って混乱することがあります。どんな人でも同じコマンドで動かせることで、ハードルを下げ、さらにドキュメント(README.md) などもシンプルになります。以前こちらのブログでも紹介させていただきましたが、make コマンドを使ってアプリケーションの立ち上げ、ビルド、デプロイまでできるようにしてます。

アプリを立ち上げたい場合:

$ make run

ビルドしリリースしたい場合:

$ make release

というイメージです。Code Deploy, Circle CIなどのCI/CDを導入するのも可能ですが、開発・リリース頻度の兼ね合いや社内用のgitではCI/CDに対応していないため見送り状態です。またBazelなどのビルドツールだと並列処理などメリットはありそうですが要検討が必要そうです。

新規・追加開発でのメリット

モノレポ対応できたことで、マイクロサービス間の修正が一貫性の変更がコードレビューで確認できます。それぞれのレポジトリの切り替えが必要なく、コードレビューをそれぞれ出す必要はなくなりました。一つのリポジトリさえクローンして修正すればいいのです。マイクロサービス化では重要な手法かと思っております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoとDockerとHerokuでLINEBOTをためしてみる

はじめに

上記の組み合わせのQiitaの記事がなかったのでまとめてみました。
Git, Docker, Herokuの最低限の知識を前提にしています。

ディレクトリ構成

linebot_go
| -- Dockerfile
| -- heroku.yml
| -- main.go

GoとDockerとHerokuの環境を準備する

LINEBOTを導入する前に、まずはこちらの記事を参考にして準備します。
Goで書いたサーバーをHerokuにDocker Deployする

pushしたらHerokuに自動デプロイされるようにする

上記の記事だと毎回デプロイするのに2コマンドを打たなければいけないので、1コマンドでデプロイできるようにします。

・上記記事(2コマンド)

$ heroku container:push web
$ heroku container:release web

・1コマンド

$ git push heroku master

ルートディレクトリにheroku.ymlを追加します。
内容は以下です。

heroku.yml
build:
  docker:
    web: Dockerfile

参考:Building Docker Images with heroku.yml

LINEBOTを導入する

LINEアカウント作成、SDKインストール

こちらの記事を参考にして、アカウント作成とSDKのインストールをする。
Go入門!?LineAPIとGOでオウム返しbotを作る
⇒Lineアカウントの準備、Line SDKのインストール

main.goを修正

SDKにある以下のサンプルを参考にしてmain.goを修正します。
line-bot-sdk-go/examples/echo_bot/server.go

main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"

    "github.com/line/line-bot-sdk-go/linebot"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello world!\n")
}

func lineHandler(w http.ResponseWriter, r *http.Request) {
    bot, err := linebot.New(
        "(自分のシークレットを入力)",
        "(自分のアクセストークンを入力)",
    )
    if err != nil {
        log.Fatal(err)
    }

    events, err := bot.ParseRequest(r)
    if err != nil {
        if err == linebot.ErrInvalidSignature {
            w.WriteHeader(400)
        } else {
            w.WriteHeader(500)
        }
        return
    }
    for _, event := range events {
        if event.Type == linebot.EventTypeMessage {
            switch message := event.Message.(type) {
            case *linebot.TextMessage:
                replyMessage := message.Text
                if replyMessage == "ぶりぶり" {
                    replyMessage = fmt.Sprintf("あああああああああああああああああああああああああああああああ!!!!!!!!!!!(ブリブリブリブリュリュリュリュリュリュ!!!!!!ブツチチブブブチチチチブリリイリブブブブゥゥゥゥッッッ!!!!!!!)")
                }
                if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(replyMessage)).Do(); err != nil {
                    log.Print(err)
                }
            case *linebot.StickerMessage:
                replyMessage := fmt.Sprintf(
                    "sticker id is %s, stickerResourceType is %s", message.StickerID, message.StickerResourceType)
                if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(replyMessage)).Do(); err != nil {
                    log.Print(err)
                }
            }
        }
    }
}

func main() {
    port, _ := strconv.Atoi(os.Args[1])
    fmt.Printf("Starting server at Port %d", port)
    http.HandleFunc("/", handler)
    http.HandleFunc("/callback", lineHandler)
    http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}

Dockerfileを修正

そのままのDockerfileだとLINEのSDKがHerokuにインストールされないので以下のコマンドを追記します。
RUN go get github.com/line/line-bot-sdk-go/linebot

Dockerfile
FROM golang:latest as builder

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /go/src/github.com/yokoe/go-server-example
COPY . .
RUN go get github.com/line/line-bot-sdk-go/linebot
RUN go build main.go

# runtime image
FROM alpine
COPY --from=builder /go/src/github.com/yokoe/go-server-example /app

CMD /app/main $PORT

Herokuにpush

git push heroku masterでpush。

LINE DevelopersのWebhookにHerokuのURLを設定

LINE Developers>Messaging API設定>Webhook設定
にHerokuのURLを入力します。
linedev.PNG

BOTを友だち追加しておためし!

30656.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golangで、デザインパターン「Template Method」を学ぶ

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Template Method」を学ぶ"

今回は、Pythonで実装した”Template Method”のサンプルアプリをGolangで実装し直してみました。

■ Template Method(テンプレート・メソッド・パターン)

Template Methodパターン(テンプレート・メソッド・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。「振る舞いに関するパターン」に属する。Template Methodパターンの目的は、ある処理のおおまかなアルゴリズムをあらかじめ決めておいて、そのアルゴリズムの具体的な設計をサブクラスに任せることである。そのため、システムのフレームワークを構築するための手段としてよく活用される。

UML class diagram

templatemethod.png
(以上、ウィキペディア(Wikipedia)より引用)

■ "Template Method"のサンプルプログラム

実際に、Template Methodパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。

  • 文字"H"を、5回連続で表示する。さらに前後では、"<<", ">>"で囲って表示する
  • 文字列"Hello, World!"を5回連続で表示する。さらに、枠線で囲って表示する
$ go run Main.go 
<<HHHHH>>

+-------------+
|Hello, World!|
|Hello, World!|
|Hello, World!|
|Hello, World!|
|Hello, World!|
+-------------+

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/TemplateMethod

  • ディレクトリ構成
.
├── Main.go
└── templatemethod
    └── display.go

(1) AbstractClass(抽象クラス)の役

AbstractClass役は、テンプレートメソッドを実装します。また、テンプレートメソッドで使っている抽象メソッドを宣言します。この抽象メソッドは、サブクラスであるConcreteClass役によって実装されます。
サンプルプログラムでは、PrinterインタフェースとAbstractDisplay構造体が、この役を努めます。

templatemethod/display.go
package templateMethod

import "fmt"

// Printer is interface
type Printer interface {
    open()
    print()
    close()
}

// AbstractDisplay is struct
type AbstractDisplay struct {
    printer Printer
}

// Display for printout result
func (a *AbstractDisplay) Display() {
    a.printer.open()
    for i := 0; i < 5; i++ {
        a.printer.print()
    }
    a.printer.close()
}

(2) ConcreteClass(具象クラス)の役

AbstractClass役で定義された抽象クラスを具体的に実装します。ここで実装したメソッドは、AbstractClass役のテンプレートメソッドから呼び出されます。
サンプルプログラムでは、CharDisplay構造体と、StringDisplay構造体が、この役を努めます。

templatemethod/display.go
// CharDisplay is struct
type CharDisplay struct {
    *AbstractDisplay
    ch byte
}

// NewCharDisplay func for initializing CharDisplay
func NewCharDisplay(ch byte) *CharDisplay {
    charDisplay := &CharDisplay{
        AbstractDisplay: &AbstractDisplay{},
        ch:              ch,
    }
    charDisplay.printer = charDisplay
    return charDisplay
}

func (c *CharDisplay) open() {
    fmt.Print("<<")
}
func (c *CharDisplay) print() {
    fmt.Print(string(c.ch))
}
func (c *CharDisplay) close() {
    fmt.Println(">>")
}
templatemethod/display.go
// StringDisplay is struct
type StringDisplay struct {
    *AbstractDisplay
    str   string
    width int
}

// NewStringDisplay func for initalizing StringDisplay
func NewStringDisplay(str string) *StringDisplay {
    stringDisplay := &StringDisplay{
        AbstractDisplay: &AbstractDisplay{},
        str:             str,
        width:           len(str),
    }
    stringDisplay.printer = stringDisplay
    return stringDisplay
}

func (s *StringDisplay) open() {
    s.printLine()
}
func (s *StringDisplay) print() {
    fmt.Printf("%s%s%s\n", "|", s.str, "|")
}
func (s *StringDisplay) close() {
    s.printLine()
}

func (s *StringDisplay) printLine() {
    fmt.Print("+")
    for i := 0; i < s.width; i++ {
        fmt.Print("-")
    }
    fmt.Println("+")
}

(3) Client(利用者)の役

サンプルプログラムでは、startMain関数が、この役を努めます。

Main.go
package main

import (
    "fmt"

    templateMethod "./templatemethod"
)

func startMain() {
    d1 := templateMethod.NewCharDisplay('H')
    d2 := templateMethod.NewStringDisplay("Hello, World!")
    d1.Display()
    fmt.Println("")
    d2.Display()
}

func main() {
    startMain()
}

■ 参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golangで、デザインパターン「Strategy」を学ぶ

GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Strategy」を学ぶ"

今回は、Pythonで実装した”Strategy”のサンプルアプリをGolangで実装し直してみました。

■ Strategyパターン(ストラテジー・パターン)

Strategyパターンは、コンピュータープログラミングの領域において、アルゴリズムを実行時に選択することができるデザインパターンである。
Strategyパターンはアルゴリズムを記述するサブルーチンへの参照をデータ構造の内部に保持する。このパターンの実現には、関数ポインタや関数オブジェクト、デリゲートのほか、オーソドックスなオブジェクト指向言語におけるポリモーフィズムと委譲、あるいはリフレクションによる動的ダック・タイピングなどが利用される。

UML class and sequence diagram

W3sDesign_Strategy_Design_Pattern_UML.jpg

UML class diagram

strategy.png
(以上、ウィキペディア(Wikipedia)より引用)

□ 備忘録

Strategyパターンでは、アルゴリズムの部分を他の部分と意識的に分離して、そのアルゴリズムとのインタフェース部分だけを規定します。そして、プログラムから委譲によってアルゴリズムを利用する形態になります。
プログラムを改良する場合、Strategyパターンを使っていれば、Strategy役のインタフェースを変更しないように注意して、ConcreteStrategy役だけを修正すればよく、さらに、委譲という緩やかな結びつきを使っているから、アルゴリズムを容易に切り替えることも可能になります

■ "Strategy"のサンプルプログラム

実際に、Strategyパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。ここでは、TaroHanakoが、ジャンケンを繰り返して勝敗を競うものになります。

<ジャンケンの戦略>

  • Taroは、ジャンケンに勝ったら、次のジャンケンでも同じ手で挑む
  • Hanakoは、"グー", "チョキ", "パー"の順番にジャンケンに挑む
$ go run Main.go 
Winner:[Hana: 0games,  0win,  0lose]
Winner:[Taro: 1games,  0win,  1lose]
Winner:[Hana: 2games,  1win,  1lose]
Even...
Even...
Winner:[Hana: 5games,  2win,  1lose]
Winner:[Taro: 6games,  1win,  3lose]
Winner:[Hana: 7games,  3win,  2lose]
Winner:[Hana: 8games,  4win,  2lose]
Winner:[Hana: 9games,  5win,  2lose]
Winner:[Taro: 10games,  2win,  6lose]
Winner:[Hana: 11games,  6win,  3lose]
Even...

...(snip)

Winner:[Taro: 9978games,  2446win,  4964lose]
Winner:[Hana: 9979games,  4964win,  2447lose]
Winner:[Taro: 9980games,  2447win,  4965lose]
Winner:[Hana: 9981games,  4965win,  2448lose]
Winner:[Hana: 9982games,  4966win,  2448lose]
Even...
Winner:[Taro: 9984games,  2448win,  4967lose]
Winner:[Hana: 9985games,  4967win,  2449lose]
Winner:[Taro: 9986games,  2449win,  4968lose]
Winner:[Hana: 9987games,  4968win,  2450lose]
Winner:[Taro: 9988games,  2450win,  4969lose]
Winner:[Hana: 9989games,  4969win,  2451lose]
Even...
Winner:[Hana: 9991games,  4970win,  2451lose]
Winner:[Taro: 9992games,  2451win,  4971lose]
Winner:[Hana: 9993games,  4971win,  2452lose]
Winner:[Taro: 9994games,  2452win,  4972lose]
Winner:[Hana: 9995games,  4972win,  2453lose]
Winner:[Taro: 9996games,  2453win,  4973lose]
Winner:[Hana: 9997games,  4973win,  2454lose]
Even...
Winner:[Taro: 9999games,  2454win,  4974lose]
Total Result:
[Taro: 10000games,  2455win,  4974lose]
[Hana: 10000games,  4974win,  2455lose]

結果は、hanakoのジャンケン戦略("グー", "チョキ", "パー"の順番にジャンケンに挑む)の方が、Taroのジャンケン戦略よりも優れている結果になりました。これは、Taroのジャンケン戦略(ジャンケンに勝ったら、次のジャンケンでも同じ手で挑む)では、絶対に連勝できないためです。

■ サンプルプログラムの詳細

Gitリポジトリにも、同様のコードをアップしています。
https://github.com/ttsubo/study_of_design_pattern_with_golang/tree/master/Strategy

  • ディレクトリ構成
.
├── Main.go
└── strategy
    ├── hand.go
    ├── player.go
    └── strategy.go

(1) Strategy(戦略)の役

戦略を利用するためのインタフェースを定める役です。
サンプルプログラムでは、Strategyインタフェースが、この役を努めます。

strategy/strategy.go
package strategy

import (
    "math/rand"
    "time"
)

// Strategy is interface
type Strategy interface {
    NextHand() *Hand
    study(win bool)
}

(2) ConcreteStrategy(具体的戦略)の役

Strategy役のインタフェースを実際に実装する役です。ここで具体的な戦略(作業・方策・方法・アルゴリズム)を実際にプログラミングします。
サンプルプログラムでは、WinningStrategy構造体と、CircularStrategy構造体が、この役を努めます。

strategy/strategy.go
// WinningStrategy is struct
type WinningStrategy struct {
    won      bool
    prevHand *Hand
}

// NewWinningStrategy func for initializing WinningStrategy
func NewWinningStrategy() *WinningStrategy {
    return &WinningStrategy{
        won:      false,
        prevHand: nil,
    }
}

// NextHand func can handle result of preHand
func (ws *WinningStrategy) NextHand() *Hand {
    if !ws.won {
        rand.Seed(time.Now().UnixNano())
        ws.prevHand = getHand(rand.Intn(3))
    }
    return ws.prevHand
}

func (ws *WinningStrategy) study(win bool) {
    ws.won = win
}
strategy/strategy.go
// CircularStrategy is struct
type CircularStrategy struct {
    hand int
}

// NewCircularStrategy func for initializing CircularStrategy
func NewCircularStrategy() *CircularStrategy {
    return &CircularStrategy{
        hand: 0,
    }
}

// NextHand func can handle result of each Hand
func (cs *CircularStrategy) NextHand() *Hand {
    return getHand(cs.hand)
}

func (cs *CircularStrategy) study(win bool) {
    cs.hand = (cs.hand + 1) % 3
}

(3) Context(文脈)の役

Strategy役を利用する役です。ConcreteStrategy役のインスタンスを持っていて、必要に応じてそれを利用します。
サンプルプログラムでは、Player構造体が、この役を努めます。

strategy/player.go
package strategy

import "fmt"

// Player is struct
type Player struct {
    Name                           string
    strategy                       Strategy
    wincount, losecount, gamecount int
}

// NewPlayer func for initializing Player
func NewPlayer(name string, strategy Strategy) *Player {
    return &Player{
        Name:     name,
        strategy: strategy,
    }
}

// NextHand func can handle result of preHand
func (p *Player) NextHand() *Hand {
    return p.strategy.NextHand()
}

// Win func can judge result as game win
func (p *Player) Win() {
    p.strategy.study(true)
    p.wincount++
    p.gamecount++
}

// Lose func can judge result as game lose
func (p *Player) Lose() {
    p.strategy.study(false)
    p.losecount++
    p.gamecount++
}

// Even func can judge result as game even
func (p *Player) Even() {
    p.gamecount++
}

// ToString func can display results of all games
func (p *Player) ToString() string {
    str := fmt.Sprintf("[%s: %dgames,  %dwin,  %dlose]", p.Name,
        p.gamecount,
        p.wincount,
        p.losecount)
    return str
}

(4) Client(依頼人)の役

サンプルプログラムでは、startMain関数が、この役を努めます。

Main.go
package main

import (
    "fmt"

    "./strategy"
)

func startMain() {
    player1 := strategy.NewPlayer("Taro", strategy.NewWinningStrategy())
    player2 := strategy.NewPlayer("Hana", strategy.NewCircularStrategy())

    for i := 0; i < 10000; i++ {
        hand1 := player1.NextHand()
        hand2 := player2.NextHand()
        if hand1.IsStrongerThan(hand2) {
            fmt.Printf("Winner:%s\n", player1.ToString())
            player1.Win()
            player2.Lose()
        } else if hand1.IsWeakerThan(hand2) {
            fmt.Printf("Winner:%s\n", player2.ToString())
            player1.Lose()
            player2.Win()
        } else {
            fmt.Println("Even...")
            player1.Even()
            player2.Even()
        }
    }

    fmt.Println("Total Result:")
    fmt.Println(player1.ToString())
    fmt.Println(player2.ToString())
}

func main() {
    startMain()
}

(5) その他

ジャンケンの勝敗を管理します。

strategy/hand.go
package strategy

// Const Value for HandGame
const (
    handValueGUU = iota
    handValueCHO
    handValuePAA
)

//Hand is struct
type Hand struct {
    handValue int
}

var hands []*Hand

func init() {
    hands = []*Hand{
        &Hand{handValueGUU},
        &Hand{handValueCHO},
        &Hand{handValuePAA},
    }
}

func getHand(handValue int) *Hand {
    return hands[handValue]
}

// IsStrongerThan func can judge result of handGame as Winner
func (myHand *Hand) IsStrongerThan(opponentHand *Hand) bool {
    return myHand.fight(opponentHand) == 1
}

// IsWeakerThan func can judge result of handGame as Looser
func (myHand *Hand) IsWeakerThan(opponentHand *Hand) bool {
    return myHand.fight(opponentHand) == -1
}

func (myHand *Hand) fight(opponentHand *Hand) int {
    if myHand == opponentHand {
        return 0
    } else if (myHand.handValue+1)%3 == opponentHand.handValue {
        return 1
    } else {
        return -1
    }
}

■ 参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang インターフェースと抽象化

抽象化

  • 具体的な実装を隠し振る舞いによって共通化させること
  • 複数の実装を同質のものとして扱う

インターフェースによる抽象化

  • Goではインターフェースでしか抽象化することができない

インターフェース

  • 型TインターフェースIを実装しているとは
    • インターフェースで定義されているメソッドを全て持つ
    • 型TインターフェースI型として振る舞うことができる
      • var i I = t // tはT型の変数とする

インターフェースの例

  • インターフェースはメソッドの集まり
    • メソッドのリストがインターフェースで規定しているものと一致する型はインターフェースを実装していることになる
sample.go
package main

import "fmt"

// バスケをするためのインターフェース
type BasketBaller interface {
    Run()
    Dunk()
    Jump()
    Shoot()
}

// 人間と犬の構造体
type Human struct {
    Height int
    Weight int
}
type Dog struct {
    Kind string
}

// 人間のインターフェースの実装
func (h Human) Run() {
    fmt.Printf("身長: %v, 体重:%v, の選手が走るよ\n", h.Height, h.Weight)
}
func (h Human) Dunk() {
    fmt.Printf("身長: %v, 体重:%v, の選手がダンクをするよ\n", h.Height, h.Weight)
}
func (h Human) Jump() {
    fmt.Printf("身長: %v, 体重:%v, の選手がジャンプをするよ\n", h.Height, h.Weight)
}
func (h Human) Shoot() {
    fmt.Printf("身長: %v, 体重:%v, の選手がシュートするよ\n", h.Height, h.Weight)
}

// 犬のインターフェースの実装
func (d Dog) Run() {
    fmt.Printf("犬(%v)が走るよ\n", d.Kind)
}
func (d Dog) Dunk() {
    fmt.Printf("犬(%v)はダンクをできないよ\n", d.Kind)
}
func (d Dog) Jump() {
    fmt.Printf("犬(%v)がジャンプをするよ\n", d.Kind)
}
func (d Dog) Shoot() {
    fmt.Printf("犬(%s)はシュートができないよ\n", d.Kind)
}

// BasketBallerインターフェースが引数になる、Basketメソッド
func Basket(b BasketBaller) {
    b.Dunk()
    b.Jump()
    b.Run()
    b.Shoot()
}

func main() {
    var human = Human{170, 64}
    var dog = Dog{Kind: "プードル"}
    var basketBaller BasketBaller

    basketBaller = human // Human型をBasketBaller型に
    Basket(basketBaller)

    basketBaller = dog // Dog型をBasketBaller型に
    Basket(basketBaller)
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む