经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » 设计模式 » 查看文章
浅析访问者模式
来源:cnblogs  作者:lubanseven  时间:2023/7/31 9:11:22  对本文有异议

0. 前言

在阅读 Kubernetes: kubectl 源码时看到有关访问者设计模式的运用。访问者模式是行为型设计模式的一种,本篇文章将对访问者模式做一个介绍。

1. 访问者模式

1.1 示例

首先,给出一个比较粗糙的示例。

实现程序,功能如下:

  1. 当男人成功时,显示我有一个好老婆;当女人成功时,显示我有一个有爱的丈夫;
  2. 当男人开心时,显示我有一个玩具;当女人开心时,显示我有一个有爱的丈夫;
  3. 当男人伤心时,显示我丢了玩具;当女人伤心时,显示我丢了有爱的丈夫;

基于上述描述,实现示例代码:

  1. type man struct {
  2. action string
  3. }
  4. type woman struct {
  5. action string
  6. }
  7. func (m *man) status() {
  8. if m.action == "happy" {
  9. fmt.Println("I have a toy")
  10. }
  11. if m.action == "sad" {
  12. fmt.Println("I lost my toy")
  13. }
  14. if m.action == "success" {
  15. fmt.Println("I have a great wife")
  16. }
  17. }
  18. func (w *woman) status() {
  19. if w.action == "happy" {
  20. fmt.Println("I have a lovely husband")
  21. }
  22. if w.action == "sad" {
  23. fmt.Println("I lost my lovely husband")
  24. }
  25. if w.action == "success" {
  26. fmt.Println("I have a lovely husband")
  27. }
  28. }
  29. func main() {
  30. m := man{
  31. action: "sad",
  32. }
  33. m.status()
  34. }

示例代码实现了想要的功能。
可以看出,男人和女人类都具有 action 且行为不一样。

那么,如果增加 action,比如当男人恋爱时,显示我终于脱单了;当女人恋爱时,显示终于有人爱我了;就需要更新男人和女人类的 status 方法,这不符合开闭原则。

1.2 解耦对象和行为

既然不符合开闭原则,那怎么样才能解耦对象和行为,使得行为的变化不会影响到对象实现呢?

进一步看上述实现,男人和女人类是行为的主体,行为有成功时的行为,开心时的行为,伤心时的行为...

既然是解耦,那先把行为拿出来作为接口,在以具体的行为类实现接口。看看怎么把行为类和对象类分开。

实现代码:

  1. type status interface {
  2. manStatus()
  3. womanStatus()
  4. }
  5. type happy struct{}
  6. type sad struct{}
  7. type success struct{}
  8. func (h *happy) manStatus() {
  9. fmt.Println("I have a toy")
  10. }
  11. func (s *sad) manStatus() {
  12. fmt.Println("I lost my toy")
  13. }
  14. func (s *success) manStatus() {
  15. fmt.Println("I have a great wife")
  16. }
  17. func (h *happy) womanStatus() {
  18. fmt.Println("I have a lovely husband")
  19. }
  20. func (s *sad) womanStatus() {
  21. fmt.Println("I lost my lovely husband")
  22. }
  23. func (s *success) womanStatus() {
  24. fmt.Println("I have a lovely husband")
  25. }
  26. type man struct {}
  27. type woman struct {}

这里把对象类的 action 属性拿掉,相应的把 action 转换为行为类。行为类实现了接口,其中定义了 manStatus()womanStatus() 方法。

这样我们拆成了两个类,对象类和行为类。怎么把这两个类联系起来呢?
看代码,男人类和男人类的行为,女人类和女人类的行为是有联系的,初始化对象类和行为类:

  1. m := man{}
  2. s := sad{}
  3. // m.demo(s)

应该通过 demo 将对象和行为类联系起来,然后调用 smanStatus()方法完成关联:

  1. func (m *man) accept(s status) {
  2. s.manStatus()
  3. }
  4. func (w *woman) accept(s status) {
  5. s.womanStatus()
  6. }
  7. func main() {
  8. m := &man{}
  9. s := &sad{}
  10. m.accept(s)
  11. }

demo 重命名为 accept 作为对象类的方法,在其中调用行为类的方法。

1.3 访问者模式

至此,我们已经实现了访问者模式。有人可能会问了,我怎么还是没看出来?我们在分析上述代码。

有两个类,对象类和行为类。
对象类实现 accept 方法,其是动作的主体,将对象类和行为类关联起来。
行为类根据不同对象实现行为方法,其是行为的主体,行为是建立在对象之上的。

