Teneo Developers

Wrapper Objects

In some cases a single resource (eg. network call or solution file resource) returns a set of data, but only parts of the whole are required, and at different times, in different places within the solution. In these cases it can be useful to define a wrapper object for that data. This object can either be loaded up front or be Lazy loaded - but the principles of the process remain the same.

Define the Wrapper Object

This wrapper object is populated (up front of lazy on first call) with the full "whole" object from which the specific data will be extracted:

groovy

1// Solution loaded
2class LazyWrapperObjectExample {
3    private Lazy<Object> _fullObject = new Lazy<Object>(() => JsonSlurper.parse("some_url/object_path".toURL()))
4}
5
6class WrapperObjectExample {
7    private Object _fullObject
8    UpFrontSessionDataExample() {
9        _fullObject = JsonSlurper.parse("some_url/object_path".toURL()))
10    }
11}
12

Define the return values

Each required value can then be created as a separate method to get just that particular part of the whole object:

groovy

1// Solution loaded
2class LazyUserWrapperExample {
3    private Lazy<Object> _user = new Lazy<Object>(() => JsonSlurper.parse("${serverLocation}/user/${userId}/".toURL()))
4
5    static String getLevel() { return _user.value.level }
6    static String getName() { return _user.value.name }
7}
8
9class UserWrapperExample {
10    private Object _user
11    UpFrontSessionDataExample() {
12        _user = JsonSlurper.parse("${serverLocation}/user/${userId}/".toURL()))
13    }
14
15    String getLevel() { return _user.level }
16    String getName() { return _user.name }
17}
18

Complex values

In some cases the object will not store the specific data required, in the form desired and will require some additional processing. In these cases a single method can be defined for the use case to:

  • Extract the correct information from the object.
  • Return it in a form useful for the user of the method.

groovy

1class UserWrapperExample {
2    // Load the data as above
3
4    // Check the user's age is within a specific range
5    // Standardised the concept of "Middle Age" for use throughout the solution
6    // If this definition changes - code changes in one place only
7    Boolean getIsMiddleAged() { return _user.age >= 40 && _user.age <60 }
8
9    // Only the Ids are required by the calling user - eg. to pass to an API call
10    // Return only the useful information, hiding the inner structure of the user object
11    // If the structure changes in the future - code only needs changing here
12    List getAccountIds() { return _user.accounts.collect { it.id } }
13
14    // The calling user only wishes to check if the user has a specific permission
15    // Hide the structure and implementation inside a single method
16    // If the structure or validation method changes in the future - code only needs changing here
17    Boolean hasPermission(requiredPermission) { return _user.grantedPermissions.contains(requiredPermission) }
18    Boolean getIsAdmin() { return checkUserHasPermission(Permissions.ADMIN) }
19}
20

Benefits

There are many benefits to using these wrapper objects. Below are some examples.

Reduced data footprint

As the "full" object is stored inside the wrapper - the entire response object is not written to the session logs. This can potentially lead to huge saving in session data size.

If the results of the method calls are written to a logged object (for example a flow variable, or passed to an integration) then these individual pieces of data will be logged. As such it is still possible to trace the values used at runtime.

Simplified usage

The calling code does not need any understanding of the structure of the returned object (or in fact where the object comes from), the calling code can simply call currentUser.userLevel or currentUser.isAdmin and get a usable result. eg. add a script match (or scripted context): currentUser.isAdmin applied to a trigger or transition to protect admin actions from being executed.

Simplified understanding

Reading code that says:

groovy

1if(user.isMiddleAge) {
2    // Offer pensions advice
3}
4

Has a much clearer purpose than:

groovy

1if(user.age >= 40 && user.age < 60) {
2    // Offer pensions advice
3}
4

Simplifies maintenance

Users with knowledge of the API handle the interpretation, users with knowledge of the solution handle using the interpreted knowledge.

If the structure of the response changes then the code change only needs to happen inside this wrapper class, for example:

Return value change

A new layer is added in the user class containing all account information such as account start date, expiry and level:

user: { name, level }

becomes

user: { name, account: { startDate, expiry, level } }

To facilitate this, the wrapper class changes:

String getLevel() { return _user.level }

Becomes:

String getLevel() { return _user.account.level }

None of the usages across the solution need any updates.

Without the wrapper class every usage around the solution of user.level would need to be changed to user.account.level.

API change

If the source of the information changes then the code change is isolated within the wrapper class

A new "permissions" endpoint is added and the granted permissions are removed from the user request. This is to facilitate quicker roll out of permission changes - as such permissions should be checked with the server whenever they need validating.

To facilitate this wrapper class changes:

Boolean hasPermission(requiredPermission) { return _user.grantedPermissions.contains(requiredPermission) }

Becomes:

Boolean hasPermission(requiredPermission) { return JsonSlurper.parse("${serverLocation}/user/${userId}/permissions".toURL())).contains(requiredPermission) }

None of the usages across the solution need any updates

Without the wrapper class every usage around the solution of user.grantedPermissions.contains(Permission.REQUIRED_PERMISSION) would need to be changed to JsonSlurper.parse("${serverLocation}/user/${userId}/permissions".toURL())).contains(Permission.REQUIRED_PERMISSION)