2

I don't know if there actually is one design pattern, or several together, or maybe what i'm asking is anti-pattern.

Context (simplified) : my company sells SaaS services (through an API) to several clients, which in turn sells this service to end users. Each client uses the service, but some want custom behaviours in several features. Obviously, the custom behaviour should only occur for that specific client. The end user is linked to one specific client, so when using the service, we know which client identifier it is, so which behaviour to have on the requested feature.

My question is : by keeping only one code base, how do I manage different behaviours depending on the client identifier ?

The current implementation is obviously the naive and dirty one (simplified) :

function someFeature() {
  // common code

  if(clientId === 'someClient') {
  // does something
  } else if(clientId === 'otherClient') {
   // does something else
  } // else if ... for every other custom behaviour, or on other parts of the feature

  // common code
}

I've read about design patterns but they all seem to be focused on parts of my problem, and I don't see how to use them all together ...

If I had to dig a solution I would probably go with an implementation of Bridge and a Factory to instanciate various classes (for each client) which would implement a same feature differently (each clients' custom behaviour). With that in mind, when a user wants to use a feature, the approriate class, thanks to the factory, would be instatiated and used with the desired custom behaviour.

This implementation looks like the correct way to do it but I think it would encourage duplication of code (for the "common code" bits), and would require the programmers to create maybe thousands of new interfaces/classes for each features multiplied by each client's custom behaviour, sometimes just for a simple line of custom code.

What do you think would be the proper way to solve my problem ?

  • Tell, don't ask. Which means to use differentiated sub classes to handle the differing behaviors. Of course, if the clientId comes in from the outside, you still will have to consult it in order to build the right sub class... – Erik Eidt Mar 1 '18 at 18:40
4

Thinking about what design patterns to use usually isn't that fruitful an exercise. Think about what properties your solutions should have.

Obviously having if statements checking the client ID sprinkled throughout your code isn't going to scale well to lots of clients. You probably want lots of clients, so you want adding a new one to be simple. So you're going to want all the client configuration in one place, as much as possible.

Also, maybe in the future somebody will want a feature to work like client A for some users, and like client B for others. Or based on time of day. So you probably don't want the feature to be responsible for knowing which options it should apply. The code to decide that should be outside the feature, and pass the options in to the feature.

So:

  var customerOptions={
  surfaceConsistencyPreference : 'springy',
  staleDataSweepingPolicy      : 'draconian'
  }


//...in some other file
function someFeature(surfaceConsistencyPreference) {
  // common code

  if(surfaceConsistencyPreference === 'mushy') {
  // does something
  } else if(surfaceConsistencyPreference === 'springy') {
   // does something else
  } // else if ... for every other custom behaviour, or on other parts of the feature

  // common code
}

The general principal is to keep a function dependent on as little else as possible. The someFeature function has no need to be dependent on a clientID - it probably shouldn't know you have clients. It does need to know which custom behavior to use. So pass that in and it won't have to worry about it, and you won't subtly have knowledge of how custom behaviors are determined replicated all over your application in a way that makes it very difficult to modify.

How you go from a customer configuration file to passing the value to a feature is too application specific for me to have anything else to tell you.

  • While words in the answer sounds right, suggested solution only replaced client id with option and all complicated decision making remain in the feature – Fabio Mar 1 '18 at 20:22
  • @Fabio - If you mean the preferences would likely really be some kind of polymorphic dispatch rather than an if statement, that is probably true, but the question is vague enough that it's hard to say where that logic belongs. – psr Mar 2 '18 at 0:18
3

Can the specific behavior be encapsulated ?

If the behavior is a related set of actions used in several places, use the strategy pattern. Your special client would use a different concrete strategy.

Examples: launch workflows with some variants, customize logs, tailor authorization and security checks.

Are there several unrelated behaviors that can be encapsulated separately ?

If it's not one single specific behavior but a combination of several unrelated behaviors, that could each be encapsulated and be of interest to more than one client, use several strategies. Each client would then use a combination of concrete strategies.

Example: one strategy for user authentication (SSO vs password), a different strategy for workflow (two step vs. three steps), and a different output strategy (html emails, vs pdf attachments). This gives 8 possible combinations for a customer.

Are different behaviors needed at class level ?

If the specific behavior can't be encapsulated easily in strategies, but are so different that they have to be embedded in classes, then you could go for a polymorphism approach: encapsulate common behaviors in base classes, use template methods to reduce redundancy. But things will get much more complex as you'll need a factory, or even an abstract factory to create objects, you might even need bridges to decouple the application specific refinements, from the customer variations.

ATTENTION

Having a single code base can be a source of productivity and synergy. However, a high quality standard will require much more complex tests, as tests have to be performed for every possible combinations in order to ensure that there is no unexpected side effets.

2
        IList<string> list = new List<string>
        {
            { "someClient" },
            { "otherClient" }
        };

        list.Contains(clientId);

Use polymorphism and try to create classes for each implementation. That would decouple all the elements,and it would be a good practice.

To answer your question :

"would require the programmers to create maybe thousands of new interfaces/classes for each features multiplied by each client's custom behaviour, sometimes just for a simple line of custom code."

If you are writing everything in one class,you are doing 2 things:

  1. Creating a future God class.

  2. Breaking the first rule of SRP.Single responsiblity principle.

"Many small classes is better"

"Large, multipurpose classes force us to scroll through code that we don’t need to know about right now."

https://www.codingblocks.net/podcast/how-to-write-classes-the-right-way/

2

You should make this kind of decisions as early in the application as possible.
In case where user can use only one option of the feature, you should move decision to the entry point of the user's requests.

You could introduce part of the feature which should be changed as interface and inject it to the feature itself.

public class MyFeature
{
    private readonly ISomeLogic _logic;
    public MyFeature(ISomeLogic logic)
    {
        _logic = logic
    }

    public void DoWork(object someArguments)
    {
        .. do staff
        var logicResult = logic.Run();
        .. use result
    }
}

Then you will have two implementations for ISomeLogic.

public class CustomerOneSomeLogic : ISomeLogic { }

public class CustomerTwoSomeLogic : ISomeLogic { }

Then in the entry point of your application you will inject correct implementation.

In case of Web API you can do it somewhere in the middleware, where you get access to current user information.

This is only place where you decide what implementation should be used.
Because you do it on entry point of your application rest of application is free of complicated if .. else conditions.

public MyFeature CreateFeature(User userInfo)
{
    if (userInfo.useLogicOne)
    {
        return new MyFeature(new CustomerOneSomeLogic());
    }
    if (userInfo.useLogicTwo)
    {
        return new MyFeature(new CustomerTwoSomeLogic());
    }
}

If you are using Dependency Injection containers then you will add that logic to the container, which every time application ask for instance of ISomeLogic it will return correct one.

Main idea is to move decision making as close as possible to the entry point of application. Moving it up, will save other lower level elements of having conditions and make testing much simpler by removing extra execution branches.

When new customer ask for changes, you will extract piece if code, which need to be changed to the interface. Introduce two implementations: default and requested by customer. Then inject correct one.

  • this looks like an interesting way to do that. but wouldn't "CustomerOneLogic" (and so on) would be bloated by so many unrelated custom methods ? – Strannch Mar 2 '18 at 8:56
  • 1
    No, because you will have one interface for one piece of logic, for example: ICalculateTotal will have two implementations: CalculateTotalWithCustomerOneRequirements and CalculateTotalWithDefaultFormula. Then if same customer want ship products differently you will introduce IShipOrder and implementations: ShipOrderWithCustomerOneRequirements and ShipOrderWithDefault – Fabio Mar 2 '18 at 9:29
  • 1
    CustomerOneLogic doesn't mean it will contain all implementations of different logics - wasn't good name, very abstracted. – Fabio Mar 2 '18 at 9:31
2

What about polymorphism?

Entrypoint:

function someFeature() 
{
   // eg: \MyCompany\ClientResponseBehaviour\AwkwardClientBehaviour
   $clientResponseBehaviour = Client::Get(client_id_here)->getBehaviour();
   $responseBehaviour = new $clientResponseBehaviour();
   $responseBehaviour->doMagic();
}

Concrete implementations:

class AwkwardClientBehaviour implements ClientBehaviourInterface {
   public doMagic() { echo 'im the awkward client!'; }
}

class NormalClientBehaviour implements ClientBehaviourInterface {
   public doMagic() { echo 'hi there, im normal.'; }
}

Don't forget the interface:

interface ClientBehaviourInterface {
    public doMagic();
}
1

This problem is big. It's bigger than the strategy pattern. It's bigger than the bridge pattern. Is's bigger than polymorphism it self.

This is nothing less then a good reason to use a dependency injection framework.

I've rarely had anything good to say about DI frameworks because they invite you to do stupid things like clutter your classes with proprietary annotations. Most problems could be solved without them but here you have a good argument for one.

The reason why is you need to be able to let each user use the default implementations unless their custom config overrides them. That really argues for a system that can manage the choice of implementations as a group. Not just one at a time.

Done this way the knowledge of which user is being serviced is completely removed from the code. It all happens in the users configuration file.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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