Last Updated: 2023-12-03
There are at least three components:
- an event: this is a data container holding information related to the event. E.g.
OrderShipper will contain the
- a listener: this performs actions necessary to respond to the event.
- some some of message bus to connect the events to the listeners.
There is probably also a configuration file with a dictionary mapping event types to arrays of listeners.
Coupling is direct knowledge one component has of another. Tight coupling is a pointer directly to a concrete class that provides the required behavior. Loose coupling is a pointer to an interface of some description.
Imagine this (in ruby, not PHP mind you)
class Post after_create :create_feed, :notify_followers def create_feed Feed.create!(self) end def notify_followers User::NotifyFollowers.call(self) end end class PostsController def create @post = Post.build(params) @post.save end end
gets rewritten as:
class Post end class PostsController def create @post = Post.build(params) if @post.save publish(:post_create, @post) end end end class FeedListener def post_create(post) Feed.create!(post) end end class NotifyFollowersListener def post_create(user) NotifyFollowers.call(user) # You can add more actions here too, if you like. end end
What are the consequences of this rewrite?
Postmodel will be simpler to test, since
NotifyFollowersneed not be mocked.
Postmodel, as it can focus on being a repository for data.
PostsControllernow has an extra responsibility and has become fatter: it must now publish the relevant events.
post_createthat is also in the controller. This does not seem terribly different from a method call, since we have to pass parameters to the event.
post_createnow need the new param with modified payload. Also because we need to have access to this additional information in other places, at least if we want not nullable params). Therefore refactoring is more difficult.
after_createetc. work with. What if a junior writes code and forgets to call the associated events?
e.g. because they are considered working, or you don't want to incur a round of QA, or, the code truly belongs elsewhere, or their team are hostile to you going in and making changes.
Here, if their code simply publishes an event whenever relevant, you can hook your code into (or swap out, accordingly) without having to mess with their code.
I guess the obvious case of dependencies to avoid are binaries that need to be installed on a system. But even within a single binary program, it could be some dependency that is difficult to test, or that is slow/memory heavy to initialize or pass around, or which breaks the layering.
Your documentation states that specific events are raised under specific circumstances. They can then, in turn, subscribe and respond to those events.
const el = document.getElementById("close_popup") el.addEventListener("click", (event) => hide(event.target))
The browser code might look like
Allow you to throttle heavy load periods (e.g. if Obama tweets a lot of work needs to be done and it would overload the system by using all RAM then going to super slow virtual memory in dealing with this). Better to release in manageable batches with a queue worker.
Firstly, their coupling differs:
notify(payload)on every entry in the list.
Therefore with pub-sub we can communicate messages between different system components without these components knowing anything about each other's identity.
Secondly, although not mandatory, observers are usually synchronous, pub/sub is usually async (message queue).
Thirdly, the observer pattern is implemented in a single application, whereas pub/sub may be cross-application.
PostsControllerabove), has more difficulty seeing the full picture than someone looking at the
after_createmacro. (Hmm... how would I get up to date in that situation? I guess I would just grep for
post_create- or perhaps have a tool for mapping these out into a text file/diagram.