9.3.10 الگو Visitor

9.3.10 الگو Visitor

الگو Visitor…

9.3.10.1 مقدمه: #

الگوی طراحی Visitor یک الگوی طراحی Behavioural است که به شما امکان می‌دهد بدون تغییر در ساختار برنامه، رفتاری را به ساختار آن اضافه کنید.
بیایید الگوی Visitor را با یک مثال درک کنیم. فرض کنید شما نگهدارنده(maintainer) یک lib هستید که ساختارهای با شکل‌های متفاوتی دارد مانند:

  1. Square
  2. Circle
  3. Triangle

هر یک از ساختارهای شکل بالا یک شکل رابط مشترک را پیاده سازی می کند. تیم های زیادی در شرکت شما وجود دارند که از lib شما استفاده می کنند. حال فرض کنید یکی از تیم از شما می خواهد که یک رفتار دیگر (getArea()) به ساختارهای Shape اضافه کنید. در نتیجه گزینه های زیادی برای حل این مشکل وجود دارد.

راه حل اول: #

اولین گزینه ای که به ذهن می رسد اضافه کردن متد getArea() در interface مربوط به shape است و سپس هر ساختار shape می تواند متد getArea() را پیاده سازی کند. این به نظر بی اهمیت می رسد اما برخی از مشکلات وجود دارد:

  • به عنوان maintainer کتابخانه، نمی خواهید کد بسیار آزمایش شده کتابخانه خود را با افزودن رفتارهای اضافی تغییر دهید.
  • ممکن است تیم هایی که از کتابخانه شما استفاده می کنند درخواست بیشتری برای رفتارهای بیشتری مانند getNumSides()، getMiddleCoordinates(). سپس، در این مورد، شما نمی خواهید به اصلاح کتابخانه خود ادامه دهید. اما شما می خواهید که تیم های دیگر کتابخانه شما را بدون تغییر واقعی کد گسترش دهند.

راه حل دوم: #

گزینه دوم این است که تیمی که این ویژگی را درخواست می کند می تواند منطق رفتار را خودش بنویسد. بنابراین بر اساس نوع shape struct آنها کد زیر را در نظر دارند:

1if shape.type == square {
2  //Calculate area for squre
3} elseif shape.type == circle {
4   //Calculate area of triangle 
5} elseif shape.type == "triangle" {
6   //Calculate area of triangle
7} else {
8  //Raise error
9}

کد بالا نیز مشکل ساز است زیرا نمی توانید از مزایای کامل interface ها استفاده کنید و به جای آن یک بررسی explicit type که شکننده(fragile) است انجام دهید. دوم، دریافت type در زمان اجرا ممکن است تأثیری بر عملکرد داشته باشد یا حتی در برخی از زبان ها امکان پذیر نباشد.

راه حل سوم: #

گزینه سوم حل مشکل فوق با استفاده از الگوی visitor است. ما یکvisitor interface را مانند زیر تعریف می کنیم.

1type visitor interface {
2   visitForSquare(square)
3   visitForCircle(circle)
4   visitForTriangle(triangle)
5}

توابع visitforSquare(square)، visitForCircle(circle)، visitForTriangle(triangle) به ما اجازه می دهد تا به ترتیب قابلیت های Square، Circle و Triangle را اضافه کنیم.

حال سوالی که به ذهن می رسد این است که چرا نمی توانیم یک روش visit**(shape)** واحد در visitor interface داشته باشیم. دلیل اینکه ما این ویژگی را نداریم این است که GO و همچنین برخی از زبان های دیگر از method overloading پشتیبانی می کنند. بنابراین یک method متفاوت برای هر یک از ساختارها مورد نیاز است.

ما یک accept method را با signature زیر به shape interface اضافه می کنیم و هر یک از shape struct باید این متد را تعریف کنند.

1func accept(v visitor)

اما یک لحظه صبر کنید، ما فقط اشاره کردیم که نمی خواهیم shape structs موجود خود را تغییر دهیم. اما هنگام استفاده از Visitor Pattern باید shape structs خود را تغییر دهیم اما این اصلاح فقط یک بار انجام می شود. در صورت اضافه کردن هر رفتار اضافی مانند getNumSides()، getMiddleCoordinates() از همان تابع accept(v visitor) فوق بدون تغییر بیشتر در shape structs استفاده می کند. اساساً shape structs فقط باید یک بار اصلاح شوند و تمام درخواست‌های آتی رفتارهای اضافی با استفاده از همان تابع پذیرش بررسی می‌شوند. ببینیم چطور! ساختار مربع (square struct) یک accept method مانند زیر را اجرا می کند:

