缘起

最近复习设计模式
拜读谭勇德的<<设计模式就该这样学>>
本系列笔记拟采用golang练习之

状态模式

  1. 状态模式(State Pattern)也叫作状态机模式(State Machine Pattern),
  2. 允许对象在内部状态发生改变时改变它的行为,
  3. 对象看起来好像修改了它的类,
  4. 属于行为型设计模式。
  5. 状态模式主要包含3个角色。
  6. 1)环境类角色(Context):定义客户端需要的接口,内部维护一个当前状态实例,并负责具体状态的切换。
  7. 2)抽象状态角色(IState):定义该状态下的行为,可以有一个或多个行为。
  8. 3)具体状态角色(ConcreteState):具体实现该状态对应的行为,并且在需要的情况下进行状态切换。
  9. (摘自 谭勇德 <<设计模式就该这样学>>)

场景

  • 某业务系统, 需要对接某对象存储系统, 以统一管理大量的文件数据
  • 由于该对象存储系统只提供了http api文档, 因此需要自行封装文件读写api
  • 码农王二狗接到Leader张阿蛋下达的开发任务, 啃了几遍文档, 感觉胸有成竹
    • 王二狗: 张哥, 这个读写api, 是不是就写一个门面, 一个方法封装一个api就行了, 感觉功能不复杂
    • 张阿蛋: 基本上是这样的. 不过你有没有注意到, 文档上有注意事项, 就是打开文件的时候, 必须确定文件的使用方式?
    • 王二狗: 啥? 哦…我看看, 还真是有这么一条, 不过这个有影响吗, 不就是多传一个参数吗?
    • 张阿蛋: 当然有影响, 如果你以读方式打开, 那么后续是不接受写入的. 如果你以写方式打开, 后续是不能读取的.
    • 王二狗: 哦, 那估计要好好封装一下http api的报错信息?
    • 张阿蛋: 这个是肯定要的, 通讯难免有出错的时候. 但是, 也许我们可以用状态模式, 提前拒绝一些非法操作
    • 王二狗: …不明觉厉, 能对我直接点吗?
    • 张阿蛋: 将文件流对象的方法委托给”未打开”, “读取中”, “写入中”, 和”已关闭”四种状态. 每种状态只响应当前状态下允许的操作, 其他操作直接返回error.
    • 王二狗: 张哥, 强!

设计

  • IFileStream: 文件流API接口
  • iFileStreamContext: 文件流上下文接口, 仅内部使用
  • iFileStreamState: 文件流状态接口, 仅内部使用
  • tMockFileStream: 虚拟的文件流API实现类, 实现IFileStream接口
  • tDefaultState: 默认状态 - 未打开状态. 该状态下只允许打开/关闭操作
  • tReadingState: 读取状态. 该状态下只允许读取/关闭操作
  • tWritingState: 写入状态. 该状态下只允许写入/关闭操作
  • tClosedState: 关闭状态. 该状态下只允许关闭操作

单元测试

state_pattern_test.go

  1. package behavioral_patterns
  2. import (
  3. "learning/gooop/behavioral_patterns/state"
  4. "testing"
  5. )
  6. func Test_StatePattern(t *testing.T) {
  7. fnTestFileStream := func(fs state.IFileStream, readonly bool) {
  8. if readonly {
  9. e := fs.OpenRead()
  10. if e != nil {
  11. t.Log(e)
  12. }
  13. e = fs.OpenWrite()
  14. if e != nil {
  15. t.Log(e)
  16. }
  17. } else {
  18. e := fs.OpenWrite()
  19. if e != nil {
  20. t.Log(e)
  21. }
  22. e = fs.OpenRead()
  23. if e != nil {
  24. t.Log(e)
  25. }
  26. }
  27. buffer := make([]byte, 8192)
  28. n,e := fs.Read(buffer)
  29. if e != nil {
  30. t.Log(e)
  31. } else {
  32. t.Logf("%v bytes read", n)
  33. }
  34. n, e = fs.Write(buffer)
  35. if e != nil {
  36. t.Log(e)
  37. } else {
  38. t.Logf("%v bytes written", n)
  39. }
  40. e = fs.Close()
  41. if e != nil {
  42. t.Log(e)
  43. }
  44. }
  45. fnTestFileStream(state.NewMockFileStream("read-only.txt"), true)
  46. fnTestFileStream(state.NewMockFileStream("write-only.txt"), false)
  47. }

