27 December 2006

Some system development principles

Empathy Box :: 5 Principles For Programming got me thinking, so I put down some principles I go by too...


A classic... minimise coupling (dependencies). E.g. having to remember to call a method before another; having to call many methods to achieve a usecase, gathering some state from earlier method calls to pass into later method calls. Where possible enscapsulate or hide (once called modularity) this complexity in a single class/method which reduces the passing of state from one class to another, i.e. keep the state as close to the behaviour that requires it.

Avoid cyclic dependencies between classes, packages and systems. All dependencies should form a directed acyclic graph. The most stable and generic code should drop to the bottom of the graph and the most volatile code should rise to the top of the graph - refactoring can help push code up or down the dependency graph. Another way of stating this is that if class A depends on class B, then class B should be more stable than class A. Splitting a large application up into multiple modules, that are compiled seperately, helps enforce dependency rules. Classes that don't change together or are not reused together don't belong in the same module.

Under no circumstances allow code duplication, copy and paste is a sin punishable by death. In fact, avoid duplication of all kinds, maximise reuse of code, data, systems...

Avoid static state, pass as many dependencies into a class through the constructor (constructor-based dependency injection). This improves encapsulation and reuse of classes. All dependencies are explicit and the caller can decide the implementation of dependencies to pass into the class (assumming dependencies are expressed as interfaces or abstract classes). Avoid singletons (or static factories that contain state) at the pain of death, when an object uses singletons it becomes difficult to manage the dependencies of that object.

Avoid mutable state where possible strive for immutability. Changing state often leads to more bugs. I often use the final keyword for variables and very occasionaly for parameters too. Also do not add setters just for the hell of it, only add them where they are absolutely essential. I always prefer passing state in through a constructor, rather than through setters, this helps avoid cyclic dependencies between objects as well as avoid mutation.

Be ruthless with refactoring, extract common abstractions whereever you seem them but avoid premature abstraction, only extract abstractions when you have multiple concrete instances that share patterns of structure or behaviour or where you are sure there will be multiple concrete instances at some point. Applies some rules to inheritance based abstraction: "the Liskov Substitution Principle" states derived classes must be usable through the base class interface without the need for the user to know the difference. Ensures details depend upon abstractions and that abstractions do not depend upon details.

Be ruthless with deleting redundant code (you can always retrieve the code from your version control system). The less code there is, the more maintainable the system and the less bugs you have. So remove unused methods, fields, classes, packages etc. Modern IDEs help highlight unused code as do code coverage tools.

Avoid premature optimisation, however dont just write cr*p code either. Balance optimisations against the complexity they add. So if an optimisation doesn't add any complexity it is a no-brainer, just do it in the first place.

Use external libraries where possible rather than reinvent the wheel. Less code means less bugs and easier maintenance. E.g. log4j, ant, hibernate, apache commons-*, spring.


Ensure the team sits together, even better if all stakeholders (e.g. developers and business users) sit together. This allows for adhoc conversations during which knew insights are created and knowledge is shared. It allows for immediate feedback on interpretations of requirements, which ensures developers are always on track, delivering what the business wants. Even a corridor seperating team members will affect the ability of the team to work together.

Focus on recruiting a team of experts who can take the system through all stages of development, i.e. they have the skill to gather requirements, think about architecture, think about design and are responsible for coding, testing, release into production and initial (or second-line) support. Seperating these roles only causes communication problems especially because systems development is an iterative process, i.e. you gather a few requirements, think a little about architecture/design, write some code, realise your design doesn't work, rework it, then find out that some of your requirements are ambiguous and have to delve a little deeper into them. I think its ridiculous to hand over a design to coders and equally for coders to just knock something up and pass the result on to some sort of test team. Seperating roles also allows for "passing the buck" when the project inevitably fails. A small, multi-function team doesn't need to rely on documents (e.g UML-based specifications) they can just talk through important issues, possibly white-boarding them where necesary.

Invest enough time and effort in recruitment. The composition of the team is the most important factor determining the success of the project. Think about succession management, team members will get bored doing what they doing, if they are not given new challenges, or they may just leave for other external factors. So recruit even when you can manage with the current head-count. Its best to have a little spare capacity and have a constant handover (knowledge sharing) than be faced with trying to recruit for and handover a collegues role within a month.


Regular releases of the system.

Don't count the pennies when it comes to buying software development tools, for Java these might include a good ide such as Jetbrains Intellij IDEA, a decent profiler such as YourKit Java Profiler and a sql database tool such as Minq Software's DBVisualizer.

Use non-ide specific build files. I've not tried using a project-model based tool such as Maven or a continuous integration tool such as CruiseControl but I think they are worth considering.

Run unit-tests regularly. I don't really practise test-driven development, but ensure that the tests you do write get executed on more than an adhoc basis.

Once in a while run your system through a profiler to ensure you've not added many more cpu hotspots, increased memory usage or introduced memory leaks. Of course, this doesnt help you find cases where you've increased the cpu usage on a uniform basis across many areas (as they will not be hotspots), so it may be worth writing some some of standard system test that you execute and record timings for. That way a few months down the line, you can reexecute the test and compare timings against previous ones.