The Service Factory is a big paradigm shift for OneStream developers. Let's look at some of the capabilities they enable, and how they can help you organize your code in a better way.
The Service Factory model was first introduced in OneStream 8.0, as the cornerstone of what we now call "Dynamic Dashboards". An explanation of its role and how to configure the system to use a Factory is available in the official documentation, but it is often difficult to grasp the difference it makes in actual practice. Here we will cover a few configurations that might help clarify the possibilities we have in our shiny new world of Services and Factories.
Intelligent Dispatching
One might be already familiar with the basic implementation of a Factory: checking the type of call, and dispatching it to a Service of suitable type. For example, the following code will check if a Component Service was requested (because the user clicked on a button with an Action), and dispatch our implementation of such Service.
Select Case wsasType
Case Is = WsAssemblyServiceType.Component
Return New MyComponentService()
If we look at the signature of the CreateWsAssemblyServiceInstance function that encludes this Select block, however, we might notice that there is a lot more information on the context that we can play with:
This means we can dispatch calls to different services depending on some condition, like the contents of a specific Text property. For example, assuming one implemented a Service that caches the results of some slow SQL query, the following example shows how such service could be enabled or disabled by modifying the Text4 property of the Workspace from which the call originates:
Select Case wsasType
Case Is = WsAssemblyServiceType.DataSet
if workspace.Text4.XFEqualsIgnoreCase("CacheRecords") Then
return new DataSetCachingService()
else
return new DataSetDirectService()
end if
In a similar manner, we could perform a check on the day of the week (in an extra function added to the Factory class), and perform different Data Management tasks:
Select Case wsasType
Case Is = WsAssemblyServiceType.DataManagementStep
If Me.IsThisTheWeekend() Then
Return new LongRunningBackupJobService()
Else
Return new QuickBasicBackupJobService()
End if
Of course, we could have implemented this check further downstream, inside the actual Service code. However, having very long-winded and complicated classes doing everything in one place, makes solutions hard to read and maintain! Thanks to Assemblies, we can now break up our code into smaller chunks, which are easier to reason about (particularly when coupled with XFProject - more on this in a later post).
Even if you don't like placing this type of check in a Factory, you can "recompose" smaller services with techniques like the ones we will discuss in the next section.
Complex Adapters
In some situations, to favour code reuse and composition, a Service can simply use another Service.
For example, let's say we want to implement a caching mechanism for some data that is slow to retrieve. We have already implemented a Service that retrieves the data, it works fine and we don't want to touch it; we just want to save its output in some circumstances. So we create a new Service with some custom functions to manipulate the cache, and just invoke the old service when we need to actually fetch data:
Public Function GetDataSet(...) [....]
Try
if me.IsCached(args.DataSetName) then
return me.GetFromCache(args.DataSetName)
else
Dim fetchSvc As New DataSetFetchingService()
Dim ds as DataSet = fetchSvc.GetDataSet(si, globals, workspace, args)
me.SetInCache(args.DataSetName, ds)
end if
Note how the code is fairly easy to read, expressing high-level tasks which are then implemented in separate chunks.
This is, in a way, conceptually similar to the old strategy of calling Business Rules from other Business Rules; but it is somewhat less formal, and of course contained in a single Workspace or Assembly, making it easier to move across applications.
Advanced Orchestration
Factories must return Service objects, of course; however, there is no rule about having to create such instances right there and then. What if I had a Factory that gets its Services from... another Factory?
For example, let's say we want to build a particular interface in substantially different ways depending on whether the user is an administrator or not. That setting will determine so many things, that you might end up having completely different services for dashboards, components, even Custom Calculate jobs.
AdminFactory will dispatch to Admin services, and UserFactory will dispatch User services, keeping their logic very clean. And then in EntryFactory, you would just delegate the choice to one factory or the other depending on privilege checking:
Public Function CreateWsAssemblyServiceInstance(...) [...]
Try
If BRApi.Security.Authorization.IsUserInAdminGroup(si) then
Dim myAdminFactory As New AdminFactory()
Return myAdminFactory.CreateWsAssemblyServiceInstance(si, globals, workspace, wsastype, itemname)
else
Dim myUserFactory As New UserFactory()
Return myUserFactory.CreateWsAssemblyServiceInstance(si, globals, workspace, wsastype, itemname)
end if
Catch ex As Exception
Throw ErrorHandler.LogWrite(si, New XFException(si, ex))
End Try
End Function
Instead of having code that checks the privilege for each individual service, polluting our class with repetitive (and error prone) copypasted lines, we have one clear check at the very start of the process, and everything flows from there.
Refactor your Factory!
I hope these brief examples demonstrate, in a practical way, some of the advantages of our Service Factory paradigm. Although initially conceived for tasks related to Dashboards, the Service Factory has now become a key element for any advanced solution built on OneStream, powering everything from XFBR Strings to Dynamic Cubes! So mastering factories will be a must for developers going forward.
Did you ever build a complex Factory you're proud of? Let us know in the comments!