10
\$\begingroup\$

I've written a library that uses generics and funcs to implement a feature toggle system without the need to use if statements.

This was mostly to hone my skills, as I'm still new to this profession. I'd really appreciate any comments on how to improve the code.

It can be found in this following git repo

An extract of the main class is below Feature Toggle Class

namespace FeatureToggle.Classes
{
    using System;
    using Enums;
    using Interfaces;
    public class FeatureToggle <T> : IFeatureToggle <T>
    {
        private ToggleStatus Status(bool active)
        {
            return active ? 
            ToggleStatus.Active : 
            ToggleStatus.Inactive;
        }

        public ToggleStatus GetToggleState(IConfigParser parser, string toggleKey)
        {
            return Status(parser.GetToggleStatus(toggleKey));
        }


        public void ExecuteMethodIfToggleOn(Action methodToRun, IConfigParser configParser, string keyName)
        {
            var response = GetToggleState(configParser, keyName);
            if (response == ToggleStatus.Active)
            {
                methodToRun();
            }
        }

        public void ExecuteMethodIfToggleOn(Action methodToRun, string keyName)
        {
            IConfigParser configParser = new ConfigParser();
            ExecuteMethodIfToggleOn(methodToRun, configParser, keyName);
        }

        public T ExecuteMethodIfToggleOn(Func<T> methodToRun, string keyName)
        {
            IConfigParser configParser = new ConfigParser();
            return ExecuteMethodIfToggleOn(methodToRun, configParser, keyName);
        }

        public T ExecuteMethodIfToggleOn(Func<T> methodToRun, IConfigParser configParser,  string keyName)
        {
            var response = GetToggleState(configParser, keyName);
            if (response == ToggleStatus.Active)
            {
                return methodToRun();
            }
            return default(T);
        }
    }
}

Feature Toggle Interface

namespace FeatureToggle.Interfaces
{
    using System;
    using Enums;

    public interface IFeatureToggle <T>
    {
        ToggleStatus GetToggleState(IConfigParser parser, string toggleKey);
        void ExecuteMethodIfToggleOn(Action methodToRun, string keyName);
        T ExecuteMethodIfToggleOn(Func<T> methodToRun, string keyName);
    }
}

Config Parser Class

namespace FeatureToggle.Classes
{
    using System;
    using System.Collections.Specialized;
    using System.Configuration;
    using Interfaces;

    public class ConfigParser : IConfigParser
    {
        private readonly NameValueCollection _toggles;
        public ConfigParser()
        {
            if (ToggleConfigTagExists())
            {
                _toggles = ConfigurationManager.GetSection("Toggles") as NameValueCollection;
            }
        }
        public bool ToggleConfigTagExists()
        {
            var toggleSection = ConfigurationManager.GetSection("Toggles");
            return toggleSection != null;
        }

        public bool GetToggleStatus(string toggle)
        {
            return ParseBoolValueFromConfig(_toggles.GetValues(toggle)?[0]);
        }

        public bool ParseBoolValueFromConfig(string status)
        {
            if (status == "1" || status.ToLower() == "true")
            {
                return true;
            }
            if (status == "0" || status.ToLower() == "false")
            {
                return false;
            }
            else
            {
                throw new ArgumentOutOfRangeException();
            }
        }
    }
}

Config Parser Interface

namespace FeatureToggle.Interfaces
{
    public interface IConfigParser
    {
        bool GetToggleStatus(string toggle);

        bool ParseBoolValueFromConfig(string status);
    }
}

Integration tests

namespace FeatureToggleTests.Integration
{
    using System;
    using FeatureToggle.Classes;
    using FeatureToggle.Enums;
    using FeatureToggle.Interfaces;
    using NUnit.Framework;

    [TestFixture]
    public class FeatureToggleIntegrationTests
    {
        [Test]
        public void TestToggleStatusActiveIsReturnedWhenParsingAnItemThatIsToggledOn()
        {
            IConfigParser configParser = new ConfigParser();
            IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();

            var toggleStatus = featureToggle.GetToggleState(configParser, "ButtonToggle");
            Assert.AreEqual(ToggleStatus.Active, toggleStatus);
        }

