4/02/2009

PropertyFor Sale

Properties. And, Java. Lots of languages really, but let's look at Java. Every Java (and other langs too) application of any size is going to be using some properties. Typically, they'll be held in a "myspecializedvalues.properties" file, but not necessarily. For this example, let's assume you are using such a file (and can then thus use Java's built-in property management mechanism), but again, its not really the point. And this approach can be applied no matter what format you choose to store your properties.

The point of this is to show you a better way to manage - a better way to encapsulate and access - your properties so that the implementation is hidden behind an expressive interface, easy for anyone to understand and use.

The point is to show the PropertyFor class, and to tell you why its better than what you probably have in place.

So, this example has a strategy factory class that needs to make its decisions based on a property setting, this property having a true/false (boolean) value. It starts off looking like this:
public class GeneratorFactory {
public String propFileName =
"ELearningSettings.properties";

public GenerationStrategy projectGenerationStrategy() {
if (shouldIgnoreCode())
return new GenerateWithoutCode();
else
return new GenerateWithCode();
}

private boolean shouldIgnoreCode() {
Object valueObj = getProperty("CODE_IS_USELESS");
if (valueObj != null)
return Boolean.parseBoolean((String)valueObj);
return false;
}

private Object getProperty(String key) {
Properties properties = new Properties();
try {
properties.load(new FileInputStream(propFileName));
} catch (IOException e) {
throw new RuntimeException(e);
}

return properties.get(key);
}
}

You'll hopefully notice something about this factory. First, it's primary responsibility is to serve up the correct strategy to its caller. Of course though, you'll also notice that not only does this class do its primary purpose, but it also has a lot of low-level knowledge about how to obtain the information it needs to make its decision. It knows all sorts of things about icky java property mania.

Now, if this is the lone property in your system, than maybe this is okay and doing any kind of extraction here might be YAGNI (you might though, even if that is the case, be bothered by that fact that this class clearly violates the SRP, nonetheless...).

Of course though, this is not the case, and so let's assume that although this is the only class you see in this example there are in fact more properties in your system and more users of them.

So, the logical thing to do is to extract that property reading management into its own class, happy to have its sole responsibility being providing property values. You do so, and end up with this new "Settings" class:
public class ELearningSettings {
public String propFileName =
"ELearningSettings.properties";

public Object getProperty(String key) {
Properties properties = new Properties();
try {
properties.load(new FileInputStream(propFileName));
} catch (IOException e) {
throw new RuntimeException(e);
}

return properties.get(key);
}
}

public class GeneratorFactory {
public GenerationStrategy projectGenerationStrategy() {
if (shouldIgnoreCode())
return new GenerateWithoutCode();
else
return new GenerateWithCode();
}

private boolean shouldIgnoreCode() {
Object valueObj =
ELearningSettings.getProperty("CODE_IS_USELESS");
if (valueObj != null)
return Boolean.parseBoolean((String)valueObj);
return false;
}
}
You slap five with your pair and check in, this is much better! Now your factory can do it's job without having any internal worry about where the settings (properties) are really coming from or how they're implemented. Any other users of properties also get the same luxury. A big improvement, no doubt.

Many systems I've seen have figured this much out, and have this in place. A "Settings" class that hides the details of how to look up properties, all you have to give it is the key to the property you want.

But, this has limitations.

First, and as many will figure out and solve on their own, a sprawl of the property key strings hard-coded in the various users of the properties. You want to change this key? Have fun chasing all the uses down. Of course you could create a constants file for these keys, and have the callers only know the constants (which, in Java at least, provides the big benefit of being able to leverage you IDE when you want to change its name), but is that really enough?

Second, yes your factory doesn't know about "ELearningSettings" or the "java...Properties" guts, but it does still have the job of knowing the format of these property values ('Object' in this case) and how to turn that into what it really wants, a boolean.

Third, particularly if haven't created a constants file (in response to the first problem), how easy is it for future developers to go to one place and see all the possible properties/settings the system has available for them?

There may be more limitations, but that's enough for me.

