Some thoughts...

Published July 12, 2006
Advertisement

Potissimus Intentio


Embarking on solving a problem can be quite an adventure in and of its self. Our goal being, quite obviously, to solve the problem put before us. The approaches each person takes to solving the problems at hand can oftentimes differ significantly, even between people with similar backgrounds and experience. With the application of patterns and refactoring, one often finds that the various solutions will converge in similar manners towards the same general solution. Development based on acceptance testing and unit testing tends to guide the initial design process in very similar manners for the participants, ultimately giving similar solutions to the same problem, again. All of this suggests that tests and refactoring end up reducing to patterns, which is not at all surprising as we shall hopefully see.

Exemplum quod Expertus


Ultimately the purpose of tests is to validate that something performs an operation correctly and to the extent required. The differences between acceptance and unit tests fall on how they are written, and the level at which they look at solutions. Unit tests look at the solution at the lowest level, each object and method individually. Unit tests examine the behavior of objects, functions, properties, and even fields to determine if the behavior of the object is well defined, and produces the expected output for both erroneous inputs, and for correct inputs. Acceptance tests, on the other hand, look at interfaces and feature requirements. Acceptance tests validate that a module implements a feature to the requirements set forth by the client. These tests are very black box, with little to no understanding of the internal operations of a module, just the expected outputs based on the inputs. Erroneous and correct inputs are tested, and should result in expected outputs, as defined by the client (be that an exception for erroneous output, or some form of a return code). Oftentimes acceptance tests define ambiguous requirements that a module must meet, in these cases clarification can either be provided at the unit test level, or by requiring the client to provide more input on the ambiguous areas.

Unit tests should be, if you are following eXtreme Programming to any extent, written before the code to make the test pass. The idea being to write the simplest piece of code to make the test pass, then to write further tests that require the implementation to get more complicated in steps. This allows you to design how you wish to interface to a particular behavior before you write any code. This could be preceded by a design session, to further refine how the object should behave. Tests are not, and should not be, the only factor in deciding how objects and interfaces should behave. As the tests get more and more complicated, requiring your objects to become more feature complete, you should refactor frequently, thus refining the behavior. This process will also tend to expose low level patterns (such as strategy, state, etc) that could be used. In fact, proper refactorings will often time end up moving towards patterns, instead of away from them. You will find that, when applying refactoring, patterns will emerge from your code naturally. Thus code based on unit tests and refactoring tends to evolve towards a more patterned state.

Acceptance tests focus on interfaces, and interactions between interfaces. These interfaces could be to individual objects, or to entire modules. The refactorings at this stage tend to be more based on removing dependencies, or moving dependencies to higher level interfaces. By moving to more abstract interfaces you remove underlying principles and assumptions from the external interfaces, thus allowing them to be changed in a cleaner and more stable fashion. This follows from the Dependency Inversion Principle, which states that both high and low level modules should not depend upon each other, but upon abstractions. Furthermore it states that abstractions should not depend upon details, but that details should depend upon abstractions. These refactorings will result in behavioral patterns emerging, which tend to be far more abstract and relate more to how modules communicate amongst each other than on how internal operations in modules behave. For instance, modules may move to message oriented patterns, which would allow for distribution along with service oriented behaviors. This would be vs. something like the strategy pattern, which is relatively low level most of the time.

Reductio ad Exemplum


Refactoring involves a process of cleaning up code by making small changes that improve the readability and maintainability of the code in question. One nice side effect of refactoring is that the resulting code typically ends up becoming simpler. Simplicity is in the eye of the beholder, and what may ultimately be simpler, to maintain and to code, may appear to be more complicated depending on the experience level of the viewer. Functionality gets moved out of the location where one would typically find it, and into smaller objects, or even into other modules. These changes in location can often times confuse inexperienced programmers, who may have trouble with various levels of abstraction and the relocation of dependant functionality. Ultimately though refactoring does produce easier to maintain and cleaner code.

Patterns tend to naturally emerge from refactorings as a natural consequence of the simplification process. By using the functionality of the language to branch to the appropriate logic and execute functionality specific to various concrete algorithms, we increase the extensibility of our modules while increasing the amount of actual hard coded logic we must maintain. Language features such as interfaces, inheritance, and polymorphism all come into play here. Common patterns one may find that decrease hard coded logic are state and strategy, both of which use polymorphic dispatch to change the executed behavior of an algorithm and move case specific logic away from algorithmic details. As these patterns emerge, however, one must resist the temptation to solidify their dependency upon them. Often times a refactoring will transition through stages, where multiple patterns will come and go. This is especially true of low level implementations, where the code can change quite frequently before finally stabilizing. At higher levels, these patterns emerge slower, but often times they too will transition through various stages before stabilizing. Stabilization will typically occur either when an object has been fully implemented and unit tested, or when a module has completed all acceptance tests. As requirements evolve though, the patterns used and the interfaces exposed through those patterns will also evolve. Dependencies will tend to decrease the amount of flexibility available for refactoring public interfaces, but internal changes can still be accomplished to enable cleaner designs. The facade pattern can also help in this regard, as it will serve as a layer between the public interface and the actual implementation.

Be wary of the temptation to refactor to a specific pattern. This can be either a good thing or a bad thing; if you know a bit about the future application of the code then you may be able to clean it up by moving to a specific pattern. Moving to a specific pattern without any foresight is a bad idea though, as you will typically either overcomplicate the code or become stuck with set of patterns that don't adequately meet your requirements. Refactoring from bad decisions is quite possible, but more time consuming than refactoring to the correct one in the first place.
0 likes 2 comments

Comments

Mushu
I just discovered that you are my antithesis!!! En garde, antithesis!!!!
July 12, 2006 12:09 PM
capn_midnight
TLDNR
July 12, 2006 10:36 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement