DEV Community

Discussion on: How to avoid the Visitor pattern in C#

Collapse
 
wiltonhotz profile image
WiltonHotz

Hi, I got this account just to be able to comment on this.
I really liked your series!

I know I'm a few months late, but I'm really enticed by the prospect of getting around the visitor pattern. I can see that this use case is very specific, so I was wondering if by any chance you have more examples using this approach? I'm trying to transpose your code to scenarios I can better understand but I'm having a hard time escaping the frame.

What I'm especially interested in is object modeling. The classic example is the C# sum type, which I wish was as easy as an F# discriminated union, but if course I end up using some sort of visitor pattern, either by interface or by inheritance.

So.. my question to you (which might seem stupid if I have gathered nothing from this last part of the series):
Very simply, I want to model a Fruit. It's either a banana, an orange or an apple.
Fruit { Banana | Orange | Apple }

I don't want to use the visitor pattern, I want to do what your title suggests. How would I go about doing that using your approach? (While being able to solve the expression problem like you did, if possible)

Best regards, and thanks again for the series!

Collapse
 
shimmer profile image
Brian Berns • Edited

Hi. I'm glad you like the series. The expression type discussed in my article is a sum type: Expr { Literal of int | Add of (Expr * Expr) }. You can see this in the F# implementation in my repository. So you can implement a fruit algebra the same way, like this:

using System;

namespace Fruit
{
    /// <summary>
    /// This represents the sum type. We can add new fruits later by
    /// extending the interface.
    /// </summary>
    interface IFruitAlgebra<T>
    {
        T Banana();
        T Orange();
        T Apple();
    }

    /// <summary>
    /// This represents the functions we can perform with the sum type.
    /// We can add new behavior later by creating other interfaces.
    /// </summary>
    interface IFruitBehavior
    {
        string Slogan();
        bool IsSlippery();
    }

    class FruitBehavior : IFruitBehavior
    {
        public FruitBehavior(Func<string> slogan, Func<bool> isSlippery)
        {
            _slogan = slogan;
            _isSlippery = isSlippery;
        }
        Func<string> _slogan;
        Func<bool> _isSlippery;

        public string Slogan()
            => _slogan();

        public bool IsSlippery()
            => _isSlippery();
    }

    class FruitAlgebra : IFruitAlgebra<IFruitBehavior>
    {
        public IFruitBehavior Banana()
            => new FruitBehavior(
                    () => "I like bananas!",
                    () => true);
        public IFruitBehavior Orange()
            => new FruitBehavior(
                    () => "Oranges are best!",
                    () => false);
        public IFruitBehavior Apple()
            => new FruitBehavior(
                    () => "Eat an apple!",
                    () => false);
    }

    static class FruitTest
    {
        public static T CreateTestFruit<T>(IFruitAlgebra<T> factory)
            => factory.Banana();