        [Test]
        public void TestToggleStatusInactiveIsReturnedWhenParsingAnItemThatIsToggledOff()
        {
            IConfigParser configParser = new ConfigParser();
            IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();

            var toggleStatus = featureToggle.GetToggleState(configParser, "NotFinished");
            Assert.AreEqual(ToggleStatus.Inactive, toggleStatus);
        }

        [Test]
        public void TestOutOfRangeExceptionIsReturnedWhenParsingAnItemThatIsToggledAsdf()
        {
            var configParser = new ConfigParser();
            IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();
            Assert.Throws<ArgumentOutOfRangeException>(() => featureToggle.GetToggleState(configParser, "asdf"));
        }

        [Test]
        public void TestNullReferenceExceptionIsReturnedWhenParsingAnItemThatDoesNotExist()
        {
            var configParser = new ConfigParser();
            IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();
            Assert.Throws<NullReferenceException>(() => featureToggle.GetToggleState(configParser, "wewewewewewewewe"));
        }

        [Test]
        public void TestFakeMethodWillNotChangeValueIfConfigItemIsToggledToFalse()
        {
            var changeMe = "Unchanged";

            FakeMethod("FakeFalse", out changeMe);

            Assert.AreEqual("Unchanged", changeMe);
        }

        [Test]
        public void TestFakeMethodWillChangeValueIfConfigItemIsToggledToTrue()
        {
            var changeMe = "Unchanged";

            FakeMethod("FakeTrue", out changeMe);

            Assert.AreEqual("has been changed", changeMe);
        }

        [Test]
        public void TestActionFakeMethodThatReturnsTrueWillReturnTrueIfConfigItemIsToggledToTrue()
        {
            IFeatureToggle<bool> featureToggler = new FeatureToggle<bool>();

            var result = featureToggler.ExecuteMethodIfToggleOn(FakeMethodThatReturnsTrue, "FakeTrue");

            Assert.IsTrue(result);
        }

        [Test]
        public void TestActionFakeMethodThatReturnsTrueWillReturnFalseIfConfigItemIsToggledToFalse()
        {
            IFeatureToggle<bool> featureToggler = new FeatureToggle<bool>();

            var result = featureToggler.ExecuteMethodIfToggleOn(FakeMethodThatReturnsTrue, "FakeFalse");

            Assert.IsFalse(result);
        }

        protected void FakeMethod(string keyName, out string changeMe)
        {
                IConfigParser configParser = new ConfigParser();
                IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();

                var response = featureToggle.GetToggleState(configParser, keyName);
                if (response == ToggleStatus.Active)
                {
                    changeMe = "has been changed";
                    return;
                }
                changeMe = "Unchanged";
        }
        protected bool FakeMethodThatReturnsTrue()
        {
            return true;
        }
    }
}

Unit Tests

namespace FeatureToggleTests.Unit
{
    using System;
    using FeatureToggle.Classes;
    using FeatureToggle.Enums;
    using FeatureToggle.Interfaces;
    using NUnit.Framework;

