R
R
random552022-02-02 11:45:30
Software design
random55, 2022-02-02 11:45:30

Which approach to use?

Every time I need to write some service, I am faced with the following question:

Suppose you need a service that performs user notifications. You can notify, for example, in Slack, via SMS and Telegram. I see 2 solutions

1. Create some abstract Notifier class with an abstract notify method,
and for each service (Slack, SMS, Telegram) create child classes that inherit Notifier (SlackNotifier, SMSNotifier, TelegramNotifier), in which to implement the nofity method in their own way.
After that, get the necessary implementation to the controller from the DI container, depending on the configuration.

2. Create a very specific Notifier, and in it, as a dependency, instantiate SlackDriver, SMSDriver or TelegramDriver that implement a common interface with the notify method, and delegate execution to the instantiated driver in the Notifier::notify() method:

public function notify() {
    $this->driver->notify();
}


And so I do not understand which approach is better and by what criteria? In the case of a specific Notifier, the controller will not depend on the abstraction (is this bad?). In the first case, we can replace the Notifier itself, in the other, its driver. I understand this is the same question: "inheritance or composition"? Help me to understand.

Answer the question

In order to leave comments, you need to log in

2 answer(s)
M
Michael, 2022-02-02
@random55

Notifications - in the real world, this is a very complex topic, with a lot of underwater rakes and regularly changing requirements. Let's see what entities we have here in terms of the Single Responsibility Principle (SRP):
1. NotificationChannel. This is it: Email, SMS, Telegram, Slack, VK, etc. It is his responsibility to send the message in a certain way (a certain text to a certain address). I would write its interface like this:

interface NotificationChannel {
  void send(String to, String text);
}

2. Notifier. It is his responsibility to generate the notification. And here, too, options are possible, since different users need to be notified about different events and in different ways. It is his responsibility to form the text of the message and send it through a specific channel. The interface looks something like this:
interface Notifier {
  void notify(User user, Event event);
}

3. Further options are possible. In particular, the formation of the text of the message can be separated into a separate responsibility by creating a class (or interface) NotificationFormatter. If you plan to configure notification channels for each user, then you may need some kind of NotificationChannelManager, etc. You can dive deep into this rabbit hole. It all depends on how detailed you want to decompose the subsystem into objects.
Personally, I would start at least with the Notifier interface, which allows you to "close" the implementation details of the notification subsystem from the calling code. This border, in my opinion, will not be superfluous. And then, at will or if necessary, I decomposed its implementation into separate, unrelated classes.

@
@insighter, 2022-02-02
_

Here the first abstraction is UserNotifier, a service that notifies the user. It hides notification methods. Something like one Notify method. It can be implemented through a DI container.
And it is internally implemented with knowledge of the notification delivery channels. But even here I would abstract from specific implementations through Slack, SMS, Telegram. The connection of specific implementations should be handled by the application, not the implementation of the UserNotify service, the connection is also via DI.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question