测试输出

  1. $ go test -v state_pattern_test.go
  2. === RUN Test_StatePattern
  3. tDefaultState.OpenRead, file=read-only.txt
  4. tMockFileStream.Switch, *state.tDefaultState => *state.tReadingState
  5. state_pattern_test.go:18: tReadingState.OpenWrite, already reading read-only.txt
  6. tReadingState.Read, file=read-only.txt, iBytesRead=8192
  7. state_pattern_test.go:37: 8192 bytes read
  8. state_pattern_test.go:42: tReadingState.Write, cannot write to read-only.txt
  9. tReadingState.Close, file=read-only.txt, iBytesRead=8192
  10. tMockFileStream.Switch, *state.tReadingState => *state.tClosedState
  11. tDefaultState.OpenWrite, file=write-only.txt
  12. tMockFileStream.Switch, *state.tDefaultState => *state.tWritingState
  13. state_pattern_test.go:28: tWritingState.OpenRead, already writing write-only.txt
  14. state_pattern_test.go:35: tWritingState.Read, cannot read write-only.txt
  15. tWritingState.Write, file=write-only.txt, written=8192
  16. state_pattern_test.go:44: 8192 bytes written
  17. tWritingState.Close, file=write-only.txt, written=8192
  18. tMockFileStream.Switch, *state.tWritingState => *state.tClosedState
  19. --- PASS: Test_StatePattern (0.00s)
  20. PASS
  21. ok command-line-arguments 0.002s

IFileStream.go

文件流API接口

  1. package state
  2. import "io"
  3. type IFileStream interface {
  4. io.ReadWriteCloser
  5. OpenRead() error
  6. OpenWrite() error
  7. }

iFileStreamContext.go

文件流上下文接口, 仅内部使用

  1. package state
  2. type iFileStreamContext interface {
  3. File() string
  4. Switch(state iFileStreamState)
  5. }

iFileStreamState.go

文件流状态接口, 仅内部使用

  1. package state
  2. type iFileStreamState interface {
  3. IFileStream
  4. }

tMockFileStream.go

虚拟的文件流API实现类, 实现IFileStream接口

  1. package state
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type tMockFileStream struct {
  7. state iFileStreamState
  8. file string
  9. }
  10. func NewMockFileStream(file string) IFileStream {
  11. fs := &tMockFileStream{
  12. nil,
  13. file,
  14. }
  15. fs.state = newDefaultState(fs)
  16. return fs
  17. }
  18. func (me *tMockFileStream) File() string {
  19. return me.file
  20. }
  21. func (me *tMockFileStream) Switch(st iFileStreamState) {
  22. fmt.Printf("tMockFileStream.Switch, %s => %s\n", reflect.TypeOf(me.state).String(), reflect.TypeOf(st).String())
  23. me.state = st
  24. }
  25. func (me *tMockFileStream) OpenRead() error {
  26. return me.state.OpenRead()
  27. }
  28. func (me *tMockFileStream) OpenWrite() error {
  29. return me.state.OpenWrite()
  30. }
  31. func (me *tMockFileStream) Read(p []byte) (n int, e error) {
  32. return me.state.Read(p)
  33. }
  34. func (me *tMockFileStream) Write(p []byte) (n int, e error) {
  35. return me.state.Write(p)
  36. }
  37. func (me *tMockFileStream) Close() error {
  38. return me.state.Close()
  39. }

tDefaultState.go

默认状态 - 未打开状态. 该状态下只允许打开/关闭操作

  1. package state
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. type tDefaultState struct {
  7. context iFileStreamContext
  8. }
  9. func newDefaultState(context iFileStreamContext) iFileStreamState {
  10. return &tDefaultState{
  11. context,
  12. }
  13. }
  14. func (me *tDefaultState) OpenRead() error {
  15. fmt.Printf("tDefaultState.OpenRead, file=%s\n", me.context.File())
  16. me.context.Switch(newReadingState(me.context))
  17. return nil
  18. }
  19. func (me *tDefaultState) OpenWrite() error {
  20. fmt.Printf("tDefaultState.OpenWrite, file=%s\n", me.context.File())
  21. me.context.Switch(newWritingState(me.context))
  22. return nil
  23. }
  24. func (me *tDefaultState) Read(p []byte) (n int, err error) {
  25. return 0, errors.New(fmt.Sprintf("tDefaultState.Read, file not opened, %s", me.context.File()))
  26. }
  27. func (me *tDefaultState) Write(p []byte) (n int, err error) {
  28. return 0, errors.New(fmt.Sprintf("tDefaultState.Write, file not opened, %s", me.context.File()))
  29. }
  30. func (me *tDefaultState) Close() error {
  31. fmt.Printf("tDefaultState.Close, file=%s\n", me.context.File())
  32. me.context.Switch(newClosedState(me.context))
  33. return nil
  34. }

tReadingState.go

