Calcolat
Calcolat

Reputation: 898

How to use interface as a method parameter but still access non-interface methods?

I've spent hours trying to find a way to implement this and so far I haven't found a good solution, so I'm hoping someone could please help point me in the right direction.

I currently have a C# Winforms project that has an abstract base class with several child classes. Most of the methods are the same but each child class has a few additional methods specific to it.

I want to be able to declare a type of the class being used once and pass that type to all my other methods without having to manually check the type everywhere by going "If Class Type=A Do This Else If Class Type=B Do That, and so on".

The problem is that I need to pass the base class or interface to accomplish this. However, by doing so it means I can no longer access the properties or methods specific to the child classes and I don't know how to fix/workaround this.

Here's a simplified example of what I'm trying to do:

Interface and Class Structure

public interface IAnimal
    {
        string NameOfAnimal { get; set; }

        void Eat();
    }

public abstract class Animal : IAnimal
    {
        public abstract string NameOfAnimal { get; set; }

        public abstract void Eat();
    }

public class Bird : Animal
{
    public Bird()
    {
        NameOfAnimal = "Bob the Bird";
        NumberOfFeathers = 100;
    }

    // Interface members
    public override string NameOfAnimal { get; set; }

    public override void Eat()
    {
        System.Windows.Forms.MessageBox.Show("Eating like a Bird");
    }

    // Bird specific properties and methods
    public int NumberOfFeathers { get; protected set; }
    public string SomeOtherBirdSpecificProperty { get; protected set; }

    public void Fly()
    {
        // Fly like a bird
    }
}

public class Fish : Animal
{
    public Fish()
    {
        NameOfAnimal = "Jill the Fish";
        DoesFishHaveSharpTeeth = true;
    }

    // Interface members
    public override string NameOfAnimal { get; set; }

    public override void Eat()
    {
        System.Windows.Forms.MessageBox.Show("Eating like a Fish");
    }

    // Fish specific properties
    public bool DoesFishHaveSharpTeeth { get; protected set; }
    public string SomeOtherFishSpecificProperty { get; protected set; }
}

Main Form Code

    private void button1_Click(object sender, EventArgs e)
    {
        IAnimal myCustomAnimal = null;
        string animalTheUserSelected = "Bird";

        // Only want to manually specify this once
        switch (animalTheUserSelected)
        {
            case "Bird":
                myCustomAnimal = new Bird();
                break;

            case "Fish":
                myCustomAnimal = new Fish();
                break;

            default:
                break;
        }

        DoSomethingWithACustomAnimal(myCustomAnimal);
    }

    private void DoSomethingWithACustomAnimal(IAnimal myAnimal)
    {
        // This works fine
        MessageBox.Show(myAnimal.NameOfAnimal);
        myAnimal.Eat();

        // This doesn't work
        MessageBox.Show(myAnimal.NumberOfFeathers);
        myAnimal.Fly();
    }

I understand why I'm having that issue in the main form code... the compiler doesn't know which type of animal is being passed to it yet so it doesn't know what to show. However, I don't know what I should do to fix this.

I've tried:

So I'm just wondering how I can solve this issue?

More specifically, I'm wondering how I can re-design the above code so that I can both:

A) Explicitly declare a type of animal only once and pass that type everywhere (without having to do lots of manual checks in every method to see what type it is before doing something with it)

and also

B) Somehow still have a way to manually access the animal-specific properties like someBird.NumberOfFeathers when I need to.

Any ideas?

Upvotes: 1

Views: 914

Answers (4)

Benjamin Gale
Benjamin Gale

Reputation: 13177

One solution would be to use the visitor pattern to define the operations you want to perform on your animal instances.

First you would define a visitor interface which provides a method for each type of animal you have in your hierarchy.

public interface IAnimalVisitor
{
    void VisitBird(Bird bird);
    void VisitFish(Fish fish);
}

And then you would need to modify your animal classes and interfaces to include a method which accepts a visitor, like so:

public interface IAnimal
{
    string NameOfAnimal { get; set; }
    void Accept(IAnimalVisitor visitor);
}

Your actual animal classes now look something like this:

public class Bird : IAnimal
{
    public Bird()
    {
        NameOfAnimal = "Bob the Bird";
        NumberOfFeathers = 100;
    }

    public string NameOfAnimal { get; set; }
    public int NumberOfFeathers { get; protected set; }

    public void Accept (IAnimalVisitor visitor)
    {
        visitor.VisitBird(this);
    }
}

public class Fish : IAnimal
{
    public Fish()
    {
        NameOfAnimal = "Jill the Fish";
        DoesFishHaveSharpTeeth = true;
    }

    public string NameOfAnimal { get; set; }
    public bool DoesFishHaveSharpTeeth { get; protected set; }

