12

Below, I define an IInstantNotification Interface. TextNotification Class and EmailNotification Class inherit from this interface.

public interface IInstantNotification<T> {
        List<string> Addresses { get; set; }
        string NotificationContent { get; set; }
        T NotificationArguments { get; set; }
        bool SendNotification();
}

public class TextNotification : IInstantNotification<TextArguments>
{
        public List<string> Addresses { get; set; }
        public string NotificationContent { get; set; }
        public TextArguments NotificationArguments { get; set; }
        public bool SendNotification(){
            //send the message and confirm
            return true;
        }

        public TextNotification(
            List<string> p_Addresses,
            string p_NotificationContent,
            MailArguments p_NotificationArguments)
        {
            Addresses = p_Addresses;
            NotificationContent = p_NotificationContent;
            NotificationArguments = p_NotificationArguments;
        }
}


public class EmailNotification : IInstantNotification<MailArguments>
{
    ...
}

I can then instantiate a class, pass the args, and send the message. Pretty straight forward.

TextNotification TextObj = new TextNotification(myAddresses,myNotifContent, myArgs);
bool success = TextObj.SendNotification();

Instead, I always end up doing something like the following: scrapping the interfaces and putting everything in a static class for organization purposes only.

public static class TextNotification
{
        public static bool SendNotification(
            List<string> p_Addresses,
            string p_NotificationContent,
            MailArguments p_NotificationArguments)
        {
           //send the message and confirm
            return true;
        }   
}


public static class EmailNotification
{
    ...
}

It seems like the steps to take the action of sending a notification now have a lot less overhead (and are just as easy to understand).

bool success = TextNotification.SendNotification(myAddresses, myNotifContent, myArgs);

To my ignorant more functional programming oriented mindset, the latter is pretty simple and it seems like the best way to implement things. I have really been struggling wrapping my mind around the reason for doing it the more "OOP" way (like at the top of my post). I realize this might be too trivial of an example, but it is the best one I could come up with.

This is all coming from a middle-tier application code perspective. Oftentimes, my code intercepts an http request, invokes static functions to execute business-layer actions, and those actions call my data layer (which are just a bunch of static functions) that then call stored procedures and things bubble back up until the response is eventually returned to the client.

Representing business-layer actions with fancy OO design patterns seems pointless.

I humbly ask for someone to help me understand where my thinking is flawed. I really want to embrace the world of OOP and its fancy design patterns, as well as being able to fully leverage C# and its potential as an OOP language. I feel like I am doing a disservice to myself writing functional-first C# code...

10
  • 2
    An excellent balance of what you want to know can be found in the book by Joshua Kerievsky called "Refactoring to Patterns", which also contains examples of factoring away from patterns (and object-orientation). Jan 18, 2021 at 8:43
  • 4
    I have nothing to add to the accepted answer but noted something unOOPy in your OOP example that could help to be aware of. Your notification interface should not have a Send method. A message does not send itself, the notification should stick to stuff that applies to the notification itself. For sending/delivering that package you should have a different class. This allows for different classes to handle notifications differently. The notification is an entity but not a router. Jan 18, 2021 at 8:56
  • 1
    Actually, I think the OOP world has to come closer to the way you think, since OOP by definition does "data-hiding", probably better stated today as "state-hiding". The OOP world tends to make the mistake of calling other paradigms "less" when they should be trying to embrace functional programming more because of a multicore world. I think F# is a step in the right direction, but we as a professional industry should somehow figure out how to do a better job of moving more developers in the F# direction. We need the simplicity of the functional , which is a strength and not a weakness. Jan 18, 2021 at 9:40
  • 1
    @MicroservicesOnDDD I will definitely take a look! Thank you. I have always struggled with the balance in design patterns and abstractions as the code base becomes increasingly more complex.
    – spencer741
    Jan 18, 2021 at 10:08
  • 1
    @MartinMaat ah. Abstracting the actual sending functionality makes a lot of sense. Thanks for catching that!
    – spencer741
    Jan 18, 2021 at 10:10

4 Answers 4

16

"I realize this might be too trivial of an example"

