Services
Services (also referred to as Object
s) are Python classes designed to perform specific tasks. Each service encapsulates a distinct functionality and can have multiple accessories. These accessories include Properties and Signals, which allow the service to expose its state and notify users about important events. Services are typically subclasses of GObject.Object
.
Core Elements of a Service
-
Service: A service represents the core functionality encapsulated in a class. for example, we have a service called a
Fabricator
that polls data from a specific source (whether a Python function or a shell script). The service continuously polls these sources at a defined interval and manages the data flow into one of its properties. -
Properties: Properties in a service represent the state or data managed by the service. For instance, a
Fabricator
service contains a property calledvalue
which stores the result of the polling process. When this property changes, the service can trigger signals to inform other components of the application. -
Signals: Signals allow a service to communicate state changes or events. In the case of
Fabricator
, when thevalue
property is updated, a signal calledchanged
is emitted. This informs listeners that theFabricator
has new data. Additionally, a more specific signal tied to the property itself,notify::value
, is emitted, indicating that thevalue
property has been modified.
How to Define a Service?
it’s really easy to define a Serivce, here’s a snippet that does exactly that…
but since we didn’t dive into how Signals and Properties work. an empty Service won’t be able do anything except existing.
I will leave this example here to revisit it later when we learn more about Service accessories…
Signals
A Service
can have various accessories, with Signal
s being one of them.
Signal
s provide a simple state-management model, allowing a Service
to notify its users when specific changes occur. Additionally, a Signal
can pass arguments, enabling the Service
to both notify its users of a change and provide relevant details through these arguments.
Defining Signals
To define a signal, first decide on a signal name in kebab-case (e.g., signal-name
) to ensure it appropriately represents its purpose. Once the name is chosen, you can define the signal using the Signal
decorator within a Service
.
A signal’s handler function must be fully typed, meaning all its arguments and return types must be explicitly declared.
To import the Signal
decorator:
Example of using the Signal
decorator:
Since Python doesn’t allow function names with hyphens (as in kebab-case), you can define the function using snake_case (signal_name
), and Fabric will automatically convert it to the correct kebab-case format when needed.
Let’s walk through an example of how Signals
work:
Output:
In this example, the NameService
class defines a name_changed
signal that is emitted whenever the name is updated. The signal is connected to a listener, which prints the new name when it is changed.
By using Signals
, services can easily notify other parts of the application about important state changes.
Connections
Service
s offer a simple way to connect signals to callback functions directly within the constructor (__init__
). This allows for easy signal handling right at initialization.
Here’s an example on how to connect signals to a callback in a various ways:
This pattern allows you to define signal connections directly in the constructor or through explicit method calls, providing flexibility in how you manage your signals.
Properties
In a Service
, properties are attributes that represent the state or data managed by the service.
Since Property
inherits from GObject.Property
, it uses GObject’s properties system, which provides native support for type safety, property change notifications, and binding capabilities with other properties or UI elements.
Defining Properties
To define a property, use the @Property
decorator from the fabric.core.service
module. it requires a specific data type and optional flags. Here’s an example:
In this example, a Property
named status
is defined with a specific type (str
) and flags ("read-write"
) using the decorator, as well as setter
function for it.
Property Notifications
Since GObject.Property
automatically emits a notify::property_name
signal whenever the property value changes, other components can connect to this signal to track updates.
To connect a callback function that listens to property changes, there are several approaches:
-
Using
connect
withnotify::property_name
This is the most explicit way to connect to a property notification signal. -
Using a Direct Argument in
Service
Constructor
Since we pass**kwargs
over to thesuper
which’s aSerivce
, You can also pass the callback directly as a keyword argument when initializing theService
, usingnotify_<property_name_in_snake_case>
as the argument name.
Output:
Each method achieves the same result but offers flexibility in how you structure your code. Using constructor arguments (notify_<property_name_in_snake_case>
) can keep your property connections tidy, especially if they are set up at initialization.
Benefits of Using Properties: WIP
Property Bindings: WIP
Builders
The builder pattern allows for constructing complex objects step by step. In the context of a Service
, you can chain function calls to set properties and connect signals in one line, simplifying the setup process. This approach minimizes boilerplate code and makes the initialization more readable and efficient.
Here’s an example of how to use the builder pattern to initialize a Fabricator
object:
Explanation:
build()
: Initializes the builder object, which allows chaining subsequent calls.connect()
: Connects signals, here linking the “changed” and “notify::value” signals to their respective handlers (which are inline functions created using lambdas).set_value()
: Sets the initial value of the service.unwrap()
: Returns the actualFabricator
instance instead of the builder object. This step is important because, without it, you’d still be working with the builder object rather than the fully constructedFabricator
.
This chaining method enables you to configure the service in a single expression, making it cleaner and more efficient.
Alternatively, you can configure the Service
using a callback that receives two arguments: the Service
instance itself (self
) and the builder object used for chaining. This approach provides more flexibility if you want to further customize your service during initialization.
Here’s how to use the callback method:
Explanation:
- The callback receives two arguments: the
self
reference to theService
instance and thebuilder
object. This allows you to use the builder to chain function calls as needed, configuring signals and setting values in a flexible manner. - The chaining functions (
connect()
,set_value()
, etc.) work the same way as in the previous example, but this method allows you to encapsulate the configuration logic within a callback.
Why Use the Builder Pattern?
The builder pattern simplifies service initialization, enabling you to configure everything in a single expression or using a callback for more complex setups. It’s especially useful when you want to avoid multiple setup steps spread throughout your code and prefer a more declarative style.
Key Benefits:
- Readability: Function chaining creates a fluent interface, making code more readable by consolidating setup logic in one place.
- Flexibility: You can configure services in one line or use callbacks for more advanced initialization.
- Cleaner Code: Reduces the need for multiple lines of initialization logic, resulting in less boilerplate code.
This pattern is ideal when working with Services
that require property setting, signal connections, and other initialization logic in a clear and concise way.