- 投稿日:2020-04-02T20:32:44+09:00
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...
- 投稿日:2020-04-02T19:33:20+09:00
マイクロサービスとモノレポ
コロナの影響で在宅勤務(テレワーク)も一ヶ月以上も続き環境の変化に慣れてきましたが、
BBCパパ
のように急な家族の割り込みによる対応をどうかわすかを考えている streampack の Tana です。マイクロサービスとモノレポ(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としても今後の保守・運用を考慮すると、
Rails
、Golang
、NEXT.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
などのビルドツールだと並列処理などメリットはありそうですが要検討が必要そうです。新規・追加開発でのメリット
モノレポ対応できたことで、マイクロサービス間の修正が一貫性の変更がコードレビューで確認できます。それぞれのレポジトリの切り替えが必要なく、コードレビューをそれぞれ出す必要もありません。一つのリポジトリさえクローンして修正すればいいのです。マイクロサービス化では重要な手法かと思っております。
- 投稿日:2020-04-02T19:33:20+09:00
モノレポとマイクロサービス
コロナの影響で在宅勤務(テレワーク)も一ヶ月以上も続き環境の変化に慣れてきましたが、
BBCパパ
のように急な家族の割り込みによる対応で頭を悩まされている streampack の Tana です。モノレポ(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としても今後の保守・運用を考慮すると、
Rails
、Golang
、NEXT.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
などのビルドツールだと並列処理などメリットはありそうですが要検討が必要そうです。新規・追加開発でのメリット
モノレポ対応できたことで、マイクロサービス間の修正が一貫性の変更がコードレビューで確認できます。それぞれのレポジトリの切り替えが必要なく、コードレビューをそれぞれ出す必要はなくなりました。一つのリポジトリさえクローンして修正すればいいのです。マイクロサービス化では重要な手法かと思っております。
- 投稿日:2020-04-02T11:50:47+09:00
GoとDockerとHerokuでLINEBOTをためしてみる
はじめに
上記の組み合わせのQiitaの記事がなかったのでまとめてみました。
Git, Docker, Herokuの最低限の知識を前提にしています。ディレクトリ構成
linebot_go
| -- Dockerfile
| -- heroku.yml
| -- main.goGoと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.ymlbuild: 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.gopackage 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
DockerfileFROM 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 $PORTHerokuにpush
git push heroku master
でpush。LINE DevelopersのWebhookにHerokuのURLを設定
LINE Developers>Messaging API設定>Webhook設定
にHerokuのURLを入力します。
BOTを友だち追加しておためし!
- 投稿日:2020-04-02T05:54:16+09:00
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
■ "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.gopackage 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.gopackage 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
- 投稿日:2020-04-02T05:37:55+09:00
Golangで、デザインパターン「Strategy」を学ぶ
GoFのデザインパターンを学習する素材として、書籍「増補改訂版Java言語で学ぶデザインパターン入門」が参考になるみたいですね。
取り上げられている実例は、JAVAベースのため、Pythonで同等のプラクティスに挑んだことがありました。
Qiita記事: "Pythonで、デザインパターン「Strategy」を学ぶ"今回は、Pythonで実装した”Strategy”のサンプルアプリをGolangで実装し直してみました。
■ Strategyパターン(ストラテジー・パターン)
Strategyパターンは、コンピュータープログラミングの領域において、アルゴリズムを実行時に選択することができるデザインパターンである。
Strategyパターンはアルゴリズムを記述するサブルーチンへの参照をデータ構造の内部に保持する。このパターンの実現には、関数ポインタや関数オブジェクト、デリゲートのほか、オーソドックスなオブジェクト指向言語におけるポリモーフィズムと委譲、あるいはリフレクションによる動的ダック・タイピングなどが利用される。UML class and sequence diagram
UML class diagram
□ 備忘録
Strategy
パターンでは、アルゴリズムの部分を他の部分と意識的に分離して、そのアルゴリズムとのインタフェース部分だけを規定します。そして、プログラムから委譲によってアルゴリズムを利用する形態になります。
プログラムを改良する場合、Strategy
パターンを使っていれば、Strategy
役のインタフェースを変更しないように注意して、ConcreteStrategy
役だけを修正すればよく、さらに、委譲という緩やかな結びつきを使っているから、アルゴリズムを容易に切り替えることも可能になります。■ "Strategy"のサンプルプログラム
実際に、Strategyパターンを活用したサンプルプログラムを動かしてみて、次のような動作の様子を確認したいと思います。ここでは、TaroとHanakoが、ジャンケンを繰り返して勝敗を競うものになります。
<ジャンケンの戦略>
- 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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
- 投稿日:2020-04-02T01:16:55+09:00
Golang インターフェースと抽象化
抽象化
- 具体的な実装を隠し振る舞いによって共通化させること
- 複数の実装を同質のものとして扱う
インターフェースによる抽象化
- Goではインターフェースでしか抽象化することができない
インターフェース
型T
がインターフェースI
を実装しているとは
- インターフェースで定義されているメソッドを全て持つ
型T
はインターフェースI型
として振る舞うことができる
var i I = t
// tはT型の変数とするインターフェースの例
- インターフェースはメソッドの集まり
- メソッドのリストがインターフェースで規定しているものと一致する型はインターフェースを実装していることになる
sample.gopackage 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) }