1
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

goでデザインパターン

Last updated at Posted at 2020-06-25

builder

任意の項目を指定して構造体を初期化するパターン
各項目の関数では自身を返し、メソッドチェインで構造体を初期化していく。

package main

type email struct {
	from, to, subject, body string
}
type EmailBuilder struct {
	email email
}
func (b *EmailBuilder) From(from string) *EmailBuilder {
	b.email.from = from
	return b
}
func (b *EmailBuilder) To(to string) *EmailBuilder {
	b.email.to = to
	return b
}
func (b *EmailBuilder) Subject(subject string) *EmailBuilder {
	b.email.subject = subject
	return b
}
func (b *EmailBuilder) Body(body string) *EmailBuilder {
	b.email.body = body
	return b
}
func sendMail(email *email) {
	// 実際の処理
}
type build func(*EmailBuilder)
func SendEmail(action build) {
	builder := EmailBuilder{}
	action(&builder)
	sendMail(&builder.email)
}
func main() {
	SendEmail(func(b *EmailBuilder) {
		b.From("hoge@example.com").
			To("huga@example.com").
			Subject("Sample").
			Body("HOGE")
	})
}

functional builder

任意の項目を指定して構造体を初期化するパターン。
上述のbuilderパターンの関数バージョン。


package main

type email struct {
	from, to, subject, body string
}

type EmailBuilder struct {
	email email
}

type Option func(b *EmailBuilder)

func From(from string) Option {
	return func(b *EmailBuilder) {
		b.email.from = from
	}
}
func To(to string) Option {
	return func(b *EmailBuilder) {
		b.email.to = to
	}
}
func Subject(subject string) Option {
	return func(b *EmailBuilder) {
		b.email.subject = subject
	}
}
func Body(body string) Option {
	return func(b *EmailBuilder) {
		b.email.body = body
	}
}
func sendMail(email *email) {
	// 実際の処理
}

func SendEmail(ops ...Option) {
	builder := EmailBuilder{}

	for _, option := range ops {
		option(&builder)
	}
	sendMail(&builder.email)
}

func main() {
	SendEmail(From("hoge@example.com"),
		To("huga@example.com"),
		Subject("Sample"),
		Body("HOGE"))
}

functional builder2

任意の項目を指定して構造体を初期化するパターンその2。
ネストした構造体を初期化する。

package main

import "fmt"

type User struct {
	id    int64
	skill *Skill
	item  *Item
}

func (u *User) Fetch(userId int64) func(u *User) {
	return func(u *User) {
		// データを取ってくる感じの処理
		// user := getUser(userId)
		u.id = userId
	}
}

type Skill struct {
	id     int64
	detail *SkillDetail
}

func (s *Skill) Fetch() func(u *User) {
	return func(u *User) {
		// データを取ってくる感じの処理
		// skill := getSkill(u.id)
		s.id = 1
	}
}

type Item struct {
	id int64
}

func (i *Item) Fetch() func(u *User) {
	return func(u *User) {
		// データを取ってくる感じの処理
		// item := getItem(u.id)
		i.id = 1
	}
}

type SkillDetail struct {
	atk int64
}

func (sd *SkillDetail) Fetch() func(u *User) {
	return func(u *User) {
		// データを取ってくる感じの処理
		// sd := getSkillDetail(u.skill.id)
		sd.atk = 1000
	}
}

func (u *User) Build(fetchLists ...func(u *User)) {
	for _, fetch := range fetchLists {
		fetch(u)
	}
}

func New(user *User) {
	user.Build(
		user.Fetch(1),
		user.skill.Fetch(),
		user.skill.detail.Fetch(),
		user.item.Fetch(),
	)

	fmt.Printf("%+v", user)
}

func main() {
	New(&User{
		skill: &Skill{
			detail: &SkillDetail{},
		},
		item: &Item{},
	})
}

factory

構造体の初期化をfactoryに移譲するパターン。
各factoryで初期化される構造体は別物だが、同じような振る舞いを持つ構造体であれば、下記のようにinterfaceを利用し、createメソッドを抽象化すれば、疎結合にできる。

package main

import "fmt"

type Employee struct {
	Name, Position string
}

func (e *Employee) setName(name string) {
	e.Name = name
}

type NewEmployeeFactory interface {
	create() *Employee
}

