Here is my program:
class Program
{
//DESIGN 1
abstract class AFoo
{
public string Bar { get; set; }
public abstract string SayHi();
}
class LoudFoo : AFoo
{
public override string SayHi()
{
return this.Bar.ToUpper();
}
}
class QuietFoo : AFoo
{
public override string SayHi() { return this.Bar.ToLower(); }
}
//DESIGN 2
class Foo{
public string Bar { get; set; }
public Func<Foo, string> SayHi { get; set; }
}
static void Main(string[] args)
{
//USING DESIGN 1
var quietFoo2 = new QuietFoo{ Bar = "Mariane"};
var loudFoo2 = new LoudFoo{ Bar = "Ginger"};
Console.WriteLine(quietFoo2.SayHi());
Console.WriteLine(loudFoo2.SayHi());
//USING DESIGN 2
var quietFoo = new Foo
{
Bar = "Felix",
SayHi = (f) => { return f.Bar.ToLower(); }
};
var loudFoo = new Foo
{
Bar = "Oscar",
SayHi = (f) => { return f.Bar.ToUpper(); }
};
Console.WriteLine(quietFoo.SayHi(quietFoo));
Console.WriteLine(loudFoo.SayHi(loudFoo));
}
}
I can accomplish the "same thing"-- actually not exactly the same thing, but similar things going two different routes.
Design 1) I can create an abstract class which forces the implementor of that class how to SayHi()
--or--
Design 2) I could create a class defines a SayHi property which is a function. (I'm calling it a delegate-- but I'm not sure that's the correct term for it here)
Design 1 bothers me because it could lead to a profliferation of classes
yet....
Design 2 bothers me because it feels redundant when I have to have Foo actually SayHi().
felix.SayHi(felix)
My question is whether it is better to use Design 1 or Design 2-- or perhaps neither of them. When I say better I am saying which is more practical in terms of being able to maintain my program. I ran into this when I created different classes which are going to be used to download files from different cloud API's (Google Drive, Box.com, DropBox)-- at first I created separate classes, but then I went the other route.
When it comes to these types of design choices, I find it helps to think about the objects in terms of the problem domain you're trying to model. You've shown LoudFoo and QuietFoo as differing in a single behavior, but this is a deliberately simplified example. In a real system, you may have compelling reasons to consider two objects as being conceptually distinct.
In the former version, SayHi is an instrinsic part of the class behavior, which is appropriate if the nature of that behavior interacts with its internal state in some way. Perhaps the implementation of SayHi depends on properties of the object that are specific to that derived class type.
In the latter version, SayingHi is a more like a tool that can be handed out to various instances. This is appropriate when there are no other reasons to distinguish between different types of Foo instances.
Stream is a good example of the former pattern, where the various methods it provides are intrinsic to the nature of the streaming operation. The various derived classes will make use of different states to implement their methods.
Comparer is a good example of the latter pattern, where lots of different object types want to operate using a notion of comparison. The classes that use this functionality don't need to have anything else in common other than wanting to consume this particular type of behavior.
Regarding your concrete application that prompted this question, what about the multi-class approach felt awkward? If there was redundancy creeping in, it likely indicates that the responsibilities could be factored in a different way that better modeled the problem. It's hard to say more without knowing additional details about the specific problem, but likely a good approach would be a combination of the two you proposed, with some single class responsible for the sequencing of the operation and a separate heirarchy (or set of interface implementations) implementing operations specific to each service. Essentially the interface (or base class) groups all of the various delegates you would pass in separately. This is akin to how a StreamReader takes a Stream and augments it with additional behaviors that operate on the Stream.
In Design 1 your behavior is implemented inside the class, but in Design 2 you're asking your caller to define the behavior.
I'm leaning towards Design 1 because it keeps the behavior implementation black-boxed inside the class. Design 2 could have your implementation changed whenever somebody instantiates a new object. I also don't like how the implementation is the responsibility of the caller.
If how you implement SayHi
changes you only have one place to change it in Design 1, but you could potentially have several places all over your code to change it if you used Design 2.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With