        public static void RunTest()
        {
            Console.WriteLine("Fruit test:");

            var fruit = CreateTestFruit(new FruitAlgebra());
            Console.WriteLine($"   Slogan: {fruit.Slogan()}");
            Console.WriteLine($"   Is slippery: {fruit.IsSlippery()}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
wiltonhotz profile image
WiltonHotz • Edited

Hi! So grateful that you replied!
For some reason I'm still having a hard time extending the types of the example you provided, although I really like the example. Very clear and instructive.

I did something similar myself, with the goal of being able to extend types selectively, and I would love to hear your comments on the approach (heavily using the code you originally provided)

Especially if you have any thoughts on the downside of using this approach :)

What I think I've done is extensible types, extensible functionality, and it's all up to the caller to decide what to call. As well as there's only one generic method to get whatever we're interested in, from the objects that can provide it.

using System;
using System.Collections.Generic;
using System.Linq;

namespace SumWithoutVisitor3
{
    class Program
    {
        static void Main(string[] args)
        {
            var rectangle = new Rectangle(10, 20);
            var triangle = new Triangle(10, 20, "A mighty fine triangle!");
            var circle = new Circle(10);

            var areas = GetAnything<IShape, double, ICanDoSomeShapeCalculations<IShapeCalculationContract>>
                ((ICanDoSomeShapeCalculations<IShapeCalculationContract> x) => x.GetArea(), rectangle, triangle, circle);

            var circumferences = GetAnything<IShape, double, ICanDoSomeMoreShapeCalculations<IShapeCalculationContract>>
                ((ICanDoSomeMoreShapeCalculations<IShapeCalculationContract> x) => x.GetCircumference(), rectangle, triangle, circle);

            var descriptions = GetAnything<IShape, string, ICanDoSomeShapePrinting<IShapePrintingContract>>
                ((ICanDoSomeShapePrinting<IShapePrintingContract> x) => x.GetDescription(), rectangle, triangle, circle);

            Console.WriteLine("Areas");
            foreach (var a in areas)
            {
                Console.WriteLine(a.Item1 + " : " + a.Item2);
            }

            Console.WriteLine("Circumferences");
            foreach (var c in circumferences)
            {
                Console.WriteLine(c.Item1 + " : " + c.Item2);
            }

            Console.WriteLine("Descriptions");
            foreach (var d in descriptions)
            {
                Console.WriteLine(d.Item1 + " : " + d.Item2);
            }
        }
        public static IEnumerable<(string, R)> GetAnything<T,R,U>(Func<U, R> map, params T[] items)
        {
            var anything = items.OfType<U>().Select(x => (x.GetType().ToString(), map(x)));
            return anything;
        }
    }
    interface IShape
    {

    }

    interface IShapeCalculationContract
    {
        double Calculate(Func<double> calculate);
    }
    // add new functionality
    interface IShapePrintingContract
    {
        string Print(Func<string> print);
    }
    class Shape : IShapeCalculationContract, IShapePrintingContract
    {
        public double Calculate(Func<double> calculate) => calculate();
        public string Print(Func<string> print) => print();
    }
    interface ICanDoSomeShapeCalculations<T> : IShape
    {
        double GetArea();
    }
    interface ICanDoSomeShapePrinting<T> : IShape
    {
        string GetDescription();
    }
    class Rectangle : ICanDoSomeShapeCalculations<IShapeCalculationContract>
    {
        public double Width { get; }
        public double Height { get; }
        public Rectangle(double width, double height)
        {
            Width = width;
            Height = height;
        }
        public double GetArea() => new Shape().Calculate(() => Width * Height);
    }
    class Triangle : ICanDoSomeShapeCalculations<IShapeCalculationContract>, ICanDoSomeShapePrinting<IShapePrintingContract>
    {
        public string Description { get; }
        public double Width { get; }
        public double Height { get; }
        public Triangle(double width, double height, string description)
        {
            Description = description;
            Width = width;
            Height = height;
        }

        public double GetArea() => new Shape().Calculate(() => (Width * Height) / 2);
        public string GetDescription() => new Shape().Print(() => Description);
    }
    // add new method 
    interface ICanDoSomeMoreShapeCalculations<T> : ICanDoSomeShapeCalculations<T>
    {
        double GetCircumference();
    }
    // add new type
    class Circle : ICanDoSomeMoreShapeCalculations<IShapeCalculationContract>
    {
        public double Radius { get; }
        public double PI { get; }
        public Circle(double a)
        {
            Radius = a;
            PI = Math.PI;
        }

        public double GetArea() => new Shape().Calculate(() => Radius * Radius * PI);
        public double GetCircumference() => new Shape().Calculate(() => 2 * Radius * PI);
    }
}

Enter fullscreen mode Exit fullscreen mode

Output:
Areas
SumWithoutVisitor3.Rectangle : 200
SumWithoutVisitor3.Triangle : 100
SumWithoutVisitor3.Circle : 314,1592653589793
Circumferences
SumWithoutVisitor3.Circle : 62,83185307179586
Descriptions
SumWithoutVisitor3.Triangle : A mighty fine triangle!