基于上述分析,我们继续改造代码:

  1. type visitor interface {
  2. manStatus()
  3. womanStatus()
  4. }
  5. type happyVisitor struct{}
  6. type sadVisitor struct{}
  7. type successVisitor struct{}
  8. func (h *happyVisitor) manStatus() {
  9. fmt.Println("I have a toy")
  10. }
  11. func (s *sadVisitor) manStatus() {
  12. fmt.Println("I lost my toy")
  13. }
  14. func (s *successVisitor) manStatus() {
  15. fmt.Println("I have a great wife")
  16. }
  17. func (h *happyVisitor) womanStatus() {
  18. fmt.Println("I have a lovely husband")
  19. }
  20. func (s *sadVisitor) womanStatus() {
  21. fmt.Println("I lost my lovely husband")
  22. }
  23. func (s *successVisitor) womanStatus() {
  24. fmt.Println("I have a lovely husband")
  25. }
  26. type man struct {}
  27. type woman struct {}
  28. func (m *man) accept(s visitor) {
  29. s.manStatus()
  30. }
  31. func (w *woman) accept(s visitor) {
  32. s.womanStatus()
  33. }

结构基本没变,只是重命名了一下,更容易分清主体。

上述示例对于行为类相比于对象类是主的体验还不明显,重新改写代码:

  1. type visitor interface {
  2. manStatus(*man)
  3. womanStatus(*woman)
  4. }
  5. type happyVisitor struct{}
  6. type sadVisitor struct{}
  7. type successVisitor struct{}
  8. func (h *happyVisitor) manStatus(m *man) {
  9. fmt.Println(m.name, "I have a toy")
  10. }
  11. func (s *sadVisitor) manStatus(m *man) {
  12. fmt.Println(m.name, "I lost my toy")
  13. }
  14. func (s *successVisitor) manStatus(m *man) {
  15. fmt.Println(m.name, "I have a great wife")
  16. }
  17. type man struct {
  18. name string
  19. }
  20. func (m *man) accept(s visitor) {
  21. s.manStatus(m)
  22. }
  23. func main() {
  24. m := &man{"hxia"}
  25. s := &sadVisitor{}
  26. m.accept(s)
  27. }

改写后的代码,行为类会访问对象类的 name 属性,对于行为类来说,对象类就是数据,是属性。

基于此,画出访问者设计模式的 UML 图:
image

访问者模式在 GoF 合著的《设计模式:可复用面向对象软件的基础》中的定义是:

  1. 允许一个或多个操作应用到一组对象上,解耦操作和对象本身
  2. Allows for one or more operation to be applied to a set of objects at runtime,
  3. decoupling the operations from the object structure

2. VisitorFunc 和访问者模式

前面介绍了访问者模式,从定义看访问者模式通过将一个或多个操作应用到一组对象上,以实现对象和操作的解耦。这里需要重点关注的点是一组对象。

一组意味着对象具有相似性,且结构是稳定的。试想如果男人和女人类中,女人没有伤心时的行为,那就没办法将其归为一组对象,或者需要实现 fake 伤心以保持对象的相似性。

既然定义的是一组,当然对象也可以是一个。假定对象是一个,使用函数 Visitor 改写上述访问者模式代码:

  1. type VisitorFunc func(man)
  2. func happyVisitor(m man) {
  3. fmt.Println(m.name, "I have a good thing")
  4. }
  5. func sadVisitor(m man) {
  6. fmt.Println(m.name, "I have a bad thing")
  7. }
  8. func successVisitor(m man) {
  9. fmt.Println(m.name, "I finish a thing")
  10. }
  11. type man struct {
  12. name string
  13. }
  14. func (m man) accept(v VisitorFunc) {
  15. v(m)
  16. }
  17. func main() {
  18. m := man{"hxia"}
  19. m.accept(sadVisitor)
  20. }

这么改写在于简单,去掉类取而代之的是函数,用函数实现了具体的 Visitor。当然,这样的 Visitor 只能处理一种操作。

类似地,如果一组对象的行为是一样的,也可以用函数 Visitor 来实现:

  1. type VisitorFunc func(person)
  2. func happyVisitor(p person) {
  3. fmt.Println(p.getName(), "I have a good thing")
  4. }
  5. func sadVisitor(p person) {
  6. fmt.Println(p.getName(), "I have a bad thing")
  7. }
  8. func successVisitor(p person) {
  9. fmt.Println(p.getName(), "I finish a thing")
  10. }
  11. type person interface {
  12. getName() string
  13. }
  14. type man struct {
  15. name string
  16. }
  17. type woman struct {
  18. name string
  19. }
  20. func (m man) getName() string {
  21. return m.name
  22. }
  23. func (w woman) getName() string {
  24. return w.name
  25. }
  26. func (m *man) accept(v VisitorFunc) {
  27. v(m)
  28. }
  29. func (w *woman) accept(v VisitorFunc) {
  30. v(w)
  31. }
  32. func main() {
  33. m := &man{"hxia"}
  34. m.accept(sadVisitor)
  35. }

2.1 嵌套 Visitor

如果操作作用于一个对象,可以用函数 Visitor 来简化实现。如果多个操作嵌套的作用于对象上,那么可以使用嵌套 Visitor 实现,其效果类似于多个小应用访问数据库以实现某个功能。