That is the point. Programs start usually simple and small, but become more and more complex over time. Without introducing more structure during this process, one will easily produce a big ball of unmaintainable mud. OOP provides tools for solving this problem - giving programs more structure.

Said that, there is no need to jump from "no structure" to "fullblown OOP" in one step when a system grows - it is not even desirable. There is a middle-ground, like using classes as simple DTOs or for introducing new datatypes, then converting certain operations on those data types into member functions of those classes. There is also no need to introduce classes, objects and interfaces everywhere, when only a certain area of a program goes beyond a certain complexity.

In your example above, you may consider to introduce a non-static class TextNotification when the program becomes larger. A reason for this could be that you notice many functions where you always pass the same three-fold combination of adresses, content and arguments around. If there is currently no need for using interfaces or member functions, why overcomplicate things by introducing them (yet)?

Later, you may consider to introduce member functions into the class which operate on or with the data. SendNotification may be such a candidate (though it is debatable if a notification should know how to "send itself", or if some other component in your system should be responsible for sending notifications).

Representing business-layer actions with fancy OO design patterns seems pointless.

Introducing OO design patterns without needs is pointless for any program or module, not just for business-layer actions. Use patterns when they make your program more evolvable and maintainable, not just because they seem to be popular. And "fancy" code is never a good idea, neither in a procedural program nor in an OO program.

1
  • 2
    Wise words in the world of the blind. Seriously, this answer is gold - we often forget that we don't need to overshoot complexity nor apply the same structure to everything.
    – T. Sar
    Jan 18, 2021 at 16:26
2

Short answer: OOP principles make code more maintainable.

In your example however, there isn't much need to use a generic interface if you only have 2 types of InstantNotification. But what if you start to add more kinds of Notifications? You'll have to test and define them all individually even though their behavior is essentially the same. This goes back to the DRY principle (Don't Repeat Yourself)

That way, your main program can have a collection (array, list or enumerable) of IInstantNotification without needing to know the exact type, so rather than calling:

TextNotification.SendNotification();
EmailNotification.SendNotification();
PushNotification.SendNotification();
FacebooklNotification.SendNotification();
SalesforceNotification.SendNotification();

It will look more like:

public List<IInstantNotification> Notifications = new List<IInstantNotification>()
foreach (var notification in Notifications)
{
    notification.SendNotification();
}

Another thing to consider is separation of concerns and the single responsibility principle. The interface you defined has one purpose (to describe behavior), if this can be done in one module, it should.

By putting your Notifications directly in the main program, it keeps the code tightly coupled, meaning that if you add a new type of notification, you will need to change the main program accordingly but if you use the abstraction of interfaces then your main program doesn't have to worry about the details of each and every Notification.

As for testing, the use of interfaces allows you to "Mock" behavior (Moq is a popular testing framework). Again, this allows you to write less code by setting the state of an InstantNotification rather than instantiating one along with all its dependencies.

1

As was already mentioned in the previous reply, the fact that you can do something does not necessarily mean that you have to :) In your case, it depends on whether you need to instantiate your classes and actually work with their object containing various data/states, or not. Using static classes/methods is in a certain way a bit against OOP principles since you actually do not use objects = instances of some class. If it is compliant with your business logic to have just a container for some methods you want to call instantly and they do not depend on objects' state, you can use static class.

-1

Representing business-layer actions with fancy OO design patterns seems pointless.

You're not wrong.

It's pretty common to have the "business logic" layer organized using more procedural* programming, eg using a service-oriented design or the Transaction Script pattern, but have the "domain model" be more object-oriented.

So in your example you really have a "notification service". In modern C# practice you would probably use IoC instead of static classes, but the pattern is really the same. eg you would have something like:

public class TextNotification : INotificationService
{
        string twilioKey;

        public TextNotification( IConfiguration config)
        {
           this.twilioKey = config["TwilioKey"];
        }
        public async Task SendNotification(
            List<string> p_Addresses,
            string p_NotificationContent
            )
        {
           //send the message 
            return;
        }   
}

Which has some OO aspects to support service substitutability and testability, but the business functionality remains purely procedural.

*"functional programming" means something different

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.