1func (obj *squre) accept(v visitor){
2    v.visitForSquare(obj)
3}

و به طور مشابه، دایره و مثلث نیز accept function را مانند بالا تعریف می کنند.

اکنون تیمی که رفتار getArea() را درخواست می‌کند، می‌تواند به سادگی concrete implementation را برای visitor interface را تعریف کند و منطق محاسبه ناحیه را در آن concrete implementation بنویسد.

areaCalculator.go

 1type areaCalculator struct{
 2    area int
 3}
 4
 5func (a *areaCalculator) visitForSquare(s *square){
 6    //Calculate are for square
 7}
 8func (a *areaCalculator) visitForCircle(s *square){
 9    //Calculate are for circle
10}
11func (a *areaCalculator) visitForTriangle(s *square){
12    //Calculate are for triangle

برای محاسبه مساحت یک مربع، ابتدا نمونه ای از مربعی که آنها به سادگی می توانند فراخوانی کنند ایجاد می کنیم.

1sq := &square{}
2ac := &areaCalculator{}
3sq.accept(ac)

به طور مشابه، تیم دیگری که برای رفتار getMiddleCoordinates() درخواست می‌کند، می‌تواند پیاده‌سازی concrete دیگری از  visitor interfaceمشابه با بالا تعریف کند.

middleCoordinates.go

 1type middleCoordinates struct {
 2    x int
 3    y int
 4}
 5
 6func (a *middleCoordinates) visitForSquare(s *square) {
 7    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
 8}
 9
10func (a *middleCoordinates) visitForCircle(c *circle) {
11    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
12}
13
14func (a *middleCoordinates) visitForTriangle(t *triangle) {
15    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
16}

UMLDiagram

در زیر نمودار mapping UML متناظر با مثال عملی shape struct و areaCalculator که در بالا ارائه کردیم آمده است.

UMLDiagram

9.3.10.2 # Mapping: #

جدول زیر mapping از اجزای مهم نمودار UML به اجزای واقعی implementation را در ‘مثال’ زیر نشان می دهد.

elementshape.go
Concrete Element Asquare.go
Concrete Element Bcircle.go
Concrete Element Crectangle.go
Visitorvisitor.go
Concrete Visitor 1areaCalculator.go
Concrete Visitor 2middleCoordinates.go
Clientmain.go

9.3.10.3 # مثال: #

shape.go

1package main
2
3type shape interface {
4    getType() string
5    accept(visitor)
6}

square.go

 1package main
 2
 3type square struct {
 4    side int
 5}
 6
 7func (s *square) accept(v visitor) {
 8    v.visitForSquare(s)
 9}
10
11func (s *square) getType() string {
12    return "Square"
13}

circle.go

 1package main
 2
 3type circle struct {
 4    radius int
 5}
 6
 7func (c *circle) accept(v visitor) {
 8    v.visitForCircle(c)
 9}
10
11func (c *circle) getType() string {
12    return "Circle"
13}

rectangle.go

 1package main
 2
 3type rectangle struct {
 4    l int
 5    b int
 6}
 7
 8func (t *rectangle) accept(v visitor) {
 9    v.visitForrectangle(t)
10}
11
12func (t *rectangle) getType() string {
13    return "rectangle"
14}

visitor.go

1package main
2
3type visitor interface {
4    visitForSquare(*square)
5    visitForCircle(*circle)
6    visitForrectangle(*rectangle)
7}

areaCalculator.go

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7type areaCalculator struct {
 8    area int
 9}
10
11func (a *areaCalculator) visitForSquare(s *square) {
12    //Calculate area for square. After calculating the area assign in to the area instance variable
13    fmt.Println("Calculating area for square")
14}
15
16func (a *areaCalculator) visitForCircle(s *circle) {
17    //Calculate are for circle. After calculating the area assign in to the area instance variable
18    fmt.Println("Calculating area for circle")
19}
20
21func (a *areaCalculator) visitForrectangle(s *rectangle) {
22    //Calculate are for rectangle. After calculating the area assign in to the area instance variable
23    fmt.Println("Calculating area for rectangle")
24}

middleCoordinates.go

 1package main
 2
 3import "fmt"
 4
 5type middleCoordinates struct {
 6    x int
 7    y int
 8}
 9
10func (a *middleCoordinates) visitForSquare(s *square) {
11    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
12    fmt.Println("Calculating middle point coordinates for square")
13}
14
15func (a *middleCoordinates) visitForCircle(c *circle) {
16    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
17    fmt.Println("Calculating middle point coordinates for circle")
18}
19
20func (a *middleCoordinates) visitForrectangle(t *rectangle) {
21    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
22    fmt.Println("Calculating middle point coordinates for rectangle")
23}

main.go

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    square := &square{side: 2}
 7    circle := &circle{radius: 3}
 8    rectangle := &rectangle{l: 2, b: 3}
 9   
10    areaCalculator := &areaCalculator{}
11    square.accept(areaCalculator)
12    circle.accept(areaCalculator)
13    rectangle.accept(areaCalculator)
14   
15    fmt.Println()
16    middleCoordinates := &middleCoordinates{}
17    square.accept(middleCoordinates)
18    circle.accept(middleCoordinates)
19    rectangle.accept(middleCoordinates)
20}

