Creating a feature toggle library, part 2
In Part 1 I introduced the idea of a feature toggle library, and I wrote that the starting point for Clarion development should not be the data dictionary. In many cases the data dictionary comes at the very end of the process.
Yes, that flies in the face of almost everything we've learned about Clarion. But that's only because we've been so fixated on what Clarion does best: browse and form business apps.
Think about where you spend most of your time. Is creating browses and forms? No, because Clarion does that so well. You spend your time writing code that models various kinds of business processes that don't fit neatly into the browse/form paradigm.Â
The right fit for TDD
If the data dictionary doesn't come first, what does? There are a lot of different answers to that question, depending on what kind of application you're developing, the stage of development, and the scope of the requirement. Unless you value trial and error highly and enjoy throwing away most of your code, you're going to want to do some design up front.Â
For small-sized (and sometimes medium-sized) business modeling tasks I regularly turn to Test Driven Development, or TDD. As the name suggests, this kind of development begins with writing tests. Of course a main benefit of TDD is that you end up with tests at the end of the process, and you can use those to continually validate the behavior of your code as you go on to maintain it. But I've found TDD equally useful as a design aid.Â
When I begin with a test I'm forced think about how I want to use my code. This short-circuits my natural tendency to jump to the implementation, and writing the implementation at the start is an investment, and the more I invest in some code the less likely I am to throw it away and start over, even if it turns out my initial implementation reflects some poor design choices.
TDD lets me think about my code at a high level of abstraction.For instance, here's a test that sketches out how I might detect whether a feature is enabled or disabled (with the test setup code omitted):
DisableEnableFeatureVerifyAccess PROCEDURE (*long addr) ! Declare Procedure Toggle CML_FeatureToggle_Toggle Feature1 equate('Feature1') Feature2 equate('Feature2') CODE Toggle.AddFeature(Feature1) Toggle.AddFeature(Feature2) Toggle.EnableFeature(Feature1) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(true),Feature1 & ' should be enabled') AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(false),Feature2 & ' should be disabled') Toggle.DisableFeature(Feature1) Toggle.EnableFeature(Feature2) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(true),Feature1 & ' should be disabled') AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(false),Feature2 & ' should be enabled')
It's really important to understand that at this point the code doesn't even compile. I don't have a class called CML_FeatureToggle_Toggle. Once I'm happy with how my test code looks, the next step will be to stub out the class and methods so the code compiles. Of course at that point the test will fail; the final step is to write code so the test passes. But I'm getting ahead of myself.
You'll note that there's no data storage indicated in the test code. I'm adding some data to my Toggle object in code; in a real application I'll be pulling this information from some kind of data store. But I don't care about a data store at this point - that's an implementation detail. All I really want to focus on is the cleanest, most high-level way to use the logic.Â
Already I'm making some design decisions. I've decided that I'm going to identify each feature by a unique string. I've also decided that features will be disabled by default, which brings up another question: Do I really want all features disabled by default?Â
It's more likely that as time goes by, features that were once implemented for a few (e.g. beta testers) will be implemented for all. So I probably want to have the option of enabling individual features by default.Â
AddFeatureEnabledByDefaultVerifyEnabled PROCEDURE (*long addr) ! Declare Procedure Toggle CML_FeatureToggle_Toggle Feature1 equate('Feature 1') Feature2 equate('Feature 2') CODE Toggle.AddFeature(Feature1) Toggle.AddEnabledFeature(Feature2) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(true),Feature1 & ' should be disabled') AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(false),Feature2 & ' should be enabled')
I just mentioned implementing features for users, so I'd better have some test code as well:
AssignFeatureToUser PROCEDURE (*long addr) ! Declare Procedure Toggle CML_FeatureToggle_Toggle Feature1 equate('Feature1') Feature2 equate('Feature2') User1 equate('User1') User2 equate('User2') CODE Toggle.AddFeature(Feature1) Toggle.AddFeature(Feature2) Toggle.AddUser(User1) Toggle.AddUser(User2) Toggle.EnableFeatureForUser(Feature1,User2) Toggle.EnableFeatureForUser(Feature2,User1) Toggle.SetActiveUser(User1) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(false),Feature1 & ' should be disabled for ' & User1) AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(true),Feature2 & ' should be enabled for ' & User1) Toggle.SetActiveUser(User2) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(true),Feature1 & ' should be enabled for ' & User2) AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(false),Feature2 & ' should be disabled for ' & User2)
In systems with only a handful of users, having user level security is probably enough. But as the number of users grows, assigning privileges to individual users quickly becomes cumbersome, which is why security schemes typically implement the concept of a group. You assign permissions to groups, so that when you associate a user with a group they have the rights assigned to the group. Here's my initial test code for groups:
Toggle CML_FeatureToggle_Toggle Feature1 equate('Feature1') Feature2 equate('Feature2') User1 equate('User1') User2 equate('User2') Group1 equate('Group1') Group2 equate('Group2') Group3 equate('Group3') CODE Toggle.AddFeature(Feature1) Toggle.AddFeature(Feature2) Toggle.AddUserToGroup(User1,Group1) Toggle.AddUserToGroup(User1,Group2) Toggle.AddUserToGroup(User2,Group2) Toggle.AddUserToGroup(User2,Group3) Toggle.EnableFeatureForGroup(Feature2,Group2) Toggle.SetActiveUser(User1) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(false),Feature1 & ' should be disabled for ' & User1) AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(true),Feature2 & ' should be enabled for ' & User1) Toggle.SetActiveUser(User2) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(false),Feature1 & ' should be disabled for ' & User2) AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(true),Feature2 & ' should be enabled for ' & User2) Toggle.DisableFeatureForGroup(Feature2,Group2) Toggle.EnableFeatureForGroup(Feature1,Group3) Toggle.EnableFeatureForGroup(Feature2,Group3) Toggle.SetActiveUser(User1) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(false),Feature1 & ' should be disabled for ' & User1) AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(false),Feature2 & ' should be disabled for ' & User1) Toggle.SetActiveUser(User2) AssertThat(Toggle.FeatureIsEnabled(Feature1),IsEqualTo(true),Feature1 & ' should be enabled for ' & User2) AssertThat(Toggle.FeatureIsEnabled(Feature2),IsEqualTo(true),Feature2 & ' should be enabled for ' & User2)
This group thing has me wondering, though. What happens if I have one group that denies permission to a feature, and another group that grants permission to a feature, and the user is a member of both groups? Which has precedence: denial or permission? I think I already have enough on my plate to get started coding, but I don't want to lose sight of this task, so I'm going to stub out another test and have it fail because it's incomplete:
ResolveConflictingPermissions PROCEDURE (*long addr) ! Declare Procedure CODE ! What happens when a user is a member of two groups, where one ! grants access to a feature and the other denies permission? SetUnitTestFailed('This test needs to be written')
All I have now is a bunch of test code that won't even compile yet. The next step: write a class with stub methods so the code can at least compile.Â
You can download the app below. You'll also need the ClarionTest template chain which is included in the ClarionMag Library on GitHub.
Â