How to Design Software That Supports Extension
One of the toughest problems when designing and developing software is that of future proofing it. You can almost never accurately predict the shelf life of software. Some software continues to get used years after its developers thought it would and some unfortunately gathers dust much earlier.
The largest cost in a software products life cycle is nearly almost always software maintenance. Making changes once the software is already out the door and in the hands of its users is much harder. So, it stands to reason that getting critical design decisions and choices right early will save you time and money in the long run. What happens if you missed one critical feature in the system but implementing it after the product has shipped would require a major architectural change? Whilst there are plenty of practices you can put in place to reduce this happening you can never be 100% sure how your product will be received by its users unless you have conducted a very exhaustive review of user feedback. This is where alpha and beta releases come in handy to have select user groups give back feedback. Hopefully any glaring issues are picked up during this stage.
Regardless, there are ways you can increase plug-ability and extensibility for future bug fixes, service packs, features, expansions etc. Basically this means any modifications that have to be performed after the software has been released and is essential to the longevity of the product. How is this achieved? By making better use of inheritance and interfaces.
Classes have types
How much do you think about classes when you use them? It’s easy to remember the distinction between a class and object; a class defines a template or blue prints for how to replicate a design and an object translates the specified template or blue print into a specific instance or respective construction. But, do you think of objects as types? If so good! It means you are more likely to use objects by their type (interface) than by their implementation.
It’s important to remember that a class defines an objects internal state and operations whilst an objects type only refers to the operations it supports or can respond to. It does not refer to the actual implementation. Objects generally aren’t limited to the amount of types they can respond to via the use of inheritance and interfaces.
Inheritance and interface inheritance
Inheritance usually defines a sub class’ interface in terms of its inherited (parent) class. It’s generally assumed that an inherited class takes on much if not all of the inherited interface and extends it in some way. Maybe changing the implementation of one or two interface members inherited. Any more and there probably isn’t any reason to use inheritance over interface inheritance because you forgo the typical information sharing habits inheritance benefits from.
In contrast, interface inheritance or sub-typing, refers to another object conforming to the same interface but does not have to have an implementation that is similar to any of its peers. That is, where implementations are widely different interfaces are good because they receive little benefit from the code reuse inheritance offers. Interfaces only describe object substitution; the ability to substitute one object in place of another and fulfill any requests made to the interface. It does nothing to ensure their implementation is similar.
Remember, class sharing (similarities) VS object substitution (Fulfillment)
Always program to an interface and never to an implementation
You should always program to an interface and not an implementation. As previously discussed, this doesn’t strictly refer to the concept of the ‘interface’ type commonly found in many OOP languages. It includes root level or parent level classes that have been inherited. There are generally three interfaces you can program to:
- Conventional interface types – When you have objects that specifically implement a particular interface always program to this interface when you are only using methods defined in this interface
- Object inheritance – Whether the parent class is abstract or not, always program to the parent class when possible. Doing this affords you a lot of flexibility through polymorphic use. If need be, create a higher level abstraction that is abstract and use that
- Creational patterns – Creational patterns: factory, abstract factory, builder and singleton ensure your system is written in terms of interfaces and not implementations. They decouple the instantiation and creation of an object with its interface
Conventional interfaces are very useful and provide a nice connection point between objects. Object inheritance is great because it makes good use of code sharing and allows sub classes to override or implement (if previously abstract) implementation details specific to the child class. One very important rule when inheriting is to never hide apart of an inherited interface. All aspects of the parent interface should be true for sub classes of this interface. If this rule is broken polymorphism becomes useless.
If you can neither use interface types nor parent classes in inheritance hierarchies try and abstract it to a creational pattern. Again, you want to decouple the creation of the object with the use of it, you allow the two to vary indecently.
By making smart choices about how you instantiate objects; object creation, how you use objects; object invocation and collaboration, and how you structure your objects; neighborhoods and patterns you can greatly increase the flexibility with which your design can adapt and change long after it was implemented. When objects communicate at boundaries (interfaces) it is significantly easier to change what’s behind them (the actual implementation). This is really the secret, supporting change by allowing parts of your software to flex.

Leave a Reply