So, our answer to each of these questions is where the magic happens, the MBark of this article. We evolve our generic "Settings" class into a domain-friendly "PropertyFor" class:
public class PropertyFor {
public static String propFileName =
"ELearningSettings.properties";

public static boolean shouldIgnoreCode() {
Object valueObj = getProperty("CODE_IS_USELESS");
if (valueObj != null)
return Boolean.parseBoolean((String)valueObj);
return false;
}

private static Object getProperty(String key) {
Properties properties = new Properties();
try {
properties.load(new FileInputStream(propFileName));
} catch (IOException e) {
throw new RuntimeException(e);
}

return properties.get(key);
}
}

public class GeneratorFactory {
public GenerationStrategy projectGenerationStrategy() {
if (PropertyFor.shouldIgnoreCode())
return new GenerateWithoutCode();
else
return new GenerateWithCode();
}
}
What did we do here? Well, we've taken all the knowledge highlighted above as our limitations and removed it from our factory (really, from all of the users of settings), and put it into our "Settings" class - which, as a result, now becomes a more useful, expressive PropertyFor utility.

So, you'll see our factory now has to worry just about choosing a strategy, and no longer has any overhead related to obtaining the values it needs to make its decision. More specifically, it doesn't know where the settings come from, doesn't know the details of how to access them or the keys that identify them, and it doesn't know what raw format they come in. The factory knows it needs the boolean value of whether or not to "ignore code", and it gets that in one clear, simple line of code.

Further, you've got the beauty of expressive methods on singular class to be very domain-centric in assisting other programmers to access the available setttings/properties for your system - these methods, not only being clearly and expressively named, but also being typed appropriately for ultimate ease to the consumer. And of course, when you need to change the name of the setting and/or the name the callers know it by, no problemo.

So, that's really the main meat.

For good measure though, we take a moment to improve the design of our PropertyFor class. First, this isn't the only boolean setting we have (use your imagination) so we should have a nice clear reusable method to handle our boolean typed settings:
public class PropertyFor {
public static String propFileName =
"ELearningSettings.properties";

public static boolean shouldIgnoreCode() {
return boolanValueFor("CODE_IS_USELESS");
}

private static boolean boolanValueFor(String key){
Object valueObj = valueFor(key);
if (valueObj != null)
return Boolean.parseBoolean((String)valueObj);
return false;
}

private static Object valueFor(String key) {
Properties properties = new Properties();
try {
properties.load(new FileInputStream(propFileName));
} catch (IOException e) {
throw new RuntimeException(e);
}

return properties.get(key);
}
}


