Design a Powerful Plugin System by using PropertyWrapper and Mirror
The plugin system is a classical software design pattern. The most important parts of design in it are how to manage plugins and how to dispatching plugin messages. Managing plugins isn’t our topic today, I will introduce a particular design to dispatch or observe messages through Swift’s reflection mechanism and the PropertyWrapper
syntax.
PropertyWrapper and Reflection
@PropertyWrapper
is a Swift attributer that you can use it to mark a property, then you can execute some operations when the property is written or read. For detail you can see the blog I written early.
The Swift reflection mechanism is so weak compared to the runtime system in Objective-C, we rarely utilize it to do anything in our projects. Let’s see what the Swift reflection can support for us.
1 | class Person { |
As you can see, using reflection can only access the object’s stored properties. Going a bit deeper, you will find the value of the child is a readonly property that means you can not assign a new value to the property, this is why the Swift reflection so useless.
But if the properties are of reference type, we can modify theirs member properties freely even though theirs reference pointers are immutable. How can we make sure that all properties we specified are of reference type and be used as same as normal properties? The property wrapper is the answer.
We declare a property wrapper class to wrap all properties we want to reflect in objects like below:
1 | @propertyWrapper |
See the code above, you won’t be confused if understanding the property wrapper’s underlying implementation. The value are now of type ValueObserver
, so we can now assign a new string "csl"
to its wrappedValue
property.
By using the PropertyWrapper, we have breakout the restriction of the reflection that forbids us modifying the reflection properties. Nowadays we can exploit the combination above to construct a powerful plugin system with implicit plugin registering, precise message dispatching and bridging to RxSwift seamlessly.
Plugin Register
Our plugin system applies the mediator design pattern, there are three key types: PluginContainer
, Plugin
and Message
.
The Plugin
is a protocol type that every classes or structures applying to it can register themselves as plugins into the plugin container.
1 | public protocol Plugin { |
You can regard the plugin container as the mediator and all registered plugins communicate with each other by sending messages to the plugin container. The plugin container will separate these messages according their types and resend them to corresponding plugins that subscript the specific plugin message types.
1 | public final class PluginContainer { |
There is a brief sample above showing the base structure of the plugin container to manage plugins. The most crucial question is how to observer messages and dispatch messages. Next, we need to understand the PluginMessage
and the MessageObserver
.
PluginMessage
The PluginMessage
type is an empty protocol. Every type conforming to the PluginMessage
protocol can be treated as plugin messages can be sent into the plugin container.
1 | protocol PluginMessage {} |
When designing a plugin system, the format of the plugin message is the first question we should consider carefully.
Normally, we can design the PluginMessage
to a concrete struct type or class type, as shown below.
1 | struct PluginMessage { |
As shown above, the PluginMessage has two properties, type and data. The type
string indicates the message’s kind. Of course, you can change the string type to enum type. The data
is used to carry the message data.
There is a nasty weakness that you cannot get a structured message data when receiving a plugin message filtered by a specified type. We must forcefully cast the message data to the original real data type. The casting operation requires us to understand the original data type beforehand that we don’t know.
But if we don’t explicitly use a type property to distinguish the message and instead use the meta type of messages (one type of message refers to one kind of message), we can use custom properties in messages of different types to pass through type-checked data.
1 | struct RefreshCourseInfo: PluginMessage { |
MessageObserver
The MessageObserver
is a property wrapper for observing plugin messages of specified types.
1 | @propertyWrapper |
If a plugin needs to subscribe to a kind of plugin message, we can create a message observer property typed with the plugin message ready to be subscribed in the plugin.
1 | class TestPlugin: Plugin { |
As shown above, we can subscribe concisely a plugin message by declaring a instance property annotated with the MessageObserver property wrapper.
Message Dispatching
Finally, we should complete the send(message:)
method in the PluginContainer in charge of distributing messages to plugins.
1 | func send<Message: PluginMessage>(message: Message) { |
First of all, we iterate all registered plugins and query their members with the Swift reflection. Then we filter the message observers observing the message type. Finally, we assign the plugin message to every observer’s wrappedValue to trigger the event sending in RxSwift.
Conclusion
The example above is just a toy code but clarifying the core concepts and tricks we needed.
Besides that, we can add message dispatching cache, message interceptor, message state and so on features.