I have classes B
and C
, inheriting from class SuperA
. If I have a list of SuperA
containing various implementations of SuperA
, how can I call method taking B
and C
argument according to the actual implementation of each element in the list, without having to test the type of each element (I would prefer to avoid if(item is B)
stuff for open/closed principle reasons).
public class Test
{
public void TestMethod()
{
var list = new List<SuperA> {new B(), new C()};
var factory = new OutputFactory();
foreach (SuperA item in list)
{
DoSomething(factory.GenerateOutput(item)); // doesn't compile as there is no GenerateOutput(SuperA foo) signature in OutputFactory.
}
}
private static void DoSomething(OutputB b)
{
Console.WriteLine(b.ToString());
}
private static void DoSomething(OutputC c)
{
Console.WriteLine(c.ToString());
}
public class SuperA
{
}
public class B : SuperA
{
}
public class C : SuperA
{
}
public class OutputB
{
public override string ToString()
{
return "B";
}
}
public class OutputC
{
public override string ToString()
{
return "C";
}
}
public class OutputFactory
{
public OutputB GenerateOutput(B foo)
{
return new OutputB();
}
public OutputC GenerateOutput(C foo)
{
return new OutputC();
}
}
}
In the above code, I wish to print :
B
C
EDIT :
A working solution I found could be changing the item type to dynamic
foreach (dynamic item in list)
{
DoSomething(factory.GenerateOutput(item));
}
I'm open to any better idea however. As pointed out in answer, the risk of runtime error after an evolution is great.
The compiler complains about your code because, as you pointed out, threre is no GenerateOutput(SuperA)
in OutputFactory
class and method call resolution happens at compile type, not at runtime, and therefore is based on the type of the reference (item
is a reference with type SuperA
) and not on the type of the runtime instance.
You can try with different approaches:
SuperA
class hierarchy, adding an abstract method or property to SuperA
and implementing it differently in SuperA
's subclassesclass SuperA {
public abstract string Content { get; }
}
class B : SuperA {
public string Content => "B";
}
class C : SuperA {
public string Content => "C";
}
class Test {
public void TestMethod() {
// ...
foreach (SuperA item in list) {
Console.WriteLine(item.Content);
}
}
Very simple but it does not work very well when SuperA
,B, and
Cclasses are out of your control or when the different desired behaviours you should provide for
Aand
Bclasses does not belong to
Band
C` classes.
TestMethod
as follows:public void TestMethod() {
var list = new List<SuperA> {new B(), new C()};
var compositeHandler = new CompositeHandler(new Handler[] {
new BHandler(),
new CHandler()
});
foreach (SuperA item in list) {
compositeHandler.Handle(item);
}
}
So you need define a Handler
interface and its implementations like follows:
interface Handler {
bool CanHandle(SuperA item);
void Handle(SuperA item);
}
class BHandler : Handler {
bool CanHandle(SuperA item) => item is B;
void Handle(SuperA item) {
var b = (B)item; // cast here is safe due to previous check in `CanHandle()`
DoSomethingUsingB(b);
}
}
class CHandler : Handler {
bool CanHandle(SuperA item) => item is C;
void Handle(SuperA item) {
var c = (C)item; // cast here is safe due to previous check in `CanHandle()`
DoSomethingUsingC(c);
}
}
class CompositeHandler {
private readonly IEnumerable<handler> handlers;
public CompositeHandler(IEnumerable<handler> handlers) {
this.handlers = handlers;
}
public void Handle(SuperA item) {
handlers.FirstOrDefault(h => h.CanHandle(item))?.Handle(item);
}
}
This approach uses type checks (item is B
), but hides them behind an interface (specifically, each implementation of the interface should provide a type check in order to select instances it can handle): if you need to add a third D extends SuperA
subclass of your hierarchy root class you need only add a third DHandler : Handler
implementation of Handler
interface, without modify neither already provided implementations nor CompositeHelper
class; the only change you should apply to existing code is the registration of the new handler
's implementation in the list you provide to CompositeHelper
's constructor, but this can easily be moved to you IoC container
configuration or to an external configuration file.
I like this approach because it makes possibile to turn a type check based algorithm into a polymorphical one.
I wrote about this topic in a recent post on my technical blog: https://javapeanuts.blogspot.com/2018/10/set-of-responsibility.html.
dynamic
based approach, as suggested in another responseI hope this can help you!