Refactoring, Declarative Coding
Tradition holds that a picture is worth a thousand words. A config file may well be worth a thousand lines of code (or easily more). Reconfigurable code can simplify maintenance, portability, reuse, and even development. When many novice programmers think of objects, they think of objects exhibiting behavior determined at compile time: hard coded, internal logic. To a large sense this is accurate, programs only do what they are written to do. Computer programs operate on functions, they take input and generate output. In a sense objects are little different since each is primarily a collection of functions, often with some internal state. The behavior of an object is based on the interplay between the functions (called methods) the input to those functions, and the object's state (called properties).
An opportunity that is often underutilized is the ability to determine behavior at runtime. Object methods, including the methods that create and destroy the objects accept input. Often the input is limited to a small set of variables, making use of the object easier. For some objects this is highly appropriate. In other cases there is a missed opportunity. Take for example an object that has a list of items. In the first version of a program, the object only needs to sort the items in one way, so the sorting function might logically be a method of the object.
In this scenario the sorting behavior is predetermined (at compile time by the programmer) and cannot be changed at runtime (while the program is running). This approach is effective in building the first version of the program because it is simple to implement. Suppose in the second version a couple of other sorting options are needed.
From this point the development can go several different ways. One logical approach might be to create separate sorting methods within the same object. If one instance of the object might have need for any of the sorting methods they might simply be public methods (methods other objects can use to retrive or trigger the sorting). If each object only needs one sorting method each, it might be chosen by setting some internal state when constructing the object.
Another logical, and perhaps cleaner, approach in the case where each object only needs one of the sorting methods is to create a separate class for each sorting method. They would share a common class ancestry (they would be polymorphic) and each would only differ in the declaration of the sorting method.
This method does prove useful, as long as the object containing the sorting method is the only object that needs to use that sorting method. If in the third version a new type of object with another list is introduced, and that object needs some of the same sorting methods as the original object then the options above yield inefficient implementations.
The sorting method is procedural code, it describes how to do something. Creating two copies of this code is problematic, if the procedures ever fall out of synch in the process of maintenance or adding new features then problems will arise. The sorting method can be defined as an object in and of itself, it may even be a stateless object, or a class that is not intended to be instantiated into an object at all. Purely stateless objects, typically those that fall into the category of Utility patterns, help create reusable procedural code. When the reusable procedures need some internal state they are often categorized as Strategy patterns.
Designing the sorting as a separate object using the Utility or Strategy patterns allows it to be shared among multiple types of objects with sorting needs. This approach is especially effective when care is taken to make each different sorting object polymorphic (they all share the same interface). Now each object that has different sorting needs can be assigned a sorting object partner during or after creation instead of requiring a separate class for each sorting method. This approach focuses more on runtime control of the sorting behavior for the object needing to sort. The object is more "configurable", and thus it's capabilities at runtime are more flexible.
The process of assigning behavior at runtime relies on "declarative coding". By creating objects that are configurable, there is now a requirement to specify configuration each time the object is used unless there is some desirable default behavior (sometimes, a good idea). Some input now determines which behavior will be exhibited. The behavior options are still predetermined withing the bounds of what has been programmed, but how and when they will be used is no longer as fixed.
Going back to the original example, the programmer can define a number of sorting objects, and any object that has a sortable list can be assigned any of these by the programmer, or by the person using the program as business logic allows. The programmer might predetermine one of more of the sorting objects in the code of the program, or use a configuration file to control the options each time the program is executed. The programmer also might allow the user to specify which sorting option to use as an input to running the program (a command line options), or the programmer might let the user change the sorting method dynamically as the program is running through some user interface option. The program itself might select the sorting method based on the data being examined, selecting the appropriate sort automatically.
To be clear, declarative coding does not remove the need for procedural code. It simply isolated procedural code (separation of concerns) to a very specific scope in the program. It then allows objects to determine their behavior more declaratively,
"I am a Foo, and I use a Bar."
instead of,"I am a Foo, and this is how I Bar..."
So going back to the example code the Use Cases reveal some opportunities to implement code declaratively. This approach will save little in the short term, but drastically improves the usability of the code overall. Depending on:
- the programmer's level of expertise (proficiency in the programming language and with objects),
- the programmer's understanding of the problem domain (the nature of the specific problem(s) the program is trying to solve),
- and the likelihood that reuse of the code will occur either through:
- other projects or,
- many similar needs in the project at hand or,
- changing needs of the project at hand
Programmers with low levels of expertise might find anticipating the most effective ways to make the code configurable difficult. Writing declarative code can be (but is not necessarily) more time intensive than writing procedural code. Extremely novice programmers should probably not attempt to do this on "new" code, only in the process of refactoring well understood code. Intermediate programmers should look for opportunities to use declarative code preemptively (Prefactoring, ala Kenneth Pugh) but only apply it to a few of the most obvious opportunities in a given project.
It is good to learn by trial and error, but perhaps not prudent when the project's time line is at risk.
The Zend Frameworks includes a very useful Zend_Config class in two varieties: Zend_Congif_Ini (windows ini like) and Zend_Config_Xml. These classes open up files of the appropriate format and convert them into arrays of data that can then be passed in as constructor or method arguments. This example will use only the XML flavor of config file, but either file is a good option depending on personal taste.
The root tag in a config XML file is always used (in a valid XML file there may only be one root tag) and the name of the tag has no effect or side effect whatsoever. Each tag inside the root tag (there may be no character data as direct children of the root tag, only tags) represents a configuration option. Normally these options are mutually exclusive, but of the "extends" attribute is used in one, and the value of that attribute is the name of another, then the values in the extended option will be inherited by the option extending it. For any values specified by both options, the values of the extending option will override the extended option. For example:
<data>
<foo>
<test>1</test>
<test2>2</test2>
</foo>
<bar extends="foo">
<test>3</test>
</bar>
</data>
<foo>
<test>1</test>
<test2>2</test2>
</foo>
<bar extends="foo">
<test>3</test>
</bar>
</data>
In the above example, the values for foo are test = 1 and test2 = 2. The values for bar are test = 3 and test2 = 2. If bar did not extend foo, then it would only have the value test = 3, and no value for test2.
Tags at the second level are effectively associate keys, the config object turns the config file into a data array. In the above example foo and bar are keys in that array. Tags beyond the second level become arrays within the array:
<data>
<foo>
<test>1</test>
<test2>
<a>2</a>
<b>3</b>
</test2>
</foo>
</data>
<foo>
<test>1</test>
<test2>
<a>2</a>
<b>3</b>
</test2>
</foo>
</data>
In the above example, the value of test2 is now an associative array with the key "a" = 2 and key "b" = 3.
The use of config files, declarative coding, and related practices address a very critical issue in software development, risk. Procedural code makes assumptions, when these assumptions are correct and durable the procedural code does what is needed. If the assumptions are or at an point in the future become incorrect then changes must be made to the code, and they are often the expensive kind. Using declarative coding effectively requires assessing code risks.
From Use Case Zero (see Refactoring, Use Cases and Abuse Cases) it is clear that this particular program needs to specify two different RDF news feeds. These might typically be accessed by a URL on the Web, but that is not always a safe assumption. Some programs might have access to the RDF file via local file system (which is almost always less resource intensive than via Web URL), or even a local database. One assumption that can be said to be reasonably safe is that URLs can encode information for data stored in a variety of means, and in the more broad sense they are a special case of a URI, which could potentially reference any data.
For the time being, the program will make the assumption that one or more RDFs are accessible somewhere, as specified by the configuration. It will be up to the program to determine how to retrieve each RDF based on the location stored in the config file. The config file simply declares how many RDF sources there are, and gives an identifier. Parsing and validation of the identifier is a procedure left up to the objects that makeup the program.
Another feature that will be needed is sorting of the items in the RDF. Initially, the requirements, and thus the use cases indicated that all RDFs would be sorted with most recent first. In a later iteration of the project it is determined that there are actually two sorting needs:
- Most recent first
- Most recent last (oldest first)
Most items in the past feed will have already been processed when they were in the upcoming feed. There is also sometimes overlap in the two feeds, so the past feed is processed first to give priority to any that might have been missed. For past items the newest is most relevant because it is the one that was missed most recently.
In an ideal world, all missed events would be posted, but since multiple posts might be considered rude or annoying in the case of a Twitter account, the most pressing option is given priority. The order of the feeds matters, so a strategy for aggregating the two (as opposed to sorting each) is also needed. Examples of aggregation strategies might include one after the other, interleaved, random, etc.
One assumption that might be highly volatile in the future is that all news feeds will be RDF. Another assumption is that all items correlate with a URL for the full details. These risks are addressed in the config file, by including a place where each feed can be configured to use a different interpreter object and/or not assume there is a URL associated with each individual news item. This way a bad assumption can be more easily corrected by implementing additional code or altering behavior based on the configuration.
The original web code called itself iteratively using javascript, so at least in some cases a configuration value for auto-running the code iteratively might be prudent. When the program has control over how often it will iterate, allowing the config to specify how often to iterate is another good option. An option for which output method (View pattern) to use is also important since at least two different output needs are known.
The format of the successful return message should be configurable at some point, though this might be accomplished by different output classes rather than declared in the config file. An example of one approach to specifying a template for the message is included below. To handle the case where it might be desirable to distinguish which feed an item came from, each feed configuration is given a "label" value. Currently Twitter only allows 140 characters per post, but that could change. To reduce risk, a control for the length of the message is declared. The format for the date string and the service for URL shortening are also included to make future changes easier.
The posting method is assumed to be Twitter, but if that service ever ceases to exist the option to migrate to an alternative is highly desirable. The current assumption is that subscribers won't want to get more than one message at a time, but since that could also change it is included as a configuration option. Since posting to Twitter requires a login and password, it is included in the config file.
This may raise a red flag with many programmers, and in fact it should! Including logins and passwords in clear text can be risky business. There are many alternatives for storing the login and password information with varying levels of security. If the config file is stored appropriately, the risk of leaving this information in clear text is negligible. Understanding the appropriate security precautions is beyond the scope of this example, but suffice to say it should not be stored anywhere it might be accessible via the web.
Finally, keeping track of what has been posted requires some storage mechanism. Many people prefer a DB, and in many project configurations this is the optimum method. Given the non-concurrent execution nature of this program, a simple file system approach is sufficient.
If concurrent execution (two executions of the same program accessing the same resources) were possible then proper file locking and synchronization methods would be needed. Most databases handle this automatically, some file systems also allow for this with a little programming know how (file systems where PHP's locking mechanism works for example).
Based on the current use cases, concurrent executions should never happen for this program. The configuration simply needs to determine where to store the file, and determine a method and threshold for limiting file size. In the current implementation a limit of 100 past posts is set, but other versions might store posts for a number of days.
A config file for the TwitteRdf program might look something like this:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<basic>
<feedFormat>Rdf</feedFormat>
<feeds>
<one>
<uri>http://sysnews.ncsu.edu/news/index.rdf</uri>
<label>Unannounced</label>
<sort>recent</sort>
<itemUrl>true</itemUrl>
</one>
<two>
<uri>http://sysnews.ncsu.edu/news/events.rdf</uri>
<label>Upcoming</label>
<sort>oldest</sort>
<itemUrl>true</itemUrl>
</two>
</feeds>
<aggregate>sequence</aggregate>
<output>www</output>
<autorun>360</autorun>
<messageConfig>
<length>135</length>
<template>[[feedLabel]]: [[title]]([[url]]), [[date]]</template>
<dateFormat>h:i A l F jS Y</dateFormat>
<urlService>Snipr</urlService>
</messageConfig>
<postService>Twitter</postService>
<postServiceConfig>
<login>login</login>
<password>password</password>
<maxPosts>1</maxPosts>
</postServiceConfig>
<localCacheConfig>
<filepath>_cache/tweets.pser</filepath>
<thresholdCriteria>count</thresholdCriteria>
<threshold>100<threshold>
</localCacheConfig>
</basic>
</config>
<config>
<basic>
<feedFormat>Rdf</feedFormat>
<feeds>
<one>
<uri>http://sysnews.ncsu.edu/news/index.rdf</uri>
<label>Unannounced</label>
<sort>recent</sort>
<itemUrl>true</itemUrl>
</one>
<two>
<uri>http://sysnews.ncsu.edu/news/events.rdf</uri>
<label>Upcoming</label>
<sort>oldest</sort>
<itemUrl>true</itemUrl>
</two>
</feeds>
<aggregate>sequence</aggregate>
<output>www</output>
<autorun>360</autorun>
<messageConfig>
<length>135</length>
<template>[[feedLabel]]: [[title]]([[url]]), [[date]]</template>
<dateFormat>h:i A l F jS Y</dateFormat>
<urlService>Snipr</urlService>
</messageConfig>
<postService>Twitter</postService>
<postServiceConfig>
<login>login</login>
<password>password</password>
<maxPosts>1</maxPosts>
</postServiceConfig>
<localCacheConfig>
<filepath>_cache/tweets.pser</filepath>
<thresholdCriteria>count</thresholdCriteria>
<threshold>100<threshold>
</localCacheConfig>
</basic>
</config>
Lots of declaration going on! In the next post we'll start implementing some of the procedure that will drive the action.