Hey, hey, hey!
The Observer Class allows you to track state changes and notifications.
(get it? because it observes?)
Dependencies
- A Signal module
- Knowledge of how object-oriented programming works.
Let’s jump straight into creating one:
You should start your first lines with something like this.
--!strict
-- observer.luau
local SignalPlus = require("../Dependencies/SignalPlus")
--[[
Observer class to handle state changes and notifications
]]
local Observer = {}
Observer.__index = Observer
And before we forget, your API/types should look something like this:
export type Observer<T> = {
_value: T,
Changed: SignalPlus.Signal<(newValue: T, oldValue: T) -> ()>,
_connections: { SignalPlus.Connection }?,
Get: (self: Observer<T>) -> T,
Set: (self: Observer<T>, value: T) -> (),
Watch: (self: Observer<T>, callback: (newValue: T, oldValue: T) -> ()) -> SignalPlus.Connection,
Map: (self: Observer<T>, fn: (current: T) -> T) -> (),
Destroy: (self: Observer<T>) -> (),}
}
export type observer_mt<T> = setmetatable<Observer<T>, typeof(Observer)>
Next, create a constructor:
--[[
```luau
local Observer = require(path.to.observer)
-- Create a new observer with an initial value
local counter = Observer.new(0)
```
]]
function Observer.new<T>(initialValue: T): observer_mt<T>
return setmetatable({
_value = initialValue,
Changed = SignalPlus() :: SignalPlus.Signal<(newValue: T, oldValue: T) -> ()>,
_connections = {} :: { SignalPlus.Connection },
}, Observer)
end
Create a method for accessing the internal value:
--[[
```luau
-- Access the current value
local currentValue = counter:Get()
print("Current value:", currentValue)
```
]]
function Observer:Get<T>(): T
return self._value
end
Continue with the most important method here: setting the value.
--[[
```luau
-- Set a new value
counter:Set(5)
print("New value set to: ", counter:Get())
```
]]
function Observer:Set<T>(newValue: T)
if self._value == newValue then
return
end
local old_value = self._value
self._value = newValue
self.Changed:Fire(newValue, old_value)
end
After creating the most important method, create a :Watch method to watch the value.
--[[
```luau
-- Watch for changes
local connection = counter:Watch(function(newValue, oldValue)
print("Counter changed from", oldValue, "to", newValue)
end)
```
]]
function Observer:Watch<T>(callback: (newValue: T, oldValue: T) -> ()): SignalPlus.Connection
if not self._connections then
self._connections = {}
end
local conn = self.Changed:Connect(callback)
table.insert(self._connections, conn)
return conn
end
Alternatively, you could utilize the .Changed event like this:
counter.Changed:Connect(function(new)
print("Counter changed to" .. tostring(new))
end)
To finalize your module, top it all off with a:
function Observer:Destroy()
self.Changed:Destroy()
table.clear(self)
setmetatable(self, nil)
end
Don’t forget to return your module.
return Observer
With all that, you could add your own other stuff like:
--[[
Calculate a new value based on the current value and a function.
```luau
local observer = Observer.new(10)
observer:Calculate(function(current)
return current * 2
end)
-- observer:Get() is now 20
```
]]
function Observer:Map<T>(fn: (current: T) -> T)
local newValue = fn(self:Get())
self:Set(newValue)
end
and
--[[
```luau
local none = observer.None
-- An observer with no value.
none:Expect("nil value")
]]
Observer.None =
table.freeze(setmetatable({ _value = nil, Changed = SignalPlus() }, Observer)) :: observer_mt<any?>
or
--[[
```luau
local observer = observer.None
local expected = observer:Expect()
-- expects a value from the observer.
-- if no value, it throws an error.
]]
function Observer:Expect<T>(str: string): T
return assert(self:Get(), str or "Expected a value from observer.")
--return if self:Get() ~= nil then self:Get() else error(str or "Expected a value from observer.")
end
Thanks for listening in, and kindly tell me if there are any inconsistencies to be modified or any improvements to be made.