Within the Magento 2 architecture, there are numerous ways of changing the behaviour of the original code. Dependency Injection allows for tricks with types and virtual types. However, the three main ways of modifying original code are preferences, observers and Plugins.
Try to stay away from overwrites through preferences
Using a preference, you can tell the ObjectManager (responsible for handling Dependency Injection) to swap one class with another. The core of Magento is based on this mechanism. Instead of having classes depend on other classes, classes should depend on interfaces. And next, the ObjectManager uses preferences to map an interface to an actual class. This allows for loose coupling.
Once the core defines a preference for an interface, you can also rewrite this preference into yet something else. For instance, the PSR logger interface in the core is mapped to a Magento version of Monolog. But you can swap out Monolog for another PSR-compliant logger simply by creating a new preference, that overrides the original preference.
This sounds cool but actually leads to conflicts quickly. As soon as multiple extensions decide to rewrite the same preference, the last preference will win, leading to a bug in the extension doing the first preference override. In general, extensions should stay away from overriding preferences. You can still define your own preference. And an extension is still able to override a preference, as long it is really obvious to everyone that the extension can only accomplish its purpose through a preference override.
Observer or plugin?
So the title of this blog doesn't mention preferences because modifying the system extensively through preferences is a bad idea - it leads to extension conflicts quickly. Instead, Magento contains two other mechanisms that allow for more stability: Observers or plugins. But which one is better?
They both have advantages. But they are different. And it leads to a discussion which method offers the right approach. Let's look into both options more closely first.
Events and observers
Magento (1 and 2) offers a way for some code to trigger an event, that is then picked by an EventManager which allows observer classes to react to that event. The mechanism seems to be related to the Observer/Observable pattern. However, because the EventManager is sitting in between and because the data sent to the observers is only modifiable when the original code (the observable) decides to pick up on changed data, it is more a Publisher/Subscriber pattern.
A specific point of extensibility
The cool thing about events is that the owner of the original code is to single out a specific point in the code, where the code is meant to be picked up upon by others: The event. The code itself, therefore, declares the specific spot where others can hook in on the code. This is also the downside of events. If the original author did not come up with an event, the code might be a black box which you can't modify. It depends on the imagination of the original author.
Plugins and interceptors
While events and observers already existed out there in Magento 1, Magento 2 introduced plugins. I often write Plugins with a capital P to avoid confusion. An original class can be extended upon by multiple plugin classes and Magento merges these classes together into yet a new class - the interceptor class. Thanks to the Dependency Injection tricks that are played by the ObjectManager, the original class is actually replaced with the interceptor class, without the original class needing to know about this.
The last line is important here: The original code does not need to know about what kind of modifications are made through plugins. While events are explicitly triggered in the original code for extensibility, plugins don't require anything of the kind. Any public method can be extended upon using this plugin/interceptor logic.
Public methods are always open to extensibility
The plugin logic hooks into a principle known as Aspect Oriented Programming (AOP), where the original code does not need to be modified to become extensible. Class inheritance would be - theoretically speaking - an example of AOP. However, inheritance also increases coupling between classes, because a child class suddenly couples itself with all public and protected methods of the parent.
With the plugin feature of Magento 2, only specific methods are coupled between the original class and the plugin class. Even cooler: Because most classes in Magento are usable through Dependency Injection (directly or indirectly via factories or whatever), any public method becomes an API to build a plugin for. All public methods are open to extensibility.
Plugins are cooler than events
Because of the above, because of the extreme flexibility of being able to extend any public method, plugins are cooler than events. This is what most developers nowadays say. So if this is true, this could be the end of events, because why would we need events in the first place.
Events are a way of communicating
However, there is still an important reason for events to be used: They form a way of communicating. A well-written library would only offer a certain amount of public methods as an API to developers. This would mean that all methods are open for extensibility. But is that a smart thing to do?
Dummy example: Modifying prices in the wrong way
Let's imagine an ERP-connector that contains a product-adapter with a limited set of public methods, containing a
getPrice() method. You could state that any public method in this class is open for usage, so - thanks to the AOP mechanism in Magento 2 - we can add a plugin-method to modify the output of
Now, let's say that same ERP-connector uses the
getPrice() method to push product prices into the ERP system. But let's say that the price in the ERP system needs to be the exact same price as stored in the Magento database. And let's say that one of the goals of the ERP-connector would be to guarantee that prices in the Magento database are the exact same as in the ERP system. Using a plugin method
afterGetPrice(), we could modify the price anyway, without the ERP-connector knowing about this - thus the ERP-connector would loose its ability to govern its responsibility.
Minimizing API methods
Of course, because we would be responsible for this (breaking?) change and we would build this on purpose. However, we have now found a flaw in the ERP-connector, where a method
getPrice() could introduce unexpected behaviour simply because it is exposed as an API to the outside world. As the developer of such an ERP-connector, I would try to prevent this thing from happening in the first place. So logically, if I want to prevent people from messing around with
getPrice(), it shouldn't be public, it should be private instead.
Developing methods as being private at first is a good thing. It minimizes the number of methods exposed as API, therefore minimizing possible bugs. And there are also fewer tests needed to guarantee its behaviour.
Events are still cool
From this point of view, I would say that making methods extensible (by making them public) is not always a good thing. But setting
getPrice() to be private prevents any extensibility. In this case, an event like
beforeSendPriceToErp(['price' => $price]) might still be a good option.
Plugins are really cool and they allow you to hack the whole system. However, events still fulfil their purpose and we shouldn't forget about this. An extensible module with a lot of events and little public methods might still be a really SOLID piece of code.
The performance bit
There is another argument that also needs to be taken into account: Performance. With events, there are always multiple objects in play when running the code: The original object generating the event, the event object, the observer object and the EventManager. With a plugin, there is actually one object in the end: The compiled interceptor object. This makes plugins more performant.
However, what benefit are we debating here? The EventManager is always initialized (for now). The event object should be seen as a Value Object and we should love Value Objects anyway. The fact that more objects are used with events & observers is not necessarily a bad thing. Especially when you take into account that Magento is not known for its small number of classes (objects) anyway.
Observers or plugins?
So which one is it? Observers? Or plugins? I would say they both fulfil a real purpose. And they both, therefore, add value. If it is just a bunch of web shops that you are building, I would say that plugins will get you there fast most of the time. So because plugins are so cool, do make sure to get used to working with them.
However, if there is a use-case - for instance, a generic ERP-connector that should be both flexible and stable - it might be that events guarantee more stability towards all involved parties. So, my take would be that Magento needs both.