This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the architecture category.
Last Updated: 2024-11-23
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 $order
.
- 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?
Post
model will be simpler to test, since Feed
and NotifyFollowers
need not be mocked.Post
model, as it can focus on being a repository for data.PostsController
now has an extra responsibility and has become fatter: it must now publish the relevant events.post_create
that 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_create
now 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_create
etc. 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.
Example: Client-side JavaScript hooking into browser events triggered by the user.
const el = document.getElementById("close_popup")
el.addEventListener("click", (event) => hide(event.target))
The browser code might look like publish("click", thisElement)
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.
PostsController
above), has more difficulty seeing the full picture than someone looking at the after_create
macro. (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.