3

I have a superClass called Block and another 3 subclasses. the class I want to implement contains 3 overloaded functions each one takes an object of one of the subclasses as a parameter. When I use one of these function, I only have a Block object (An object from the superClass). My question is what is the cleanest way to choose which function to call.

What I did until now is if conditions on the object type then casting it. but it seems unclean.

Those are the overloaded functions.

        public void WriteBlock(TableBlock block) { }
        public void WriteBlock(TextBlock block) { }
        public void WriteBlock(ListBlock block) { }

And This is The function I want to implement.

        public void WriteBlocks(List<Block> blocks)
        {
            BlockWriter w = new BlockWriter();

            foreach (var block in blocks)
            {
                w.WriteBlock(block);
            }
        }

Note that I have no access on the Blocks classes.

A.khaled
  • 505
  • 1
  • 5
  • 13

2 Answers2

2

No. Given this:

public void WriteBlocks(List<Block> blocks)

the only thing the compiler knows about each item in the list is that it is a Block. That's all it should know. That's what makes polymorphism possible. There can be any number of classes that inherit from Block, but within this context those distinctions don't matter.

But if all the compiler knows is that each item is a Block, it can't know whether any individual item might be a TableBlock, TextBlock, or some other inherited type. If, at compile time, it doesn't know what the runtime type will be, it can't know whether there even is an overload for that specific type.

Suppose what you're trying to do could compile, because you have an overload for every type that inherited from Block. What would or should happen if you added a new type - class PurpleBlock : Block - and there was no overload for it? Should this no longer compile just because you added a new type?

If the method that calls WriteBlocks knows what sort of Block is in the list, then it can supply that information:

public void WriteBlocks<TBlock>(List<TBlock> blocks) where TBlock : Block

Now you can call WriteBlock<TextBlock>(listOfTextBlocks) and the compiler will know that each item in the list is a TextBlock, not just a Block.

It follows, then, that BlockWriter would have to be generic also so that you could have different implementations for different types of Block. It might make more sense to inject it. Either way, you're likely to perceive that you've "moved" the problem. If the class that calls WriteBlocks "knows" the type of the Block, then it might make more sense for that method to determine the type of BlockWriter to use.


As mentioned in your comment, the list might include different types of Block, not just one. That requires either a method or a class that returns a specific BlockWriter depending on the type of Block. That means runtime type-checking, which isn't ideal, but it's not too bad if you keep it in one place.

Here's a simple example:

public class BlockWriterFactory
{
    public BlockWriter GetBlockWriter(Block block)
    {
        if (block is TextBlock)
            return new TextBlockWriter();

        if (block is TableBlock)
            return new TableBlockWriter();

        if (block is ListBlock)
            return new ListBlockWriter();

        // this could be a "null" class or some fallback
        // default implementation. You could also choose to
        // throw an exception.
        return new NullBlockWriter();
    }
}

(A NullBlockWriter would just be a class that does nothing when you call its Write method.)

This sort of type-checking isn't ideal, but at least this keeps it isolated into one class. Now you can create (or inject) an instance of the factory, and call GetBlockWriter, and the rest of your code in that method still wouldn't "know" anything about the different types of Block or BlockWriter.

BlockWriter w = new BlockWriter();

would become

BlockWriter w = blockWriterFactory.GetBlockWriter(block);

...and then the rest would still be the same.

That's the simplest possible factory example. There are other approaches to creating such a factory. You could store all of your implementations in a Dictionary<Type, BlockWriter> and attempt to retrieve an instance using block.GetType().

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
  • Thanks for your answer. But actually The list contains different Types of blocks so I guess it is impossible without editing the Block class. – A.khaled Jul 10 '19 at 19:04
  • In that case you could have a method or class that returns a `BlockWriter` for a given type of block - for example, `GetBlockWriter(Block block)`. That method would contain unfortunate type checking, and you'd probably want to return a default or `NullBlockWriter` if the type of `Block` is unsupported. But at least then the rest of your code would be polymorphic. – Scott Hannen Jul 10 '19 at 19:18
  • 1
    Did you mean to use the same block writer in each of your `if` statements? (i. e. all `TextBlock`)? – Chris Dunaway Jul 10 '19 at 19:47
2

Yes, it is possible using the dynamic type which allows for this.

If you use:

        foreach (var block in blocks)
        {
            w.WriteBlock(block as dynamic);
        }

It should call the intended WriteBlock overload.

This is described in greater length in another question: https://stackoverflow.com/a/40618674/3195477

And also here: method overloading and dynamic keyword in C#.


Caveats:

I am not sure if there is any runtime penalty associated with this type of dynamic "cast".

Also whenever I see this pattern it makes me wonder if the class hierarchy could be improved. i.e., should whatever WriteBlock will do actually be moved inside the Block classes? That might be "more polymorphic". Also using dynamic could be a somewhat fragile approach, as you can add new Block derived types and forget to an an overloaded WriteBlock for them, which may cause an error. (This is more evidence that some of WriteBlock should be incorporated into the Block classes themselves).

For instance, add a virtual PrepareForWriting() to the base Block class, which returns a BlockWritable. Then you only need one WriteBlock(BlockWritable data) to do the writing work. BlockWritable could be a string, Json, XML, etc. This assumes you are able to modify the Block classes (which it seems you cannot).

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
  • Thanks. It Actually Worked !! – A.khaled Jul 10 '19 at 19:34
  • 1
    Generally speaking I wouldn't use `dynamic` unless you're backed into a corner and have no other choice. It's often a sign that we're working against, not with, the compiler. It's also a bit similar to "giving up" and declaring things as `object`. Under 99% of normal circumstances when we're dealing with known types we shouldn't use `dynamic`. – Scott Hannen Jul 10 '19 at 19:43
  • @ScottHannen I agree, hence my comments about refactoring the class structure. In this case the OP mentioned that some classes are outside their control so perhaps it is a valid case for it. – StayOnTarget Jul 10 '19 at 20:24