Output:

1Calculating area for square
2Calculating area for circle
3Calculating area for rectangle
4
5Calculating middle point coordinates for square
6Calculating middle point coordinates for circle
7Calculating middle point coordinates for rectangle

9.3.10.3 # پیاده سازی به صورت یک جا: #

  1package main
  2
  3import "fmt"
  4
  5type shape interface {
  6    getType() string
  7    accept(visitor)
  8}
  9
 10type square struct {
 11    side int
 12}
 13
 14func (s *square) accept(v visitor) {
 15    v.visitForSquare(s)
 16}
 17
 18func (s *square) getType() string {
 19    return "Square"
 20}
 21
 22type circle struct {
 23    radius int
 24}
 25
 26func (c *circle) accept(v visitor) {
 27    v.visitForCircle(c)
 28}
 29
 30func (c *circle) getType() string {
 31    return "Circle"
 32}
 33
 34type rectangle struct {
 35    l int
 36    b int
 37}
 38
 39func (t *rectangle) accept(v visitor) {
 40    v.visitForrectangle(t)
 41}
 42
 43func (t *rectangle) getType() string {
 44    return "rectangle"
 45}
 46
 47type visitor interface {
 48    visitForSquare(*square)
 49    visitForCircle(*circle)
 50    visitForrectangle(*rectangle)
 51}
 52
 53type areaCalculator struct {
 54    area int
 55}
 56
 57func (a *areaCalculator) visitForSquare(s *square) {
 58    //Calculate area for square. After calculating the area assign in to the area instance variable
 59    fmt.Println("Calculating area for square")
 60}
 61
 62func (a *areaCalculator) visitForCircle(s *circle) {
 63    //Calculate are for circle. After calculating the area assign in to the area instance variable
 64    fmt.Println("Calculating area for circle")
 65}
 66
 67func (a *areaCalculator) visitForrectangle(s *rectangle) {
 68    //Calculate are for rectangle. After calculating the area assign in to the area instance variable
 69    fmt.Println("Calculating area for rectangle")
 70}
 71
 72type middleCoordinates struct {
 73    x int
 74    y int
 75}
 76
 77func (a *middleCoordinates) visitForSquare(s *square) {
 78    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
 79    fmt.Println("Calculating middle point coordinates for square")
 80}
 81
 82func (a *middleCoordinates) visitForCircle(c *circle) {
 83    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
 84    fmt.Println("Calculating middle point coordinates for circle")
 85}
 86
 87func (a *middleCoordinates) visitForrectangle(t *rectangle) {
 88    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
 89    fmt.Println("Calculating middle point coordinates for rectangle")
 90}
 91
 92func main() {
 93    square := &square{side: 2}
 94    circle := &circle{radius: 3}
 95    rectangle := &rectangle{l: 2, b: 3}
 96    areaCalculator := &areaCalculator{}
 97    square.accept(areaCalculator)
 98    circle.accept(areaCalculator)
 99    rectangle.accept(areaCalculator)
100    
101    fmt.Println()
102    middleCoordinates := &middleCoordinates{}
103    square.accept(middleCoordinates)
104    circle.accept(middleCoordinates)
105    rectangle.accept(middleCoordinates)
106}

Output:

1Calculating area for square
2Calculating area for circle
3Calculating area for rectangle
4
5Calculating middle point coordinates for square
6Calculating middle point coordinates for circle
7Calculating middle point coordinates for rectangle