简体   繁体   中英

What is the benefit of Visitor pattern in an API?

I am trying to understand the benefit of having the visitor pattern within an API. The below example is one I saw and I wanted an example as to why the pattern in beneficial ie benefits. What would be the alternative implementation that would be negative and why compared to this. What benefit can be gained from the below implementation. In this api it contacts multiple universities to get the courses that they offer. Each get course service then has a defined number of responses using the Visitor Pattern:

Controller

[HttpGet]
public async Task<IActionResult> Get()
{
    // CourseService already retrieved for a given uni 
    var result = await courseService.GetCourses(userSession);
    return result.Accept(new CourseVisitor());
}

Service - Each Uni has there own GetCourses Service but they all have set responses due to the Visitor pattern

public async Task<CoursesResult> GetCourses(UserSession userSession) {

// Depending on response from a given uni a set number of responses can be returned across ass uni services e.g
return new CoursesResult.BadRequest(); **or**
return new CoursesResult.Success(); etc
}

Element Abstract / Concrete Element

  public abstract class GetCourses
    {
        public abstract T Accept<T>(ICourseVisitor<T> visitor);

        public class Successful : CoursesResult
        {
            public CourseList Response { get; }

            public Successful(CourseList response)
            {
                Response = response;
            }

            public override T Accept<T>(ICourseVisitor<T> visitor)
            {
                return visitor.Visit(this);
            }
        }
   // Other responses then defined e.g Bad Request

IVisitor

    public interface ICourseVisitor<out T>
{
    T Visit(GetCoursesResult.Successful result);
    T Visit(GetCoursesResult.BadRequest result);

Visitor

    internal class CourseVisitor : ICourseVisitor<IActionResult>
{
    public IActionResult Visit(GetCourses.Successful result)
    {
        return new OkObjectResult(result.Response);
    }

UPDATED QUERY Additionally I'm trying to understand why the service couldn't return something like this:

//Service returns this: return new Successful(listResponse)

 public interface ICoursesResult
    {
      IActionResult Accept();
    }

 public class Successful : ICoursesResult
    {
        public CourseList Response { get; }

        public Successful(CourseList response)
        {
            Response = response;
        }

        public IActionResult Accept()
        {
            return OkObjectResult(Response);
        }
    }

There is an extensive research regarding this in code project - Visitor Pattern ReExplained .

I will provide the headline.

Visitor pattern is here to solve to things, by presenting two aspects:

  • There is an iterating mechanism, which knows how to iterate on Object hirerachy.it doesn't know anything about the behavior of objects in the hierarchy.
  • The new behaviors which needs to be implemented knows nothing about the iteration mechanism, they don't know how to iterate the object hierarchy.

Now Both of this aspects, are independent of each other, and they shouldn't be messed with each other. So, this is all about OOPs principal known as Single Responsibility principal , taking you all back to SOLID architecture .


The key players in this features are:

  • Visitor - An Interface which defines Visit operation. This is the core of visitor pattern. It defines a Visit operation for each type of Concreate Element in the object structure.
  • ConcreateVisitor - Implements the operations defined in the Visitor interface.
  • ElementBase : It is an Abstract/Interface which defines Accept operation that takes the visitor as an argument.
  • ConcreateElement - These types implements Accept method of Element interface.
  • Object Structure - It holds all the element of the data structure as a collection, list or something which can be enumerated and used by the visitors. It provide the interface to all visitor to visit its element. These element include the method called "Accept”. The collection is then enumerated

Now, the aim of all this pattern, the key, is to create data model with limited functionality and the set of visitors with specific functionality that will operate upon the data. The pattern allows the each element of the data structure to be visited by the visitor passing the object as an argument to the visitor methods.


The Benefits after all-

The Key of separating the algorithm from its data model is the ability to add new behaviors easily. The classes of data model are created with the common method called Visit which can accept visitor object at runtime. Then different visitor object can be crated and passed it to this method, then this method had a callback to the visitor method passing itself to it as a parameter.

Another things worth mentioning is :

Adding a new type to the object hierarchy requires changes to all visitors, and this should be seen as an advantage as it definitely forces us to add the new type to all places where you kept some type-specific code. Basically it don't just let you forget that.

The visitor pattern is only useful:

  • If the interface you want implemented is rather static and doesn't change a lot.
  • If all types are known in advance, ie at design time all objects must be known.

At the bottom line:

Visitor implements the following design principals :

  • Separation of Concern - Visitor pattern promotes this principle, multiple aspects/concerns are separated to multiple other classes, as it encourages cleaner code and code re-usability, and also code is more testable.
  • Single Responsibility Principle - Visitor pattern also enforce this principle. An object should have almost one responsibility. Unrelated behaviors must be separated from it to another class.
  • Open Close Principle - Visitor pattern also follows this principal, as if, we want to extend an object behavior, the original source code is not altered. The Visitor pattern provide us the mechanism to separate it to another class and apply those operations for an object at run time.

Benefits of Visitor implementations are :

  • Separate the behavior of data structure from them. Separate visitors object are created for to implement such behavior.
  • It solve Double Dispatch problem which are rarely faced but has important impact.

You can dive deeper into the article to understand the entire meaning of this but if I am to present an example:

First of all we will define the interface which we call IVisitable. It will define a single Accept method which will accept an argument of IVisitor. This interface will serve as the base for all types in the product list. All types like Book, Car and Wine (in our example) will implement this type.

/// <summary>
/// Define Visitable Interface.This is to enforce Visit method for all items in product.
/// </summary>
internal interface IVisitable
{
    void Accept(IVisitor visit);
}   

Then we will implement it:

  #region "Structure Implementations"


    /// <summary>
    /// Define base class for all items in products to share some common state or behaviors.
    /// Thic class implement IVisitable,so it allows products to be Visitable.
    /// </summary>
    internal abstract class Product : IVisitable
    {
        public int Price { get; set; }
        public abstract void Accept(IVisitor visit);
    }

    /// <summary>
    /// Define Book Class which is of Product type.
    /// </summary>
    internal class Book : Product
    {
        // Book specific data

        public Book(int price)
        {
            this.Price = price;
        }
        public override void Accept(IVisitor visitor)
        {
            visitor.Visit(this);
        }
    }

    /// <summary>
    /// Define Car Class which is of Product type.
    /// </summary>
    internal class Car : Product
    {
        // Car specific data

        public Car(int price)
        {
            this.Price = price;
        }

        public override void Accept(IVisitor visitor)
        {
            visitor.Visit(this);
        }
    }

    /// <summary>
    /// Define Wine Class which is of Product type.
    /// </summary>
    internal class Wine : Product
    {
        // Wine specific data
        public Wine(int price)
        {
            this.Price = price;
        }

        public override void Accept(IVisitor visitor)
        {
            visitor.Visit(this);
        }
    }

    #endregion "Structure Implementations"  

Create a visitor interface and implement it:

/// <summary>
/// Define basic Visitor Interface.
/// </summary>
internal interface IVisitor
{
    void Visit(Book book);
    void Visit(Car car);
    void Visit(Wine wine);
}

#region "Visitor Implementation"


/// <summary>
/// Define Visitor of Basic Tax Calculator.
/// </summary>
internal class BasicPriceVisitor : IVisitor
{
    public int taxToPay { get; private set; }
    public int totalPrice { get; private set; }

    public void Visit(Book book)
    {
        var calculatedTax = (book.Price * 10) / 100;
        totalPrice += book.Price + calculatedTax;
        taxToPay += calculatedTax;
    }

    public void Visit(Car car)
    {
        var calculatedTax = (car.Price * 30) / 100;
        totalPrice += car.Price + calculatedTax;
        taxToPay += calculatedTax;
    }

    public void Visit(Wine wine)
    {
        var calculatedTax = (wine.Price * 32) / 100;
        totalPrice += wine.Price + calculatedTax;
        taxToPay += calculatedTax;
    }
}


#endregion "Visitor Implementation"

Execution:

 static void Main(string[] args)
    {
        Program.ShowHeader("Visitor Pattern");

        List<Product> products = new List<Product>
        { 
            new Book(200),new Book(205),new Book(303),new Wine(706)
        };

        ShowTitle("Basic Price calculation");
        BasicPriceVisitor pricevisitor = new BasicPriceVisitor();
        products.ForEach(x =>
        {
            x.Accept(pricevisitor);
        });

        Console.WriteLine("");
    }     

The visitor pattern is normally used when you have a polymorphic type and you want to perform an externally defined operation based on the specific subtype of the object. In your example, CoursesResult is a polymorphic type and a visitor lets you convert a Successful response into an OkObjectResult without directly coupling these two types.

Your alternative approach where CoursesResult directly returns an IActionResult is traditional polymorphism, which is simpler but couples domain logic to the MVC layer.

Now, I don't know what your full set of responses looks like, but if you only have one successful response and the rest are errors, then the simplest approach here is to directly return the successful response and throw exceptions for the other cases:

public async Task<CourseList> GetCourses(UserSession userSession) {
  return courseList; /* or */ throw new BadRequestException();
}

Then your controller can simply catch exceptions and convert them to the appropriate IActionResult .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM