7

On several occasions, I was faced with the following design issue that I don't know how to resolve. Imagine, for instance, an application which at some point receives a JSON object from an API. Before being able to use this object, the application should perform several transformations: for instance, one transformation may convert dates from one format to another, and another transformation may add extra data from cache (such as an entity describing a user, based on a sole user ID from the original object).

In order to be able to choose what transformations to apply at runtime, the transformations are defined in specific classes, such as DateFormatTransform and ExtraDataFromCacheTransform.

Basically, the code which performs the transformation would be like this:

foreach (var transform in this.GetRelevantTransforms())
{
    entity = transform.Apply(entity);
}

this.GetRelevantTransforms would return an instance of DateFormatTransform, ExtraDataFromCacheTransform and other classes. Each of those classes implement an interface containing the method:

SampleEntity Apply(SampleEntity x)

Later on, additional needs may arise, requiring to change the signature of Apply. For example, one may need the transforms to be aware of the current user, because depending on the user, some transformations may be executed differently:

SampleEntity Apply(SampleEntity x, User currentUser)

The problem is that meanwhile, I can have a few dozen of transforms, and such basic change would require to walk through those dozens of files, changing both the interface (such as IDateFormatTransform) and the class (such as DateFormatTransform).

I suppose that having to change about fifty files just to add one simple argument is not a sign of a clean code base.

What are the alternatives?

Should I use a DTO, such as TransformArguments, which would always be the only parameter of Apply method? Or are there other techniques?

Or is my approach with a set of transformations flawed from the beginning, and if yes, how do I fix it?

2 Answers 2

8

Essentially, it boils down to whether or not you can come up with a useful generalized interface for the call site(s), in a way that decouples parameter passing from the actual call - by encapsulating those parameters in the transform object and relying on polymorphism. You can do this if you can determine the parameters at creation time, but it may require some rethinking and some reorganization of the code (but, hey, that's more or less inevitable).

So, instead of changing
entity = transform.Apply(entity);
to
entity = transform.Apply(entity, user);
everywhere throughout the codebase, you would do:

var transform = new UserBasedTransform(user);   // at creation site   
//--------------
entity = transform.Apply(entity);    // at call site

Or, if you cannot determine the parameter at that time, you may pass in a factory that can return the parameter later on when it becomes possible:
Transform transform = new UserBasedTransform(userFactory); // at creation site

In a sense the interface of the transform is an abstraction (even if it's just the public methods of a class, and not an actual interface type) that represents a generalized way of working with transforms. You want to base your abstractions on the aspects of the problem that are relatively stable as the codebase evolves; in your case, it's the parameters that keep changing, so treat them as an implementation detail, and don't reference them anywhere in client code. What seems to be stable is that you are applying a transform to an entity, so organize your client-facing interface around that (or, if that's not the case, find something that has that property and use that). Note that this is specific to the change patterns you observed, so it's not about finding one solution that fits all situations, but about finding a design that works well for your particular domain.

This will likely simplify your clients, but it may require a different approach to constructing your transform objects.

1

It seems like whether or not the transformer needs knowledge of the user is an implementation detail and not relevant to the caller

Given this, can you inject a user context into the transformer that needs it?

class UserTransform {

    public UserTransform(IUserContextAccessor userContextAccessor) {
        _userContextAccessor = userContextAccessor
    }

    public SampleEntity Apply(SampleEntity x) {
        var user = _userContextAccessor.CurrentUser;
        // do user specific thing
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.