type DeveloperFactory struct {}
func (DeveloperFactory) create() *Employee {
	return &Employee{"", "Developer"}
}

type ManagerFactory struct {}
func (ManagerFactory) create() *Employee {
	return &Employee{"", "Manager"}
}

func NewEmployee(factory NewEmployeeFactory) *Employee {
	return factory.create()
}
func main() {
	developerFactory := DeveloperFactory{}
	dev := NewEmployee(developerFactory)
	dev.setName("Sam")
	managerFactory := ManagerFactory{}
	manager := NewEmployee(managerFactory)
	manager.setName("Bob")

	fmt.Println(dev)
	fmt.Println(manager)
}

decorator

構造体にいろんな機能をデコレーションするかのように追加できるパターン。
構造体の中にinterfaceがあるのがポイント。interfaceを満たしていれば、デコレートし続けることができる

package main
import "fmt"

type Shape interface {
	Render() string
}
type Circle struct {
	Radius float32
}
func (c *Circle) Render() string {
	return fmt.Sprintf("Circle of radius %f",
		c.Radius)
}

type ColoredShape struct {
	Shape Shape
	Color string
}
func (c *ColoredShape) Render() string {
	return fmt.Sprintf("%s has the color %s",
		c.Shape.Render(), c.Color)
}
type TransparentShape struct {
	Shape Shape
	Transparency float32
}
func (t *TransparentShape) Render() string {
	return fmt.Sprintf("%s has %f%% transparency",
		t.Shape.Render(), t.Transparency * 100.0)
}

func main() {
	circle := Circle{2}
	fmt.Println(circle.Render())
	redCircle := ColoredShape{&circle, "Red"}
	fmt.Println(redCircle.Render())
	rhsCircle := TransparentShape{&redCircle, 0.5}
	fmt.Println(rhsCircle.Render())
}

state

状態を構造体として表すパターン。

  • On/Offの状態を構造体で持つ。On構造体はOffメソッド、Off構造体はOnメソッドを実装している。
  • Offの状態の時のOffメソッド、Onの状態の時のOnメソッドの実行は、BaseState構造体のOn/Offメソッドでカバー。
  • 状態を構造体で表し、状態遷移をメソッドで表すことで、if文を使うことなく、stateを扱えている。
package main
import "fmt"

type Switch struct {
	State State
}
func NewSwitch() *Switch {
	return &Switch{NewOffState()}
}
func (s *Switch) On() {
	s.State.On(s)
}
func (s *Switch) Off() {
	s.State.Off(s)
}
type State interface {
	On(sw *Switch)
	Off(sw *Switch)
}
type BaseState struct {}
func (s *BaseState) On(sw *Switch) {
	fmt.Println("Light is already on")
}
func (s *BaseState) Off(sw *Switch) {
	fmt.Println("Light is already off")
}
type OnState struct {
	BaseState
}
func NewOnState() *OnState {
	fmt.Println("Light turned on")
	return &OnState{BaseState{}}
}
func (o *OnState) Off(sw *Switch) {
	fmt.Println("Turning light off...")
	sw.State = NewOffState()
}
type OffState struct {
	BaseState
}
func NewOffState() *OffState {
	fmt.Println("Light turned off")
	return &OffState{BaseState{}}
}
func (o *OffState) On(sw *Switch) {
	fmt.Println("Turning light on...")
	sw.State = NewOnState()
}
func main() {
	sw := NewSwitch()
	sw.On()
	sw.Off()
	sw.Off()
}

strategy

機能の切り替えを容易にするパターン。
似たような機能が複数あったとして、その各機能のinterfaceを揃えてあげれば、具象的な処理を切り替えるだけでストラテジパターンが作れる。

  • MarkdownListStrategy、HtmlListStrategyから生えてるメソッドでmarkdown、htmlの出力がされる。
  • MarkdownListStrategy、HtmlListStrategyはListStrategyのinterfaceを満たしている。
  • MarkdownListStrategy、HtmlListStrategyのどちらを使うかはSetOutputFormatで切り替えれる。
  • AppendListメソッドでbufferに出力内容をためていく。二つのStrategyがListStrategyを満たしているため、ここの処理は共通化できる。