    public void Accept (IAnimalVisitor visitor)
    {
        visitor.VisitFish(this);
    }
}

Now for anything you want to do with each of your animals you will need to define an implementation of the IAnimalVisitor interface. In your example you displayed message boxes that showed information about the animal so an implementation of the IAnimalVisitor interface that does that could look like this:

public class AnimalMessageBoxes : IAnimalVisitor
{
    private void VisitAnimal(IAnimal animal)
    {
        MessageBox.Show(animal.NameOfAnimal);
    }

    public void VisitBird(Bird bird)
    {
        visitAnimal(bird);
        MessageBox.Show(bird.NumberOfFeathers);
    }

    public void VisitFish(Fish fish)
    {
        visitAnimal(fish);
        MessageBox.Show(fish.DoesFishHaveSharpTeeth);
    }
}

Now you just need to pass your visitor to your animal and the correct information will be displayed. Your event handling code now looks something like this:

string animalTheUserSelected = "Bird";
IAnimal myCustomAnimal = null;

switch (animalTheUserSelected)
{
    case "Bird":
        myCustomAnimal = new Bird();
            break;

    case "Fish":
        myCustomAnimal = new Fish();
            break;

     default:
        break;
}

AnimalMessageBoxes msgBoxes = new AnimalMessageBoxes();
myCustomAnimal.Accept(msgBoxes);

If you want to do something else to your animal instances (save them to a file, generate a UI, play sounds...) you just need to define a new IAnimalVisitor implementation that provides your desired behaviour.

For the sake of balance I will say that this might not be an appropriate design as it adds some additional complexity; each 'operation' requires you to implement a visitor and the addition of another animal to your hierarchy requires you to update your visitor interface and all of it's implementations to account for the new case.

Depending on your point of view this can be either good or bad. Some consider the points above to be bad and a reason to avoid the visitor pattern and to use the other methods already suggested. Others (like me) consider the points above to be a good thing; your code will now only compile when you provide an implementation for each animal in your hierarchy and your operations are separated into small, dedicated classes.

My suggestion would be to try the SSCCE I have provided and research the visitor pattern further to decide if this solution is acceptable for your requirements.

Upvotes: 3

Jaster
Jaster

Reputation: 8581

I understand, what you are trying to achive, but your approach is just wrong.

Think it this way: does your "Main" class/form realy need to know if there is a bird or a dog? The answer is "NO". There are some common properties which you exposed via an interface (i suggest using a base class here!). Everything else is specific to the given animal. The easiest approach is extending your interface with a DoAnimalSpecificStuff() method - which would perform the specific opperations.

When it comes to presentation, you should take a look at the MVP and MVVM Patterns.

P.S. use a Factory Pattern for animal creation!

Upvotes: 2

Matt Burland
Matt Burland

Reputation: 45135

It really depends on exactly what you are trying to do and what you are hoping to achieve. All the things you've tried so far are possible approaches, but without more context it's hard to give a general answer. It depends on what the right level of abstraction is for what you are trying to do.

One thing to remember is that you can have a class implement as many interfaces as you need. So you could do something like:

public interface IAnimal
{
    string NameOfAnimal { get; set; }
    void Eat();
}

public interface IFly
{
    void Fly();
}

public interface IHaveFeathers
{
    int NumberOfFeathers { get; set; }
}

Then you Bird class can be:

public Bird : Animal, IFly, IHaveFeathers
{
    // implementation
}

And now in a method you can do something like:

private void DoSomethingWithACustomAnimal(IAnimal myAnimal)
{
    // This works fine
    MessageBox.Show(myAnimal.NameOfAnimal);
    myAnimal.Eat();

    var feathered = myAnimal as IHaveFeathers;
    if (feathered != null)
    {
        MessageBox.Show(feathered.NumberOfFeathers);
    }

    var flier = myAnimal as IFly;
    if (flier != null)
    {
        flier.Fly();
    }
}

The other thing to think about is how to abstract what you need to a higher level. So you need to Fly, but why? What happens with the Animal that can't fly? By Fly are you really just trying to Move? Then perhaps you could do:

public interface IAnimal
{
    string NameOfAnimal { get; set; }
    void Eat();
    void Move();
}

And in your Bird you can do this:

public Bird : Animal, IFly, IHaveFeathers
{
    public override void Move()
    {
        Fly();
    }

    public void Fly()
    {
        // your flying implementation
    }
    // rest of the implementation...
}

Upvotes: 4

CriketerOnSO
CriketerOnSO

Reputation: 2620

You have to explicitly cast the IAnimal object to specific type, if you want to access specific properties/methods.

You can use as operator and then check if the cast was successful like:

Bird b = myAnimal as Bird;
if(b != null)
{
     MessageBox.Show(b.NumberOfFeathers);
}

Upvotes: 1

Related Questions