读取状态. 该状态下只允许读取/关闭操作

  1. package state
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. type tReadingState struct {
  7. context iFileStreamContext
  8. iBytesRead int
  9. }
  10. func newReadingState(context iFileStreamContext) iFileStreamState {
  11. return &tReadingState{
  12. context,
  13. 0,
  14. }
  15. }
  16. func (me *tReadingState) OpenRead() error {
  17. return errors.New(fmt.Sprintf("tReadingState.OpenRead, already reading %s", me.context.File()))
  18. }
  19. func (me *tReadingState) OpenWrite() error {
  20. return errors.New(fmt.Sprintf("tReadingState.OpenWrite, already reading %s", me.context.File()))
  21. }
  22. func (me *tReadingState) Read(p []byte) (n int, err error) {
  23. size := len(p)
  24. me.iBytesRead += size
  25. fmt.Printf("tReadingState.Read, file=%s, iBytesRead=%v\n", me.context.File(), me.iBytesRead)
  26. return size, nil
  27. }
  28. func (me *tReadingState) Write(p []byte) (n int, err error) {
  29. return 0, errors.New(fmt.Sprintf("tReadingState.Write, cannot write to %s", me.context.File()))
  30. }
  31. func (me *tReadingState) Close() error {
  32. fmt.Printf("tReadingState.Close, file=%s, iBytesRead=%v\n", me.context.File(), me.iBytesRead)
  33. me.context.Switch(newClosedState(me.context))
  34. return nil
  35. }

tWritingState.go

写入状态. 该状态下只允许写入/关闭操作

  1. package state
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. type tWritingState struct {
  7. context iFileStreamContext
  8. written int
  9. }
  10. func newWritingState(context iFileStreamContext) iFileStreamState {
  11. return &tWritingState{
  12. context,
  13. 0,
  14. }
  15. }
  16. func (me *tWritingState) OpenRead() error {
  17. return errors.New(fmt.Sprintf("tWritingState.OpenRead, already writing %s", me.context.File()))
  18. }
  19. func (me *tWritingState) OpenWrite() error {
  20. return errors.New(fmt.Sprintf("tWritingState.OpenWrite, already writing %s", me.context.File()))
  21. }
  22. func (me *tWritingState) Read(p []byte) (n int, err error) {
  23. return 0, errors.New(fmt.Sprintf("tWritingState.Read, cannot read %s", me.context.File()))
  24. }
  25. func (me *tWritingState) Write(p []byte) (n int, err error) {
  26. size := len(p)
  27. me.written += size
  28. fmt.Printf("tWritingState.Write, file=%s, written=%v\n", me.context.File(), me.written)
  29. return size, nil
  30. }
  31. func (me *tWritingState) Close() error {
  32. fmt.Printf("tWritingState.Close, file=%s, written=%v\n", me.context.File(), me.written)
  33. me.context.Switch(newClosedState(me.context))
  34. return nil
  35. }

tClosedState.go

关闭状态. 该状态下只允许关闭操作

  1. package state
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. type tClosedState struct {
  7. context iFileStreamContext
  8. }
  9. func newClosedState(context iFileStreamContext) iFileStreamState {
  10. return &tClosedState{
  11. context,
  12. }
  13. }
  14. func (me *tClosedState) OpenRead() error {
  15. return errors.New(fmt.Sprintf("tClosedState.OpenRead, file(%s) already closed ", me.context.File()))
  16. }
  17. func (me *tClosedState) OpenWrite() error {
  18. return errors.New(fmt.Sprintf("tClosedState.OpenWrite, file(%s) already closed ", me.context.File()))
  19. }
  20. func (me *tClosedState) Read(p []byte) (n int, e error) {
  21. return 0, errors.New(fmt.Sprintf("tClosedState.Read, file(%s) already closed ", me.context.File()))
  22. }
  23. func (me *tClosedState) Write(p []byte) (n int, e error) {
  24. return 0, errors.New(fmt.Sprintf("tClosedState.Write, file(%s) already closed ", me.context.File()))
  25. }
  26. func (me *tClosedState) Close() error {
  27. return nil
  28. }

状态模式小结

  1. 状态模式的优点
  2. 1)结构清晰:将状态独立为类,消除了冗余的if...elseswitch...case语句。
  3. 2)将状态转换显示化:通常对象内部都是使用数值类型来定义状态的,
  4. 状态的切换通过赋值进行表现,不够直观;
  5. 而使用状态类,当切换状态时,是以不同的类进行表示的,转换目的更加明确。
  6. 3)状态类职责明确且具备扩展性。
  7. 状态模式的缺点
  8. 1)类膨胀:如果一个事物具备很多状态,则会造成状态类太多。
  9. 2)状态模式的结构与实现都较为复杂,如果使用不当,将导致程序结构和代码的混乱。
  10. 3)状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,
  11. 增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,
  12. 而且修改某个状态类的行为也需要修改对应类的源码。
  13. (摘自 谭勇德 <<设计模式就该这样学>>)

(end)