34
loading...
This website collects cookies to deliver better user experience
Mediator
as an object encapsulating interactions between other objects (components
) from a given set. The pattern limits, or even cuts off completely, direct dependencies between classes. Components can only communicate with each other through the Mediator
, which becomes the central hub for information and control. The Mediator
controls the flow of information using its internal logic.Mediator
pattern seems to me to fit rather for refactoring close-by classes, the number of interactions of which has already begun to disturb their maintenance, but have well-established responsibilities. In some cases, the use of Mediator
will make sense from the very beginning of code development, but it can also be an overkill.Android Architecture Components
. It intercepts interactions with the UI and, according to its logic, informs the data model and the other way around - it passes changes in the model to the UI.components
Mediator
who is calling appropriatecomponents
Mediator
, which all thecomponents
know. Each component
is a separate and independent object that probably already inherits from another class or implements the needed interfaces. You may be tempted to introduce an additional Component
interface, but it would not do much, because the Mediator
needs to know the specific interfaces of its components
to be able to call specific methods after receiving the message.mediator
object can get components in the constructor, via DI, or even create the necessary instances and manage their lifecycle.interface Mediator {
// mediator declares methods for communication
fun method(sender: Any, args: Any? = null)
}
// components don't have to share any interface, but they all have to have reference to the Mediator
// they shouldn't have dependencies to eachother, it kills the point of having a Mediator in between
class ComponentA(private val mediator: Mediator) {
fun operationA() {
mediator.method(this, "arg A")
}
}
class ComponentB(private val mediator: Mediator) {
fun operationB() {
mediator.method(this, 10.34)
}
}
class ComponentC(private val mediator: Mediator) {
fun operationC() {
mediator.method(this, println("print me!"))
}
}
// Mediator encapsulates relationships between components
// it has references to all components it manages
// and sometimes it can even manage component lifecycle
class Concretemediator : Mediator {
// components may be injected or passed in constructor
// but Mediator reference must be passed to each component
val componentA = ComponentA(this)
val componentB = ComponentB(this)
val componentC = ComponentC(this)
override fun method(sender: Any, args: Any?) {
// checking which object is sender
when (sender) {
// Mediator knows how to handle the incoming message
is ComponentA -> println("arg from A: $args")
is ComponentB -> println("arg from B: ${args as Float * 3}")
is ComponentC -> (args as () -> Any).invoke()
}
}
}
notify(sender, event, context)
method, I used method(sender, args)
here, but the principle is the same, the Mediator
interface determines the form of communication. The event
in the former case can be an object inheriting from some generic interface, or a sealed class
, which will create a nice closed set of events that is easier to navigate through.Context
can indicate where the message is coming from, e.g. whether the product was added to the shopping cart from the list of recommended additional products or directly from the product page. The arguments of the method, or even the Mediator
methods, will depend heavily on the specific implementation, and the information needed.Mediator
is dialog boxes that contain UI elements such as buttons and text inputs. Oftentimes, the state of user interface elements depends on one another. The "OK" button should be inactive, if not all required form fields have been filled, etc. If there are many view elements, the number of such dependencies between them may increase and be difficult to control.Dialog
itself can be such a Mediator
.// view manager, controlling UI elements inside it
interface UiDirector {
// `event` is naive String for simplifying the example
// `sender` has to be UI element extending `UiElement` class
fun notify(sender: UiElement, event: String)
}
// `UiElement` class is an UI component inside `UiDirector`
abstract class UiElement(val uiDirector: UiDirector)
// button is UI element
class Button(uiDirector: UiDirector) : UiElement(uiDirector) {
fun click() {
// that informs `uiDirector` about being clicked
// but has no other logic regarding this
uiDirector.notify(this, "click")
}
}
class TextBox(uiDirector: UiDirector) : UiElement(uiDirector) {
// TextBox is also an UI element
// lets assume it has inner logic refreshing value of the `text` field
// that takes care of pasting the text or manual typing it
// using `observable` makes sure that after each change the `uiDirector` gets notified
// there is no need to remember about calling `notify()` in every place that changes `text`
val text: String by Delegates.observable("") { property, oldValue, newValue ->
uiDirector.notify(this, "text_changed")
}
}
// Dialog window, handles view state according to UI components events
class FancyDialog : UiDirector {
private val title = "fancy dialog"
// dialog creates instances of its `components`
private val okButton = Button(this)
private val cancelButton = Button(this)
private val input = TextBox(this)
private var inputText = ""
override fun notify(sender: UiElement, event: String) {
when (sender) {
// after each UI component change it gets update
input -> if (event == "text_change") inputText = input.text
// and has logic called after buttons are clicked
okButton -> if (event == "click") submit()
cancelButton -> if (event == "click") dismiss()
}
}
private fun dismiss() {}
private fun submit() {}
}
Delegates.observable
in the TextBox
class makes it easy to notify the view manager of a change. No need to do this in every place that updates the text
field.Chat
object that decides who gets which message.// helper alias
typealias ParticipantName = String
// Mediator interface
interface Chatroom {
fun registerParticipant(participant: Participant)
// message recipient is optional, when left empty the message is public
fun send(message: String, from: ParticipantName, to: ParticipantName? = null)
}
// Participant class has very simple API, limited to sending and receiving the message
// no logic controlling where the message actually goes
class Participant(val name: String, private val chatroom: Chatroom) {
init {
// the participant is registering itself in the chatroom
chatroom.registerParticipant(this)
}
// is able to send the message
fun send(message: String, to: ParticipantName? = null) {
chatroom.send(message, this.name, to)
}
// and receive it
fun receive(message: String) {
println("[$name] gets: $message")
}
}
// `object` is here just to make it easier to use in `main()`
object SuperChat : Chatroom {
// chatroom modes
enum class Mode {
PUBLIC, // every message is delivered to every participant
PRIVATE, // only messages with recipient will be delivered
MIXED // both direct messages and public ones will be delivered
}
var mode: Mode = Mode.PRIVATE
private val participants = mutableListOf<Participant>()
override fun registerParticipant(participant: Participant) {
participants.add(participant)
}
// handling messages
override fun send(message: String, from: ParticipantName, to: ParticipantName?) {
when (mode) {
// depending on the mode they will be handled differently
Mode.PUBLIC -> participants.forEach { it.receive("$from says: $message") }
Mode.PRIVATE -> participants.find { it.name == to }?.receive("$from says: $message")
Mode.MIXED -> {
if (to == null) participants.forEach { it.receive("$from says: $message") }
else participants.find { it.name == to }?.receive("$from says: $message")
}
}
}
}
fun main() {
val alice = Participant("Alice", SuperChat)
val bob = Participant("Bob", SuperChat)
val charlie = Participant("Charlie", SuperChat)
alice.send("hi all!")
bob.send("hi Alice", "Alice") // only this message will be delivered in PRIVATE mode
charlie.send("hi Alice")
SuperChat.mode = SuperChat.Mode.PUBLIC
}
PRIVATE
, only the message with the recipient will be printed [Alice] gets: Bob says: hi Alice
. The rest of the messages will go to the Mediator
but not to the other participants. Changing the message delivery policy will have this effect for MIXED
:[Alice] gets: Alice says: hi all!
[Bob] gets: Alice says: hi all!
[Charlie] gets: Alice says: hi all!
[Alice] gets: Bob says: hi Alice
[Alice] gets: Charlie says: hi Alice
[Bob] gets: Charlie says: hi Alice
[Charlie] gets: Charlie says: hi Alice
There's a bug here because Alice is getting a message from herself. Her hi all
had no recipient, so it went to all participants - including herself. Just like Charlie answering hi Alice
but without a recipient.
Mediator
in the form of aSuperChat
object. Additionally, it has its internal messaging policy, it is not just a proxy between objects. The chat could have a list of muted participants who, e.g. temporarily and as a punishment, cannot send messages. But instead of blocking messages from being sent, you can block messages from being delivered without alerting the participant about the `shadowban.Mediator
is a very simple pattern in terms of structure, it's basically a single interface. There is no need to add this to the name of objects that act as Mediator. Depending on the implementation, Mediator
may not even be visible outside, unlike the Facade
, that by definition, is an entry interface to some functionality.notify()
method can and should refer to the domain of the problem being solved, as in the send()
chat. This pattern should be transparent outside the class group and be only an implementation detail, and organizational improvement.Mediator
pattern cuts out dependencies between components. It takes over the interaction between them, becoming the main communication hub for a group of classes. There is a reverse of the controls because components are now just telling "what happened" instead of telling others to "do something". It can be found e.g. in the form of ViewModel
in Android, where it separates UI interactions from data model changes.Component
may also have only the notify()
method and decide for itself how to react to a given message from the Mediator
(two-way mediation), but this significantly complicates the matter and starts to be a different pattern.