はじめに
最近業務でGo+Clean Architectureで開発を行っているので、Clean Architectureへの理解を深める為
に、昔プライベートで作ったブラックジャックをGo+Clean Architectureでリファクタリングしてみました。
個人的な経験則ですが、何か新しいスキルを学ぶ時に下記のようなステップで学んでいくと体系だって理解できてきた気がしています。
- まずは先人の真似をする。
- 自分なりの理解で先人の真似した部分を崩してみたり、改造して、その結果を予測できるようにする。
- まっさらな状態から何かしらを作ってみる。
上記3ステップのうち、既に業務でステップ2までは経験できたので、今回はステップ3について取り組んだ内容を記事にまとめていきます!
また、本記事ではClean Architectureとは?については言及しません。
既に多数の記事で分かりやすい入門記事や解説記事があるので、ここでは自分なりの理解でClean Architectureやってみた、こんな感じにクラス分けしてみた、っていう一例を紹介できればと思います。
※お前のClean Architectureは間違っている!などいろいろご批判もあるかと思いますが、お手柔らかに・・・温かい目で見ていただけると幸いです。
成果物
個人開発用のリポジトリですが、今回作成したプロジェクトを下記で公開していますので、ローカルPCでソースを眺めたい、実行してみたい方はクローンしてみてください。
https://github.com/yuta-yoshinaga/go_trumpcards
ちなみに以前作成したWeb版(正確にはJS版)とCUI版も置いておきます。
今回はこの2つをミックスしてみました。
https://github.com/yuta-yoshinaga/typescript_trumpcards
https://github.com/yuta-yoshinaga/java_trumpcards
※正直、今見るとだいぶイケてないコードです・・・
※今回のコードがイケてるとは思ってませんが
実行イメージ
CUI
----------
dealer score
CLOVER 3,
----------
player score 20
DIAMOND 12,SPADE 12
----------
Please enter a command.
q・・・quit
r・・・reset
h・・・hit
s・・・stand
- コマンドラインにプレイヤーが起こすアクションをコマンドとして入力する。
- プレイヤーがバーストするかスタンドするまでディーラーのスコアは見えない。
- プレイヤーがバーストするかスタンドするまでディーラーの手札は1枚しか見えない。
Web
- プレイヤーが起こすアクションのボタンを押す。
- プレイヤーがバーストするかスタンドするまでディーラーのスコアは見えない。
- プレイヤーがバーストするかスタンドするまでディーラーの手札は1枚しか見えない。
設計資料
有名なこの図の色分け通りにクラス図とシーケンス図を作成してみました。
出典:The Clean Code Blog https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
クラス図
シーケンス図
※詳細なメソッド呼び出しなどは割愛した簡略版のシーケンス図です。
ソース
Enterprise Business Rules
entities
package entities
// カード定数
const (
CardDesignJoker = 0
CardDesignSpade = 1
CardDesignClover = 2
CardDesignHeart = 3
CardDesignDiamond = 4
CardDesignMin = CardDesignJoker
CardDesignMax = CardDesignDiamond
CardValueJoker = 0
CardValueMin = 0
CardValueMax = 13
CardCnt = (CardValueMax * 4)
)
// Card カードクラス
type Card struct {
design int // カード種類
value int // カード値
draw bool // カード払い出しフラグ
}
// NewCard コンストラクタ
func NewCard(design int, value int, draw bool) *Card {
return &Card{
design: design,
value: value,
draw: draw,
}
}
// GetDesign カード種類取得
func (c *Card) GetDesign() int {
return c.design
}
// GetValue カード値取得
func (c *Card) GetValue() int {
return c.value
}
// SetDraw カード払い出しフラグ設定
func (c *Card) SetDraw(draw bool) {
c.draw = draw
}
// GetDraw カード払い出しフラグ取得
func (c *Card) GetDraw() bool {
return c.draw
}
まずはブラックジャックの根幹をなすカード情報クラスです。
カードの柄や数値、山札からプレイヤーへ渡されたのかどうかなどのステータスを保持しています。
package entities
import "math/rand"
// TrumpCards トランプカードクラス
type TrumpCards struct {
deck []*Card // 山札
deckDrawCnt int // 山札配った枚数
deckCnt int // 山札枚数
}
// NewTrumpCards コンストラクタ
func NewTrumpCards(jokerCnt int) *TrumpCards {
t := new(TrumpCards)
t.deckCnt = CardCnt + jokerCnt
t.cardsInit()
t.deckInit()
return t
}
// cardsInit カード初期化
func (t *TrumpCards) cardsInit() {
t.deck = make([]*Card, 0)
for i := 0; i < t.deckCnt; i++ {
var design, value int
if 0 <= i && i <= 12 {
// スペード
design = CardDesignSpade
value = i + 1
} else if 13 <= i && i <= 25 {
// クローバー
design = CardDesignClover
value = (i - 13) + 1
} else if 26 <= i && i <= 38 {
// ハート
design = CardDesignHeart
value = (i - 26) + 1
} else if 39 <= i && i <= 51 {
// ダイアモンド
design = CardDesignDiamond
value = (i - 39) + 1
} else {
// ジョーカー
design = CardValueJoker
value = (i - 52) + 1
}
card := NewCard(design, value, false)
t.deck = append(t.deck, card)
}
}
// deckInit 山札初期化
func (t *TrumpCards) deckInit() {
t.deckDrawFlagInit()
t.deckDrawCnt = 0
}
// deckDrawFlagInit 山札ドローフラグ初期化
func (t *TrumpCards) deckDrawFlagInit() {
for _, v := range t.deck {
v.SetDraw(false)
}
}
// Shuffle 山札シャッフル
func (t *TrumpCards) Shuffle() {
n := len(t.deck)
for i := n - 1; i >= 0; i-- {
j := rand.Intn(i + 1)
t.deck[i], t.deck[j] = t.deck[j], t.deck[i]
}
t.deckInit()
}
// DrawCard 山札配る
func (t *TrumpCards) DrawCard() *Card {
var res *Card = nil
if t.deckDrawCnt < t.deckCnt {
t.deck[t.deckDrawCnt].SetDraw(true)
res = t.deck[t.deckDrawCnt]
t.deckDrawCnt++
}
return res
}
続いてゲームで使用するカードを山札として管理するクラスです。
コンストラクタでジョーカーの枚数を指定します。
今回はブラックジャックなのでジョーカーは用いませんが、ゆくゆくババ抜きなどの別のトランプゲームを実装する際にはジョーカーの枚数をここで調整します。
package entities
// Player プレイヤークラス
type Player struct {
cards []*Card // プレイヤーカード
}
// NewPlayer コンストラクタ
func NewPlayer() *Player {
return &Player{
cards: make([]*Card, 0),
}
}
// GetCardsSize プレイヤーカードの枚数取得
func (p *Player) GetCardsSize() int {
return len(p.cards)
}
// AddCard カード追加
func (p *Player) AddCard(card *Card) {
p.cards = append(p.cards, card)
}
// GetCard 指定番目のカード取得
func (p *Player) GetCard(idx int) *Card {
var res *Card = nil
if 0 <= idx && idx < len(p.cards) {
res = p.cards[idx]
}
return res
}
// Reset カードリセット
func (p *Player) Reset() {
p.cards = make([]*Card, 0)
}
トランプゲームのプレイヤークラスです。
配られたカードの情報などを保持します。
package entities
// BlackJackPlayer ブラックジャックプレイヤークラス
type BlackJackPlayer struct {
Player // 親クラス
score int // スコア
}
// NewBlackJackPlayer コンストラクタ
func NewBlackJackPlayer() *BlackJackPlayer {
return &BlackJackPlayer{
Player: Player{
cards: make([]*Card, 0),
},
score: 0,
}
}
// AddCard カード追加
func (bp *BlackJackPlayer) AddCard(card *Card) {
bp.cards = append(bp.cards, card)
bp.CalcScore()
}
// CalcScore 手札から現在のスコア計算
func (bp *BlackJackPlayer) CalcScore() {
aceFlag := false
bp.score = 0
for i := 0; i < len(bp.cards); i++ {
value := bp.cards[i].GetValue()
if 2 <= value && value <= 10 {
// 2~10
bp.score += value
} else if 11 <= value && value <= 13 {
// 11~13
bp.score += 10
} else {
if aceFlag {
// 2枚目のエースは強制的に1で換算する
bp.score++
} else {
// エースは後ほど計算する
aceFlag = true
}
}
}
if aceFlag {
// エース計算
tmpScore1 := bp.score + 1
tmpScore2 := bp.score + 11
if 22 <= tmpScore1 && 22 <= tmpScore2 {
// どちらもバーストしているならエースを1
bp.score = tmpScore1
} else if tmpScore1 <= 21 && 22 <= tmpScore2 {
// エースが11でバーストしているならエースを1
bp.score = tmpScore1
} else {
// どちらもバーストしていないならエースを11
bp.score = tmpScore2
}
}
}
// GetScore スコア取得
func (bp *BlackJackPlayer) GetScore() int {
return bp.score
}
プレイヤークラスを継承してブラックジャック用のスコア計算機能やスコア情報を保持できるように拡張したクラスです。
package entities
// BlackJack ブラックジャッククラス
type BlackJack struct {
trumpCards *TrumpCards // トランプカード
player *BlackJackPlayer // プレイヤー
dealer *BlackJackPlayer // ディーラー
gameEndFlag bool // ゲーム終了フラグ
}
// NewBlackJack コンストラクタ
func NewBlackJack(trumpCards *TrumpCards, player *BlackJackPlayer, dealer *BlackJackPlayer) *BlackJack {
return &BlackJack{
trumpCards: trumpCards,
player: player,
dealer: dealer,
gameEndFlag: false,
}
}
// Reset ゲーム初期化
func (b *BlackJack) Reset() {
b.gameEndFlag = false
// 山札シャッフル
for i := 0; i < 10; i++ {
b.trumpCards.Shuffle()
}
// プレイヤー・ディーラー初期化
b.player.Reset()
b.dealer.Reset()
// プレイヤー・ディーラー手札を2枚づつ配る
for i := 0; i < 2; i++ {
b.player.AddCard(b.trumpCards.DrawCard())
b.dealer.AddCard(b.trumpCards.DrawCard())
}
}
// PlayerHit プレイヤーヒット
func (b *BlackJack) PlayerHit() {
if !b.gameEndFlag {
b.player.AddCard(b.trumpCards.DrawCard())
if 22 <= b.player.GetScore() {
// バーストしたので強制終了
b.PlayerStand()
}
}
}
// PlayerStand プレイヤースタンド
func (b *BlackJack) PlayerStand() {
b.DealerHit()
}
// DealerHit ディーラーヒット
func (b *BlackJack) DealerHit() {
for {
if b.dealer.GetScore() < 17 {
// ディーラーは自分の手持ちのカードの合計が「17」以上になるまでヒットし続ける(カードを引き続ける)
b.dealer.AddCard(b.trumpCards.DrawCard())
} else {
// ディーラーは自分の手持ちカードの合計が「17」以上になったらステイする(カードを引かない)。
b.DealerStand()
break
}
}
}
// DealerStand ディーラースタンド
func (b *BlackJack) DealerStand() {
b.gameEndFlag = true
}
// GameJudgment ゲーム勝敗判定
func (b *BlackJack) GameJudgment() int {
res := 0
score1 := b.player.GetScore()
score2 := b.dealer.GetScore()
diff1 := 21 - score1
diff2 := 21 - score2
if 22 <= score1 && 22 <= score2 {
// プレイヤー・ディーラー共にバーストしているので負け
res = -1
} else if 22 <= score1 && score2 <= 21 {
// プレイヤーバーストしているので負け
res = -1
} else if score1 <= 21 && 22 <= score2 {
// ディーラーバーストしているので勝ち
res = 1
} else {
if diff1 == diff2 {
// 同スコアなら引き分け
res = 0
if score1 == 21 && b.player.GetCardsSize() == 2 && b.dealer.GetCardsSize() != 2 {
// プレイヤーのみがピュアブラックジャックならプレイヤーの勝ち
res = 1
}
} else if diff1 < diff2 {
// プレイヤーの方が21に近いので勝ち
res = 1
} else {
// ディーラーの方が21に近いので負け
res = -1
}
}
return res
}
// GetGameEndFlag ゲーム終了フラグ
func (b *BlackJack) GetGameEndFlag() bool {
return b.gameEndFlag
}
//GetPlayer プレイヤー
func (b *BlackJack) GetPlayer() *BlackJackPlayer {
return b.player
}
//GetDealer ディーラー
func (b *BlackJack) GetDealer() *BlackJackPlayer {
return b.dealer
}
山札、プレイヤー、ディーラーの情報を保持して、ブラックジャックゲームのアルゴリズムを実装したクラスです。
Application Business Rules
usecases
package usecases
import (
"go_trumpcards/entities"
"go_trumpcards/usecases/presenters"
)
// BlackJackInteractorIF ブラックジャックインタラクターインタフェース
type BlackJackInteractorIF interface {
Reset() string
Hit() string
Stand() string
}
// BlackJackInteractor ブラックジャックインタラクタークラス
type BlackJackInteractor struct {
bj *entities.BlackJack
bjp presenters.BlackJackPresenter
}
// NewBlackJackInteractor コンストラクタ
func NewBlackJackInteractor(bjp presenters.BlackJackPresenter) *BlackJackInteractor {
return &BlackJackInteractor{
bj: entities.NewBlackJack(entities.NewTrumpCards(0), entities.NewBlackJackPlayer(), entities.NewBlackJackPlayer()),
bjp: bjp,
}
}
// Reset ゲーム初期化
func (bi *BlackJackInteractor) Reset() string {
bi.bj.Reset()
return bi.bjp.Output(bi.bj)
}
// Hit ヒット
func (bi *BlackJackInteractor) Hit() string {
bi.bj.PlayerHit()
return bi.bjp.Output(bi.bj)
}
// Stand スタンド
func (bi *BlackJackInteractor) Stand() string {
bi.bj.PlayerStand()
return bi.bjp.Output(bi.bj)
}
コントローラーからブラックジャックの各アクションを実行できるようにするクラスです。
コントローラーへの戻り値を作成するプレゼンターはインタフェース型になっており、プレゼンターインスタンスはコンストラクタインジェクションをするようになっています。
package presenters
import (
"go_trumpcards/entities"
)
// BlackJackPresenter ブラックジャックプレゼンターインタフェース
type BlackJackPresenter interface {
Output(bj *entities.BlackJack) string
}
usecase層ではプレゼンタークラスを直接参照することができないので、プレゼンターインタフェースをusecase層で作成しています。
Interface Adapters
controllers
package controllers
import (
"encoding/json"
"net/http"
"go_trumpcards/usecases"
"github.com/ant0ine/go-json-rest/rest"
)
// BlackJackWebInput ブラックジャックWebインプット
type BlackJackWebInput struct {
Command string `json:"command"`
}
// BlackJackWebOutputCard ブラックジャックWebアウトプットカード
type BlackJackWebOutputCard struct {
Design string `json:"design"`
Value int `json:"value"`
}
// BlackJackWebOutputPlayer ブラックジャックWebアウトプットプレイヤー
type BlackJackWebOutputPlayer struct {
Score int `json:"score"`
Cards []*BlackJackWebOutputCard `json:"cards"`
}
// BlackJackWebOutput ブラックジャックWebアウトプット
type BlackJackWebOutput struct {
Dealer *BlackJackWebOutputPlayer `json:"dealer"`
Player *BlackJackWebOutputPlayer `json:"player"`
Message string `json:"message"`
}
// BlackJackWebController ブラックジャックWebコントローラークラス
type BlackJackWebController struct {
bji usecases.BlackJackInteractorIF
}
// NewBlackJackWebController コンストラクタ
func NewBlackJackWebController(bji usecases.BlackJackInteractorIF) *BlackJackWebController {
return &BlackJackWebController{
bji: bji,
}
}
// Exec ゲーム実行
func (bwc *BlackJackWebController) Exec(w rest.ResponseWriter, r *rest.Request) {
var param BlackJackWebInput
status := http.StatusOK
responseStr := ""
err := r.DecodeJsonPayload(¶m)
if err != nil || param.Command == "" {
status = http.StatusBadRequest
responseStr = `{"message":"param error."}`
} else {
switch param.Command {
case "q", "quit":
responseStr = `{"message":"bye."}`
case "r", "reset":
responseStr = bwc.bji.Reset()
case "h", "hit":
responseStr = bwc.bji.Hit()
case "s", "stand":
responseStr = bwc.bji.Stand()
default:
responseStr = `{"message":"Unsupported command."}`
}
}
response := new(BlackJackWebOutput)
response.Dealer = new(BlackJackWebOutputPlayer)
response.Player = new(BlackJackWebOutputPlayer)
err = json.Unmarshal([]byte(responseStr), &response)
if err != nil || responseStr == "" {
status = http.StatusBadRequest
response.Message = "error."
}
w.WriteHeader(status)
_ = w.WriteJson(response)
}
HTTPリクエストで受け取ったコマンドを解析して、usecase層のinteractorを呼び出します。
interactorはインタフェース型になっており、インスタンスをコンストラクタインジェクションして、ユニットテスト時にモックによるテストを行いやすいようにしています。
package controllers
import "go_trumpcards/usecases"
// BlackJackCuiController ブラックジャックCUIコントローラークラス
type BlackJackCuiController struct {
bji usecases.BlackJackInteractorIF
}
// NewBlackJackCuiController コンストラクタ
func NewBlackJackCuiController(bji usecases.BlackJackInteractorIF) *BlackJackCuiController {
return &BlackJackCuiController{
bji: bji,
}
}
// Exec ゲーム実行
func (bcc *BlackJackCuiController) Exec(command string) string {
res := ""
switch command {
case "q", "quit":
res = "bye."
case "r", "reset":
res = bcc.bji.Reset()
case "h", "hit":
res = bcc.bji.Hit()
case "s", "stand":
res = bcc.bji.Stand()
default:
res = "Unsupported command."
}
return res
}
コマンドラインから受け取った入力を解析して、usecase層のinteractorを呼び出します。
presenters
package presenters
import (
"encoding/json"
"go_trumpcards/entities"
"go_trumpcards/interface_adapters/controllers"
)
// BlackJackWebPresenter ブラックジャックWebプレゼンタークラス
type BlackJackWebPresenter struct {
}
// NewBlackJackWebPresenter コンストラクタ
func NewBlackJackWebPresenter() *BlackJackWebPresenter {
return &BlackJackWebPresenter{}
}
// Output ゲーム状態を出力
func (bjp *BlackJackWebPresenter) Output(bj *entities.BlackJack) string {
resObj := new(controllers.BlackJackWebOutput)
// dealer
dealer := bj.GetDealer()
resObj.Dealer = new(controllers.BlackJackWebOutputPlayer)
resObj.Dealer.Cards = make([]*controllers.BlackJackWebOutputCard, 0)
if bj.GetGameEndFlag() {
resObj.Dealer.Score = dealer.GetScore()
for i := 0; i < dealer.GetCardsSize(); i++ {
resObj.Dealer.Cards = append(resObj.Dealer.Cards, bjp.GetCardObj(dealer.GetCard(i)))
}
} else {
resObj.Dealer.Cards = append(resObj.Dealer.Cards, bjp.GetCardObj(dealer.GetCard(0)))
}
// player
player := bj.GetPlayer()
resObj.Player = new(controllers.BlackJackWebOutputPlayer)
resObj.Player.Cards = make([]*controllers.BlackJackWebOutputCard, 0)
resObj.Player.Score = player.GetScore()
for i := 0; i < player.GetCardsSize(); i++ {
resObj.Player.Cards = append(resObj.Player.Cards, bjp.GetCardObj(player.GetCard(i)))
}
if bj.GetGameEndFlag() {
switch bj.GameJudgment() {
case 0:
resObj.Message = "It is a draw."
case 1:
resObj.Message = "You are the winner."
default:
resObj.Message = "It is your loss."
}
}
res, _ := json.Marshal(resObj)
return string(res)
}
// GetCardObj カード情報取得
func (bjp *BlackJackWebPresenter) GetCardObj(card *entities.Card) *controllers.BlackJackWebOutputCard {
res := new(controllers.BlackJackWebOutputCard)
switch card.GetDesign() {
case entities.CardDesignSpade:
res.Design = "SPADE"
case entities.CardDesignClover:
res.Design = "CLOVER"
case entities.CardDesignHeart:
res.Design = "HEART"
case entities.CardDesignDiamond:
res.Design = "DIAMOND"
default:
res.Design = "Unsupported card"
}
res.Value = card.GetValue()
return res
}
usecase層のinteractorから呼び出されて、コントローラーへの返却値を生成します。
こちらはWebへのアウトプットなのでjsonで返却する為の処理を実装しています。
package presenters
import (
"strconv"
"go_trumpcards/entities"
)
// BlackJackCuiPresenter ブラックジャックCUIプレゼンタークラス
type BlackJackCuiPresenter struct {
}
// NewBlackJackCuiPresenter コンストラクタ
func NewBlackJackCuiPresenter() *BlackJackCuiPresenter {
return &BlackJackCuiPresenter{}
}
// Output ゲーム状態を出力
func (bjp *BlackJackCuiPresenter) Output(bj *entities.BlackJack) string {
player := bj.GetPlayer()
dealer := bj.GetDealer()
res := "----------\n"
// dealer
res += "dealer score "
if bj.GetGameEndFlag() {
res += strconv.Itoa(dealer.GetScore()) + "\n"
for i := 0; i < dealer.GetCardsSize(); i++ {
if i != 0 {
res += ","
}
res += bjp.GetCardStr(dealer.GetCard(i))
}
res += "\n"
} else {
res += "\n"
res += bjp.GetCardStr(dealer.GetCard(0)) + ",\n"
}
res += "----------\n"
// player
res += "player score " + strconv.Itoa(player.GetScore()) + "\n"
for i := 0; i < player.GetCardsSize(); i++ {
if i != 0 {
res += ","
}
res += bjp.GetCardStr(player.GetCard(i))
}
res += "\n----------\n"
if bj.GetGameEndFlag() {
switch bj.GameJudgment() {
case 0:
res += "It is a draw.\n"
case 1:
res += "You are the winner.\n"
default:
res += "It is your loss.\n"
}
res += "\n----------\n"
}
return res
}
// GetCardStr カード情報文字列取得
func (bjp *BlackJackCuiPresenter) GetCardStr(card *entities.Card) string {
res := ""
switch card.GetDesign() {
case entities.CardDesignSpade:
res = "SPADE "
case entities.CardDesignClover:
res = "CLOVER "
case entities.CardDesignHeart:
res = "HEART "
case entities.CardDesignDiamond:
res = "DIAMOND "
default:
res = "Unsupported card "
}
res += strconv.Itoa(card.GetValue())
return res
}
usecase層のinteractorから呼び出されて、コントローラーへの返却値を生成します。
こちらはCUIへのアウトプットなのでコマンドラインへ表示する為の処理が実装されています。
Frameworks & Drivers
web
package web
import (
"go_trumpcards/interface_adapters/controllers"
"go_trumpcards/interface_adapters/presenters"
"go_trumpcards/usecases"
"log"
"net/http"
"os"
"github.com/ant0ine/go-json-rest/rest"
)
// BlackJackWeb ブラックジャックWebクラス
type BlackJackWeb struct {
bjc *controllers.BlackJackWebController
}
// NewBlackJackWeb コンストラクタ
func NewBlackJackWeb() *BlackJackWeb {
return &BlackJackWeb{
bjc: controllers.NewBlackJackWebController(usecases.NewBlackJackInteractor(presenters.NewBlackJackWebPresenter())),
}
}
// Exec ゲーム実行
func (web *BlackJackWeb) Exec() {
api := rest.NewApi()
api.Use(rest.DefaultDevStack...)
router, err := rest.MakeRouter(
rest.Post("/blackjac/exec", web.bjc.Exec),
)
if err != nil {
log.Fatal(err)
}
api.SetApp(router)
http.Handle("/", http.FileServer(http.Dir("public")))
http.Handle("/blackjac/exec", api.MakeHandler())
log.Fatal(http.ListenAndServe(getListenPort(), nil))
}
func getListenPort() string {
port := os.Getenv("PORT")
if port != "" {
return ":" + port
}
return ":80"
}
HTTPリクエストを受け付けるための初期設定及び、各種クラスのインスタンス生成、インジェクションを行っています。
ui
package ui
import (
"bufio"
"fmt"
"os"
"go_trumpcards/interface_adapters/controllers"
"go_trumpcards/interface_adapters/presenters"
"go_trumpcards/usecases"
)
// BlackJackCui ブラックジャックCUIクラス
type BlackJackCui struct {
bjc *controllers.BlackJackCuiController
}
// NewBlackJackCui コンストラクタ
func NewBlackJackCui() *BlackJackCui {
return &BlackJackCui{
bjc: controllers.NewBlackJackCuiController(usecases.NewBlackJackInteractor(presenters.NewBlackJackCuiPresenter())),
}
}
// Exec ゲーム実行
func (cui *BlackJackCui) Exec() {
fmt.Println(cui.bjc.Exec("r"))
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Println("Please enter a command.")
fmt.Println("q・・・quit")
fmt.Println("r・・・reset")
fmt.Println("h・・・hit")
fmt.Println("s・・・stand")
scanner.Scan()
res := cui.bjc.Exec(scanner.Text())
fmt.Println(res)
if res == "bye." {
break
}
}
}
コマンドラインで対話形式でのUIを提供する為の実装がされています。web同様に各種クラスのインスタンス生成、インジェクションも行っています。
Other
package main
import (
"flag"
"fmt"
"go_trumpcards/frameworks_drivers/ui"
"go_trumpcards/frameworks_drivers/web"
"log"
"strings"
)
func main() {
flag.Parse()
switch strings.ToLower(flag.Arg(0)) {
case "cui":
cui := ui.NewBlackJackCui()
cui.Exec()
case "web":
web := web.NewBlackJackWeb()
web.Exec()
default:
log.Fatal(fmt.Errorf("Error: param not found %s", flag.Arg(0)))
}
}
最後にエントリーポイントであるmain.goです。
コマンドライン引数でCUIかWebかを選択できるようになっています。
作ってみて
- 今回の場合、コントローラーってCUI/Web分けずに、まとめた方がよいのかな?と思いました。
- CUI/Webの違いを吸収するのはFrameworks & Drivers層でやればもうちょっとすっきりできるかもしれないなと。
- でもwin/macのデスクトップアプリ版作るときのプレゼンターの実装がどんな戻り値になるべきか?によってはそもそもusecase層の戻り値から見直さないとですかね。
- あとは手抜きでInput DataとかOutput Dataとか作らなかったが、プレゼンターが直接entitiesを参照するのは良くないと作り終わってから気づきました。のでリファクタリング候補の一つです。
- BlackJackクラスはentities?usecase?に少し迷いました。この辺は人によっていろいろと解釈が分かれそうな気がしますね。
- 個人的にはentitiesってDBの1レコード分のデータの構造体(JavaでいうPOJO?Java警察に怒られそう・・・)とかを置く場所っていう先入観が強いので、混乱します・・・
- いびつな形でも一回自分で作ってみることで有識者の人にアドバイスもらえたり、色々議論できるようにもなると思うので、自分の手を動かしてみることが重要ですね。
まとめ
最後にClean Architectureで作ってみた感想をまとめます。
- 基本的にインタフェースに依存するように設計するので、上位レイヤー、下位レイヤー、どこからでも開発を始めやすい。
- インタフェースに依存するように設計するので、各レイヤーがおのずと疎結合になり、ユニットテストしやすい。
- ユニットテストしやすいので、リファクタリングしてもすぐにテスト実施して確認でき、安心して変更できる。
- 各レイヤーの責務がある程度明確で、単体テストもしやすいので、分業体制で並行しての開発作業がしやすい。
- 今回のようにCUI版とWeb版を作りたいっていう要件(そもそもそんな要件あるかってのは置いておいて)でも、上位レイヤーを差し替えるだけで対応しやすい。
正直Clean Architectureならではというよりかは、SOLIDの原則の依存性逆転の原則(Dependency inversion principle)
によるメリットへの感想が多いですね。
ただPresenterの考え方が、CUIとWebでのアウトプットの違いを吸収しやすいなと今回のプロジェクトでは感じたので、ここはClean Architectureならではのメリットでしょうか。
今回実際に自分の手を動かしながら1から作ってみたので、より理解が深まった気がします。
業務ではタイミングが合わないとプロジェクト立ち上げ時の苦労とかは経験できないと思うので、小さなプロジェクトを自分なりに作ってみて、試行錯誤してみるのも良いかと思います。
クラス設計した後の、どのレイヤーにこれを置くべきか?この処理はどのレイヤーでやるべきか?など一応ある程度の指針はClean Architectureは示してくれていますが、ガチガチに固まっている、こうではないとダメだという性質のアーキテクチャーではないと思うので、ある程度は要件に応じて形が多少いびつになっても良いのかなと思います!
今回はClean Architecture実装の一例として、紹介させていただきました。
それではまた次の記事でお会いしましょう!