    [TestFixture]
    public class FeatureToggleTests
    {
        [Test]
        public void TestSuccessfullParseReturnsToggleStatusActive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<bool>();
            var toggleResponse = toggle.GetToggleState(testParser, "positive");
            Assert.AreEqual(ToggleStatus.Active, toggleResponse);
        }
        [Test]
        public void TestUnSuccessfullParseReturnsToggleStatusInactive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<bool>();
            var toggleResponse = toggle.GetToggleState(testParser, "anythingElse");
            Assert.AreEqual(ToggleStatus.Inactive, toggleResponse);
        }

        [Test]
        public void TestSuccessfullFuncCallWhenToggleStatusActive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<bool>();
            Func<bool> theAction = AlwaysReturnTrue;

            var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "positive");

            Assert.AreEqual(true, toggleResponse);
        }
        [Test]
        public void TestUnSuccessfullFuncCallWhenToggleStatusInactive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<bool>();
            Func<bool> theAction = AlwaysReturnTrue;

            var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "anythingElse");
            Assert.AreEqual(false, toggleResponse);
        }

        [Test]
        public void TestSuccessfullFuncStringCallWhenToggleStatusActive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<string>();
            Func<string> theAction = AlwaysReturnFire;

            var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "positive");

            Assert.AreEqual("Fire", toggleResponse);
        }
        [Test]
        public void TestUnSuccessfullFuncStringCallWhenToggleStatusInactive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<string>();
            Func<string> theAction = AlwaysReturnFire;

            var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "anythingElse");
            Assert.AreNotEqual("Fire", toggleResponse);
        }

        [Test]
        public void TestSuccessfullFuncTestDataTypeCallWhenToggleStatusActive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<TestDataType>();
            Func<TestDataType> theAction = AlwaysReturnNewTestDataType;

            var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "positive");

            Assert.AreEqual(new TestDataType().HappynessIs, toggleResponse.HappynessIs);
        }
        [Test]
        public void TestUnSuccessfullFuncTestDataTypeCallWhenToggleStatusInactive()
        {
            IConfigParser testParser = new ConfigParserTestDouble();
            var toggle = new FeatureToggle<TestDataType>();
            Func<TestDataType> theAction = AlwaysReturnNewTestDataType;

            var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "anythingElse");
            Assert.IsNull(toggleResponse);
        }

        protected bool AlwaysReturnTrue()
        {
            return true;
        }

        protected string AlwaysReturnFire()
        {
            return "Fire";
        }

        protected TestDataType AlwaysReturnNewTestDataType()
        {
            return new TestDataType();
        }
    }

    public class TestDataType
    {
        public string HappynessIs = "Happy";
    }

    internal class ConfigParserTestDouble : IConfigParser
    {
        public bool ToggleConfigTagExists()
        {
            throw new System.NotImplementedException();
        }

        public bool GetToggleStatus(string toggle)
        {
            return toggle == "positive";
        }

        public bool ParseBoolValueFromConfig(string status)
        {
            throw new System.NotImplementedException();
        }
    }
}

Enum

namespace FeatureToggle.Enums
{
    public enum ToggleStatus
    {
        Active,
        Inactive
    }
}

Explanation of classes:

FeatureToggle class consumes generic types and a configParser is injected in. When you want to toggle a method in your main library you call FeatureToggle.ExecuteMethodIfToggleOn(). Since it uses generic types it will output your return type/do stuff in method if the "toggle" status is on.

ConfigParser class parses out a true or false value from app.config. though it is implementing IConfigParser so I can extend the behaviour to read from a database or text file or whatever else I might need to read toggle values from.

The enum was there to make an explicit indication if something is toggled on or off, though I think this is over kill and a bool would be fine.

The integration and unit tests are there to prove this works and will pickup any bugs I introduce in any further changes. These tests just implement some quick test doubles with simple behaviour to ensure I can assert against an expected value.

\$\endgroup\$
9
  • \$\begingroup\$ I think it would be better if you posted the ConfigParser too because this code seems to heavily rely on it \$\endgroup\$
    – IEatBagels
    May 9, 2018 at 17:39
  • 1
    \$\begingroup\$ @TopinFrassi added :) \$\endgroup\$
    – ScottBot
    May 9, 2018 at 18:17
  • \$\begingroup\$ For me it's hard to follow the idea of this code. What are you "toggling"? If there's a section in your ConfiguarionManager, that contains all the strings that correlate to some boolean value, why don't you just retrieve the value and determine whether to execute the method or not just based on that? I'm hesitant to write and an answer as I'm feeling there's something I'm missing. \$\endgroup\$
    – Denis
    May 9, 2018 at 19:42
  • \$\begingroup\$ Well It's to "toggle" pieces of code on or off. The configuration manager stuff is just one implementation of IConfigParser. The idea of it is you inject your implementation of IConfigParser in to the feature toggle. This then allows you to execute a method or not. So instead of wrapping your toggled code in if statements adding further levels of indentation you call your method as a parameter \$\endgroup\$
    – ScottBot
    May 9, 2018 at 20:04
  • \$\begingroup\$ Another approach would be the 'plugin' paradigm. \$\endgroup\$
    – aybe
    May 11, 2018 at 17:48