package main
import (
	"fmt"
	"strings"
)
type OutputFormat int
const (
	Markdown OutputFormat = iota
	Html
)
type ListStrategy interface {
	Start(builder *strings.Builder)
	End(builder *strings.Builder)
	AddListItem(builder *strings.Builder, item string)
}
type MarkdownListStrategy struct {}
func (m *MarkdownListStrategy) Start(builder *strings.Builder) {}
func (m *MarkdownListStrategy) End(builder *strings.Builder) {}
func (m *MarkdownListStrategy) AddListItem(
	builder *strings.Builder, item string) {
	builder.WriteString(" * " + item + "\n")
}
type HtmlListStrategy struct {}
func (h *HtmlListStrategy) Start(builder *strings.Builder) {
	builder.WriteString("<ul>\n")
}
func (h *HtmlListStrategy) End(builder *strings.Builder) {
	builder.WriteString("</ul>\n")
}
func (h *HtmlListStrategy) AddListItem(builder *strings.Builder, item string) {
	builder.WriteString("  <li>" + item + "</li>\n")
}
type TextProcessor struct {
	builder strings.Builder
	listStrategy ListStrategy
}
func NewTextProcessor(listStrategy ListStrategy) *TextProcessor {
	return &TextProcessor{strings.Builder{}, listStrategy}
}
func (t *TextProcessor) SetOutputFormat(fmt OutputFormat) {
	switch fmt {
	case Markdown:
		t.listStrategy = &MarkdownListStrategy{}
	case Html:
		t.listStrategy = &HtmlListStrategy{}
	}
}
func (t *TextProcessor) AppendList(items []string) {
	t.listStrategy.Start(&t.builder)
	for _, item := range items {
		t.listStrategy.AddListItem(&t.builder, item)
	}
	t.listStrategy.End(&t.builder)
}
func (t *TextProcessor) Reset() {
	t.builder.Reset()
}
func (t *TextProcessor) String() string {
	return t.builder.String()
}
func main() {
	tp := NewTextProcessor(&MarkdownListStrategy{})
	tp.AppendList([]string{ "foo", "bar", "baz" })
	fmt.Println(tp)
	tp.Reset()
	tp.SetOutputFormat(Html)
	tp.AppendList([]string{ "foo", "bar", "baz" })
	fmt.Println(tp)
}

proxy

処理の前にある処理を挟みたい場合のパターン。interfaceの差異の吸収だったり、バリデーションだったり。
実際に実行したいメソッド名とプロキシのメソッド名は合わせる。利用者側にはプロキシが挟まっていることを意識させない。


package proxy
import "fmt"

type Driven interface {
  Drive()
}
type Car struct {}
func (c *Car) Drive() {
  fmt.Println("Car being driven")
}
type Driver struct {
  Age int
}
type CarProxy struct {
  car Car
  driver *Driver
}
func (c *CarProxy) Drive() {
  if c.driver.Age >= 18 {
    c.car.Drive()
  } else {
    fmt.Println("Driver too young")
  }
}
func NewCarProxy(driver *Driver) *CarProxy {
  return &CarProxy{Car{}, driver}
}
func main() {
  car := NewCarProxy(&Driver{12})
  car.Drive()
}

再帰的な構造体

構造体の中にinterfaceを定義することで、再帰的な構造体が作れる。

  • AdditionalExpressionとDoubleExpressionはleft/rightにExpressionのinterfaceを持つ。
  • AdditionalExpressionとDoubleExpression自体がExpressionのinterfaceを満たしているので、left/rightを再帰的にすることができる

package main
import (
	"fmt"
	"strings"
)
type Expression interface {
	Print(sb *strings.Builder)
}
type DoubleExpression struct {
	value float64
}
func (d *DoubleExpression) Print(sb *strings.Builder) {
	sb.WriteString(fmt.Sprintf("%g", d.value))
}
type AdditionExpression struct {
	left, right Expression
}
func (a *AdditionExpression) Print(sb *strings.Builder) {
	sb.WriteString("(")
	a.left.Print(sb)
	sb.WriteString("+")
	a.right.Print(sb)
	sb.WriteString(")")
}
func main() {
	e := AdditionExpression{
		&DoubleExpression{1},
		&AdditionExpression{
			left:  &DoubleExpression{2},
			right: &DoubleExpression{3},
		},
	}
	sb := strings.Builder{}
	e.Print(&sb)
	fmt.Println(sb.String()) // 1+(2+3)
}
1
7
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
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?