Search code examples
c#oopinheritance

simulate method call mechanism of inheritance


I have a class hierarchy like this:

public class Base { }
public class Child1 : Base { }
public class Child2 : Base { }

I want to generate hashes for those classes. If I implemented these methods inside the classes, I could easily use the inheritance mechanism to get the correct method calls:

public class Base { public virtual string GetHash() { ... } }
public class Child1 : Base { public override string GetHash() { ... } }
public class Child2 : Base { public override string GetHash() { ... } }

For a given Base instance foo I could simply call foo.GetHash() and it would always be the correct method, because the inheritance mechanism redirects to the specific GetHash method for the specific instance foo (if foo is Child1 then it uses Child1.GetHash, if it is Base, it uses Base.GetHash etc.).

However, for reasons not relevant here (has to do with seperation of concerns) I cannot implement the hashing methods inside the class hierarchy. Instead I have a Hasher class for that job.

Here is a naive way of implementing it with a conditional chain:

public class Hasher
{
    public string GetHash(Base data)
    {
        if (data is Child1 c1)
            return GetHashChild1(c1);
        else if (data is Child2 c2)
            return GetHashChild2(c2);
        else
            return GetHashBase(data);
    }

    private string GetHashChild1(Child1 c) { ... }
    private string GetHashChild2(Child2 c) { ... }
    private string GetHashBase(Base b) { ... }
}

Is there a way to implement it differently so that

  • in case I add new Child classes to the hierarchy, I only need to implement their corresponding hashing methods (and not update any if-else-if/switch statements or fill dictionaries etc.)?
  • in other words: the redirection to the correct method happens automatically?

Maybe generic methods (GetHash<T>(T ...)) can help somehow?


Solution

  • I would propose the visitor pattern:

    public class Base
    {
        public virtual T Accept<T>(IVisitor<T> visitor) => visitor.Visit(this);
    }
    public class Child1 : Base
    {
        public override T Accept<T>(IVisitor<T> visitor) => visitor.Visit(this);
    }
    public class Child2 : Base
    {
        public override T Accept<T>(IVisitor<T> visitor) => visitor.Visit(this);
    }
    
    public interface IVisitor<out T>
    {
        T Visit(Base child1);
        T Visit(Child1 child1);
        T Visit(Child2 child1);
    }
    
    public class GetHashVisitor : IVisitor<string>
    {
        public string Visit(Base child1) => "...";
        public string Visit(Child1 child1) => "...";
        public string Visit(Child2 child1) => "...";
    }
    

    This achieves the goal of separating the Hashing from the actual object hierarchy.

    If you make base.Accept abstract and remove the IVisitor.Visit(Base) method, the compiler will enforce creation of a new method in all your visitors whenever a new child class is added. So while this is not simpler than a switch or a list of ifs, it can be safer and more flexible.