Reputation: 1456
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 i.e 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);
}
}
Upvotes: 4
Views: 1396
Reputation: 70721
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
.
Upvotes: 1
Reputation: 10927
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:
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:
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:
At the bottom line:
Visitor implements the following design principals:
Benefits of Visitor implementations are:
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("");
}
Upvotes: 2