1 Answer 1

3
\$\begingroup\$

Here, I am posting an alternative approach you could try.

Instead of this you could make a plugin system,

Pros

  • Your application does not have potentially toggled code
  • Concerns are separated, your app does the plumbing, features are delegated
  • Plugins work against a well-thought/defined public interface
  • Testing might be easier in the end (rough guess)
  • (whatever else I forgot)

Cons

  • A bit of plumbing is involved at the beginning
  • More types in the end
  • (whatever else I forgot)

What a plugin implements:

public interface IPlugin
{
    void Do(IWorkspace workspace);
}

The workspace it performs operations on (here it's very simple):

public interface IWorkspace
{
    void ShowMessage(string message);
}

Two examples of plugins:

public sealed class Plugin1 : IPlugin
{
    public void Do(IWorkspace workspace)
    {
        workspace.ShowMessage("Plugin1 here !");
    }
}

public sealed class Plugin2 : IPlugin
{
    public void Do(IWorkspace workspace)
    {
        workspace.ShowMessage("Plugin2 here !");
    }
}

An example of the workspace (should be in your app):

using System.Windows;

internal sealed class Workspace : IWorkspace
{
    public void ShowMessage(string message)
    {
        MessageBox.Show(message);
    }
}

Here the mini-app that shows these plugins (WPF):

XAML

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Menu x:Name="Menu" />
    </Grid>
</Window>

Code behind:

using System;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;

namespace WpfApp1
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            // get the plugins in assembly

            var plugins = Assembly
                    .GetExecutingAssembly()
                    .GetTypes()
                    .Where(s => typeof(IPlugin).IsAssignableFrom(s))
                    .Where(s => s.IsClass)
                    .Where(s => s.GetConstructor(Type.EmptyTypes) != null)
                    .Select(s => (IPlugin) Activator.CreateInstance(s))
                    .ToArray()
                ;

            // populate plugins in menu

            var root = new MenuItem {Header = "Plugins"};

            foreach (var plugin in plugins)
            {
                var item = new MenuItem {Header = plugin.GetType().Name};
                item.Click += (sender, args) => { plugin.Do(Workspace); };
                root.Items.Add(item);
            }

            Menu.Items.Add(root);
        }

        private IWorkspace Workspace { get; } = new Workspace();
    }
}

enter image description here

enter image description here

IMHO the pros definitely outweighs the cons, while the beginning is a little difficult, in the end things are well separated and your app is less likely to become a huge mess.

One of the strong arguments is that your plugins work against a public interface and don't see/deal with whatever (private) gory details of your app.

What you should do next:

  • your workspace/plugin system ends in its own assembly, it will be referenced by your app, plugins and unit tests
  • code your plugins into separate assemblies, organize them as you see fit
  • make a system that dynamically dis/en-ables plugins and update the menu, much like some extensions in Visual Studio

This is merely an example to push you to think out of the box, change it to your needs but IMO it address a very important aspect: no more spaghetti logic in your app.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks aybe, I've done some digging in to the plugin paradigm and I feel that it would possibly take more effort to achieve my desired outcome using it. I am interested in it however and will look to use it at work. My issue at the moment is making the code self descriptive, I find that the most challenging, and would probably struggle to achieve that with plugin \$\endgroup\$
    – ScottBot
    May 12, 2018 at 19:13
  • \$\begingroup\$ Good luck mate ! \$\endgroup\$
    – aybe
    May 14, 2018 at 6:31

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.