代码示例如下:

  1. type VisitorFunc func(*man) error
  2. func happyVisitor(m *man) error {
  3. fmt.Println(m.name)
  4. return nil
  5. }
  6. func sadVisitor(m *man) error {
  7. fmt.Println(m.age)
  8. return nil
  9. }
  10. func successVisitor(m *man) error {
  11. fmt.Println(m.sex)
  12. return nil
  13. }
  14. func validationFunc(m *man) error {
  15. if m.name == "" {
  16. return errors.New("empty name")
  17. }
  18. return nil
  19. }
  20. type man struct {
  21. name string
  22. age int
  23. sex string
  24. }
  25. type Visitor interface {
  26. Visit(VisitorFunc) error
  27. }
  28. func (m *man) Visit(fn VisitorFunc) error {
  29. fmt.Println("in man")
  30. if err := fn(m); err != nil {
  31. return err
  32. }
  33. fmt.Println("out man")
  34. return nil
  35. }
  36. type validationVisitor struct {
  37. visitor Visitor
  38. }
  39. func (v validationVisitor) Visit(fn VisitorFunc) error {
  40. return v.visitor.Visit(func(m *man) error {
  41. fmt.Println("in validation")
  42. if m.name == "" {
  43. return errors.New("empty name")
  44. }
  45. if err := fn(m); err != nil {
  46. return err
  47. }
  48. fmt.Println("out validation")
  49. return nil
  50. })
  51. }
  52. type errorVisitor struct {
  53. visitor Visitor
  54. }
  55. func (v errorVisitor) Visit(fn VisitorFunc) error {
  56. return v.visitor.Visit(func(m *man) error {
  57. fmt.Println("in error")
  58. if err := fn(m); err != nil {
  59. return err
  60. }
  61. fmt.Println("out error")
  62. return nil
  63. })
  64. }
  65. type ageVisitor struct {
  66. visitor Visitor
  67. }
  68. func (v ageVisitor) Visit(fn VisitorFunc) error {
  69. return v.visitor.Visit(func(m *man) error {
  70. fmt.Println("in age")
  71. if err := fn(m); err != nil {
  72. return err
  73. }
  74. fmt.Println(m.name, m.age)
  75. fmt.Println("out age")
  76. return nil
  77. })
  78. }
  79. type VisitorList []Visitor
  80. func (l VisitorList) Visit(fn VisitorFunc) error {
  81. for i := range l {
  82. if err := l[i].Visit(fn); err != nil {
  83. return err
  84. }
  85. }
  86. return nil
  87. }
  88. func main() {
  89. var visitor Visitor
  90. m1 := &man{name: "hxia", age: 18}
  91. m2 := &man{name: "huyun", age: 29}
  92. m3 := &man{name: "troy", age: 25}
  93. visitors := []Visitor{m1, m2, m3}
  94. visitor = VisitorList(visitors)
  95. visitor = validationVisitor{visitor: visitor}
  96. visitor = errorVisitor{visitor: visitor}
  97. visitor = ageVisitor{visitor: visitor}
  98. visitor.Visit(happyVisitor)
  99. }

代码有点长,其基本是 Kubernetes:kubectl 访问者模式的主体,把它看懂了,再去看 kubectl 的访问者模式实现就不难了。

首先,对象实现了 Visitor (类似于上例的 accept),接受函数 Visitor,函数 Visitor 访问对象操作:

  1. func (m *man) Visit(fn VisitorFunc) error {
  2. ...
  3. }

接着,将多个对象装入 VisitorList,且该 VisitorList 也实现了 Visitor 接口方法。这么做是为了遍历访问每个对象:

  1. func (l VisitorList) Visit(fn VisitorFunc) error {
  2. for i := range l {
  3. if err := l[i].Visit(fn); err != nil {
  4. return err
  5. }
  6. }
  7. return nil
  8. }

然后,是在函数 Visitor 操作对象之后对对象做一些其它操作,这里定义了 validationVisitor 用来验证对象的名字是否为空:

  1. func (v validationVisitor) Visit(fn VisitorFunc) error {
  2. ...
  3. }
  4. // main
  5. visitor = validationVisitor{visitor: visitor}
  6. visitor.Visit(happyVisitor)

通过层层嵌套 Visitor 实现对象的嵌套操作。

这些代码了解了,再去看 Kubernetes:kubectl 应该不难了,代码在 这里

3. 总结

优点

  • 访问者模式适用于对象复杂且具有较多操作的场景,使用访问者模式可解耦对象和操作,简化对象职责。
  • 访问者模式侧重在访问者,对于访问者而言,多加几个访问者操作不影响对象的实现,符合开闭原则。

缺点

  • 由于访问者模式解耦了对象和它的操作,对象的属性暴露给访问者,打破了对象和操作的封装。
  • 访问者模式对对象不友好,如果对象结构不稳定,很难使用访问者模式。同时,如果要加入新对象,需要访问者接口,实现和对象都要改,不符合开闭原则。

4. 参考文章

访问者模式
GO 编程模式:K8S VISITOR 模式


原文链接:https://www.cnblogs.com/xingzheanan/p/17592248.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号