Question

Considering a feature needs changes at many places in different modules of the software: UI, business logic, backend, etc.

What is a good approach to do so?

We are using dependency injection and considering to use the ApplicationBuilder to exchange the modules at one single place, BUT this would require code duplication or many different states inside the modules.

Any better idea?

Was it helpful?

Solution

To avoid changes in many places, there are some strategies:

The obvious one: Put as much of the feature specific code in one place.

The other obvious one: Choose behaviour by picking the right class, not by using a switch statement.

Less obvious: Have a list of features that is calculated once. Then your code shouldn’t do things for feature A, then B, then C, but for each feature in the feature list.

Of course if your code doesn’t work like that (yet) then a new feature gives you a reason to improve your code.

OTHER TIPS

It's hard to give a concrete answer without the actual code but from experience I often found that I could make the database and the UI work transparently for both with and without the new feature.

So I would prepare the database (add a table, column, view, procedure, ...) by ADDING functionality and still supporting the original functionality. (Don't break the current interfaces) If adding one of these duplicates existing items consider providing the current data (structure) through views and procedures (that could be dropped later on) Try to prevent the duplication of data; stick to proper database design.

The UI would get the same treatment: use grids that dynamically show the results based on the type of data that they are given (no hard coded columns), populate menu items and buttons based on the type of the data that is shown; databinding is a very powerful mechanism for this. If adding new UI elements duplicates code/UI definitions, consider introducing abstract parent classes or a more dynamic build-up of the UI through databinding.

At the business layer, create new classes that implement the new feature and are as compatible as possible with the current state. If this new implementation duplicates a lot of code consider lifting the duplicate code in an abstract parent class.

Then, in the logic that defines the implementation for the interfaces (DI Container initialization) test for the feature switch and load the new classes instead of the old ones.

I dislike leaking the switch and the testing thereof everywhere in code. I prefer the switch by abstraction: branch by abstraction by Martin Fowler

This way, removing the switch later on will be not that painful, it will have minimal impact.

I used the ideas, especially the one proposing to centralize all the feature dependendent things, which I consider very important in a very large codebase (> 1Mio LOC), to create some sample code, which shows a possible approach.

namespace Module1
{
    public class Input1
    {
       public int Param1;
       public int Param2;
    }
    //Describes an aspect in Module1 with all Dependencies
    interface IAspect1
    {
        int RunService(Input1 input);
    }

    class MyClass1
    {
        IAspect1 _aspect;
        public MyClass1(IAspect1 featureAspect)
        {
            _aspect = featureAspect;
        }

        public int UseIt(Input1 input)
        {
            int calculation = 1000; //Start calculation

            calculation += _aspect.RunService(input); //use feature

            calculation += 1000; //End calculation

            return calculation;

        }
    }
}

namespace Module2
{
    //Describes another aspect in Module2
    interface IAspect2
    {
        bool IsEnabled();
    }

    class MyClass2
    {
        IAspect2 _aspect2;
        public MyClass2(IAspect2 featureAspect)
        {
            _aspect2 = featureAspect;
        }

        public bool UseIt()
        {
            return _aspect2.IsEnabled();
        }
    }

}


class FeatureA_Enabled : Module1.IAspect1, Module2.IAspect2
{
    //Explicit implementation prevents from misuse of the Feature class.
    int Module1.IAspect1.RunService(Input1 input)
    {
        return input.Param2;
    }
    bool Module2.IAspect2.IsEnabled()
    {
        return true;
    }
}
class FeatureA_Old : Module1.IAspect1, Module2.IAspect2
{
    public int RunService(Input1 input)
    {
        return 0; //Does nothing
    }

    bool Module2.IAspect2.IsEnabled()
    {
        return false;
    }
}

public class FeatureToggleTests
{

    [TestCase(0, 2999)]
    [TestCase(1, 2000)]
    public void FeatureStrategy_WithAspect1(int configuration, int expected)
    {
        object[] features = { new FeatureA_Enabled(), new FeatureA_Old() };
        var feature = features[configuration]; //A central feature router determines, which implementation to use


        Assert.IsNotNull(feature as Module1.IAspect1);

        //MyClass1 has introduced the feature aspect and gets it injected, depending on configuration
        //The class does not know anything about feature routers or other aspects, like IAspect2.
        var sut = new Module1.MyClass1(feature as Module1.IAspect1); 


        int actual = sut.UseIt(new Input1() { Param2 = 999 }); //Usage somewhere in Module1

        Assert.AreEqual(expected, actual);
    }

    [TestCase(0, true)]
    [TestCase(1, false)]
    public void FeatureStrategy_WithAspect2(int configuration, bool enabled)
    {
        object[] features = { new FeatureA_Enabled(), new FeatureA_Old() };
        var feature = features[configuration]; 

        Assert.IsNotNull(feature as Module2.IAspect2);

        var sut = new Module2.MyClass2(feature as Module2.IAspect2);


        bool actual = sut.UseIt(); 

        Assert.AreEqual(enabled, actual);
    }

}

The feature class collects all aspects of the feature at one place Dependencies to other modules are allowed at this place. All toggles could be placed in a "Tooggles" module Removing a toggle should be straightforward, just get rid of the aspects and wrap them into the classes

Licensed under: CC-BY-SA with attribution
scroll top