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 آنها کد زیر را در نظر دارند:

if shape.type == square {
  //Calculate area for squre
} elseif shape.type == circle {
   //Calculate area of triangle 
} elseif shape.type == "triangle" {
   //Calculate area of triangle
} else {
  //Raise error
}

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

راه حل سوم: #

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

type visitor interface {
   visitForSquare(square)
   visitForCircle(circle)
   visitForTriangle(triangle)
}

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

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

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

func accept(v visitor)

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

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

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

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

areaCalculator.go

type areaCalculator struct{
    area int
}

func (a *areaCalculator) visitForSquare(s *square){
    //Calculate are for square
}
func (a *areaCalculator) visitForCircle(s *square){
    //Calculate are for circle
}
func (a *areaCalculator) visitForTriangle(s *square){
    //Calculate are for triangle

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

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

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

middleCoordinates.go

type middleCoordinates struct {
    x int
    y int
}

func (a *middleCoordinates) visitForSquare(s *square) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
}

func (a *middleCoordinates) visitForCircle(c *circle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
}

func (a *middleCoordinates) visitForTriangle(t *triangle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
}

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

package main

type shape interface {
    getType() string
    accept(visitor)
}

square.go

package main

type square struct {
    side int
}

func (s *square) accept(v visitor) {
    v.visitForSquare(s)
}

func (s *square) getType() string {
    return "Square"
}

circle.go

package main

type circle struct {
    radius int
}

func (c *circle) accept(v visitor) {
    v.visitForCircle(c)
}

func (c *circle) getType() string {
    return "Circle"
}

rectangle.go

package main

type rectangle struct {
    l int
    b int
}

func (t *rectangle) accept(v visitor) {
    v.visitForrectangle(t)
}

func (t *rectangle) getType() string {
    return "rectangle"
}

visitor.go

package main

type visitor interface {
    visitForSquare(*square)
    visitForCircle(*circle)
    visitForrectangle(*rectangle)
}

areaCalculator.go

package main

import (
    "fmt"
)

type areaCalculator struct {
    area int
}

func (a *areaCalculator) visitForSquare(s *square) {
    //Calculate area for square. After calculating the area assign in to the area instance variable
    fmt.Println("Calculating area for square")
}

func (a *areaCalculator) visitForCircle(s *circle) {
    //Calculate are for circle. After calculating the area assign in to the area instance variable
    fmt.Println("Calculating area for circle")
}

func (a *areaCalculator) visitForrectangle(s *rectangle) {
    //Calculate are for rectangle. After calculating the area assign in to the area instance variable
    fmt.Println("Calculating area for rectangle")
}

middleCoordinates.go

package main

import "fmt"

type middleCoordinates struct {
    x int
    y int
}

func (a *middleCoordinates) visitForSquare(s *square) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
    fmt.Println("Calculating middle point coordinates for square")
}

func (a *middleCoordinates) visitForCircle(c *circle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
    fmt.Println("Calculating middle point coordinates for circle")
}

func (a *middleCoordinates) visitForrectangle(t *rectangle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
    fmt.Println("Calculating middle point coordinates for rectangle")
}

main.go

package main

import "fmt"

func main() {
    square := &square{side: 2}
    circle := &circle{radius: 3}
    rectangle := &rectangle{l: 2, b: 3}
   
    areaCalculator := &areaCalculator{}
    square.accept(areaCalculator)
    circle.accept(areaCalculator)
    rectangle.accept(areaCalculator)
   
    fmt.Println()
    middleCoordinates := &middleCoordinates{}
    square.accept(middleCoordinates)
    circle.accept(middleCoordinates)
    rectangle.accept(middleCoordinates)
}

Output:

Calculating area for square
Calculating area for circle
Calculating area for rectangle

Calculating middle point coordinates for square
Calculating middle point coordinates for circle
Calculating middle point coordinates for rectangle

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

package main

import "fmt"

type shape interface {
    getType() string
    accept(visitor)
}

type square struct {
    side int
}

func (s *square) accept(v visitor) {
    v.visitForSquare(s)
}

func (s *square) getType() string {
    return "Square"
}

type circle struct {
    radius int
}

func (c *circle) accept(v visitor) {
    v.visitForCircle(c)
}

func (c *circle) getType() string {
    return "Circle"
}

type rectangle struct {
    l int
    b int
}

func (t *rectangle) accept(v visitor) {
    v.visitForrectangle(t)
}

func (t *rectangle) getType() string {
    return "rectangle"
}

type visitor interface {
    visitForSquare(*square)
    visitForCircle(*circle)
    visitForrectangle(*rectangle)
}

type areaCalculator struct {
    area int
}

func (a *areaCalculator) visitForSquare(s *square) {
    //Calculate area for square. After calculating the area assign in to the area instance variable
    fmt.Println("Calculating area for square")
}

func (a *areaCalculator) visitForCircle(s *circle) {
    //Calculate are for circle. After calculating the area assign in to the area instance variable
    fmt.Println("Calculating area for circle")
}

func (a *areaCalculator) visitForrectangle(s *rectangle) {
    //Calculate are for rectangle. After calculating the area assign in to the area instance variable
    fmt.Println("Calculating area for rectangle")
}

type middleCoordinates struct {
    x int
    y int
}

func (a *middleCoordinates) visitForSquare(s *square) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
    fmt.Println("Calculating middle point coordinates for square")
}

func (a *middleCoordinates) visitForCircle(c *circle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
    fmt.Println("Calculating middle point coordinates for circle")
}

func (a *middleCoordinates) visitForrectangle(t *rectangle) {
    //Calculate middle point coordinates for square. After calculating the area assign in to the x and y instance variable.
    fmt.Println("Calculating middle point coordinates for rectangle")
}

func main() {
    square := &square{side: 2}
    circle := &circle{radius: 3}
    rectangle := &rectangle{l: 2, b: 3}
    areaCalculator := &areaCalculator{}
    square.accept(areaCalculator)
    circle.accept(areaCalculator)
    rectangle.accept(areaCalculator)
    
    fmt.Println()
    middleCoordinates := &middleCoordinates{}
    square.accept(middleCoordinates)
    circle.accept(middleCoordinates)
    rectangle.accept(middleCoordinates)
}

Output:

Calculating area for square
Calculating area for circle
Calculating area for rectangle

Calculating middle point coordinates for square
Calculating middle point coordinates for circle
Calculating middle point coordinates for rectangle