And then, it really is pretty inefficient of us to read the properties file over and over, so let's take care of doing that just once and being done with it:
public class PropertyFor {
public static String propFileName =
"ELearningSettings.properties";

private static Properties properties;
static {
loadProperties();
}

private static void loadProperties() {
properties = new Properties();
try {
properties.load(new FileInputStream(propFileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static boolean shouldIgnoreCode() {
return boolanValueFor("CODE_IS_USELESS");
}

private static boolean boolanValueFor(String key){
Object valueObj = properties.get(key);
if (valueObj != null)
return Boolean.parseBoolean((String)valueObj);
return false;
}
}
So there you have it: PropertyFor.

And, of course, this isn't a post about TDD, but here is the test that was in place and passing before and after any of these refactorings went down:
public class GenerationStrategyFactoryTest {
@Test
public void needingACodelessStrategy() {
propertiesFileIs("CodelessGenerationStrategy");
assertTrue(strategy() instanceof GenerateWithoutCode);
}

@Test
public void needingACodeStrategy() {
propertiesFileIs("IncludeCodeGenerationStrategy");
assertTrue(strategy() instanceof GenerateWithCode);
}

private void propertiesFileIs(String fileNameBase) {
String filePath = "testdata/"
+ fileNameBase + ".properties";
PropertyFor.propFileName = filePath;
}

private GenerationStrategy strategy() {
return new GeneratorFactory().
projectGenerationStrategy();
}
}
As an exercise, go ahead and write the test you think I have for the PropertyFor class, because of course you know I do!


ps// I'm really not liking this code highlighter library so far. Any other Bloggers have good suggestions for a good library?!?

4 comments:

  1. Why not use the XML marshaling/unmarshaling available via java.beans.XMLEncoder?

    Of course there are limitations (e.g. the schema is now coupled to the bean api) but at least you don't have to deal with lower-level code like properties.get(...) and Boolean.parseBoolean(...)

    Just a thought.
    --johnt

    ReplyDelete
  2. Hi there, good post, what you say makes sense, but here is one way of looking at it...

    Your factory violated SRP , so you introduced a collaborator (Settings) and got the factory to delegate the unwanted responsibility to the collaborator. As a result, your factory now conforms to SRP.

    This was an excellent tradeoff. The factory has gained in simplicity: it has shed a responsibility. The only cost was a small increase in the complexity (and therefore decrease in the readability) of the factory's projectGenerationStrategy() method: while before it called a method in the factory class (shouldIgnoreCode()), now it calls a method (with the same name) in the collaborator. The projectGenerationStrategy() method is harder to understand because while before it told us WHAT information it needed to know to make a decision, now in addition it also gives us implementation details: it tells us HOW it finds out that information (i.e. by asking a collaborator for the information):


    BEFORE: shouldIgnoreCode();
    AFTER: Collaborator.shouldIgnoreCode();

    BEFORE: should I ignore code?
    AFTER: Collaborator: should I ignore code?

    BEFORE: Question?
    AFTER: Collaborator:Question?

    BEFORE: WHAT?(The question)
    AFTER: WHAT?(The question) + HOW?(the question is answered by asking a collaborator)

    But even that small cost was too much for you, so you didn't stop there: you gave the settings class an 'expressive interface'.
    You made its interface fluent, i.e. to paraphrase Fowler in fluent interface , you gave usages of the interface a 'language-like flow'.

    You did this by giving the collaborator a name that turns a question to a collaborator, into a request for something containing the answer to the question:


    BEFORE: Collaborator.shouldIgnoreCode();
    AFTER: PropertyFor.shouldIgnoreCode();

    BEFORE: Collaborator: should I ignore code?
    AFTER: Property for 'should I ignore code?'

    BEFORE: Collaborator:Question?
    AFTER: Property For(Question)

    But while there is something very satisfying about what you have done, in a way you have just swapped one kind of implementation detail (HOW? information) for another:

    BEFORE: WHAT?(The question) + HOW? (the question is answered by asking a collaborator)
    AFTER: WHAT?(The question) + HOW? (the answer to the question is represented as a property)

    But there is a way (in Java anyway) to get the factory's projectGenerationStrategy() method back to its original simplicity, we can push the implementation detail (the name of the collaborator) out of the method, and into a static import:

    import static PropertyFor.*;

    public class GeneratorFactory
    {
    public GenerationStrategy projectGenerationStrategy()
    {
    if (shouldIgnoreCode())
    return new GenerateWithoutCode();
    else
    return new GenerateWithCode();
    }
    }

    ReplyDelete
  3. Hi Philip --

    First, your walkthrough of the flow I took the reader (and myself!) through is a fantastic addition to the write-up - you're dead on. Thanks for that.

    As for the static import. I'm torn about this one. I definitely agree it adds to the simplicity of the projectGenerationStrategy method (we now have no need to know the 'How'). That said, sometimes this purist increase in readability can obfuscate what's really going on. In other words - is it too fancy, will it cost future readers extra time to track down where that answer comes from? In this example probably not (too simple), but maybe for other less straightforward cases. Can't really say myself if it is or isn't, but it's something to consider.

    I'll keep that question in mind.

    Anyhoo, thanks again!
    MB

    ReplyDelete
  4. Hi JT --

    Interesting thought. I'll give that a try.

    That said, even before I do, my gut tells me that I'd still benefit from the 'PropertyFor' class, no matter what mechanism we end up using to read the properties. In fact, that's part of the benefit of the class - we can swap out new implementations (hopefully) without affecting "property users" like GenStrategyFactory.

    So, as for XMLEncoder - wanna pair on it? ;-)

    Cheers
    MB

    ReplyDelete

Lemme know what ya think.

 
/* */