简体   繁体   中英

Delegate Method or Abstract Class

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.

As a rule of thumb: less code == more maintainable.

In the specific case you have, you are also having a decoupled design - the logic of how to SayHi is separate from the class that says it, giving you the option to compose the behavior. Low coupling is also a hallmark of code that is generally to maintain.

My preference would be with the second design.

The first design is more standard, and the logic is consistent (means that any other class using the LoudFoo (or QuietFoo ) will have same result everywhere. However, it is reuseable, but only in its inherited path. Means that child class from LoudFoo (say DerivedLoudFoo cannot use SayHi logic defined in QuietFoo ).

That may sound simple, but can be troublesome later on. You can read my answer at here for real-life case.

The second is more extendable, but the drawbacks are it can have different behavior. Don't use this for core business process (such as insert/update/delete), as it will hard to debug or modify. However, this is best to use in Framework level for some methods such as OnAfterInsert , OnAfterSubmit .

Assuming this is more than a totally made-up example and could actually be translated into real code (which I have a hard time figuring out what it could be), I find option 2 terrible.

  • You can pretty much assign anything to SayHi , including a lambda that is not related to a Bar in any way, which doesn't seem to be your original intent.

  • You're basically trying to stick a nicely crafted functional peg into a good old object-oriented hole. Using a lambda you separated data ( Bar ) from behavior that operates on it, which is valid functional practice, but then by making Foo.SayHi a property, you're back to an OO style trying to encapsulate the two of them back into the same class. Seems a bit contrived.

Design 2 bothers me because it feels redundant when I have to have Foo actually SayHi().

If Foo class is redefined to

class Foo
    public property Bar as string
    public property SayHi as func(of string)
end class

Then closure can be used to create and invoke SayHi function without passing Foo as a function parameter:

dim Bar = "Felix"

dim Felix as new Foo with {
    .Bar = Bar,
    .SayHi = function() Bar.toLower
}

dim FelixSays = Felix.SayHi()

I'm leaning towards Design 1 because it keeps the behavior implementation black-boxed inside the class.

Design 2 is always ready to black-box behavior implementation, for example inside a factory method:

function CreateQuietFoo(Bar as string) as Foo
    return new Foo with {
        .Bar = Bar,
        .SayHi = function() Bar.toLower
    }
end function

dim Felix = CreateQuietFoo("Felix")
dim Oscar = CreateQuietFoo("Oscar")

This way the caller doesn't have to provide SayHi method to create a quiet Foo instance, he simply uses CreateQuietFoo factory method.

My question is whether it is better to use Design 1 or Design 2-- or perhaps neither of them.

Use Design 2 if you prefer Composition over Inheritance . It makes code more flexible.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM