In many software development projects, Agile methods have proven to be a meaningful alternative to other, more complex process models. When using Agile, development-centric models, however, the question regularly arises about how the architecture should be adequately addressed – for example, Scrum does not feature an explicit architect role in the development process.
One of the principles of Agile software development is to make design decisions as late as possible (Last Responsible Moment). Agile suggests not creating a big design up-front (BDUF) for a target system, because the scope of an Agile project is variable in size. A target system often cannot be defined at all because details in complex systems are mostly unknown and emerge at a later point in time. The software development process is also seen as a learning process. The more you know about a domain, the better a suitable design can be created that specifically addresses these unknown details.
This becomes possible because refactoring is one of the typical engineering practices used in Agile teams. Software architecture is understood to be the result of a permanent refactoring and this approach is called emergent design.
Refactoring as precondition
The term source code refactoring describes a process in which existing source code is restructured. The external behaviours and functionality of the program remain unchanged. Only the structures and composition of the source code change.
In addition, refactoring slows down the aging of software, and in an ideal world may even stop it. Already mature software can be revamped with refactoring. This is done by continuously “cleaning up” the code and reducing complexity. The internal architecture is consequently improved, and an object model is created that increases readability and extensibility, leading directly to higher maintainability.
Typically, refactoring involves a set of standardised essential micro-refactoring procedures and practices. Each procedure or practice follows the preservation of the program behaviour, so that the conformity of the functional requirements is maintained.
Modern integrated development environments (IDEs) often provide extensive automated support for the software developer during refactoring. When refactoring, the complexity is reduced and hidden or dormant errors as well as security gaps in the program can often be found and eliminated at the same time. However, refactoring creates the risk that external functionality may be changed, or errors may be incorporated into the source code. It is not surprising, then, that refactoring is an integral part of Test-Driven Development (TDD). Extensive test coverage is a prerequisite for painless refactoring.
Unit Test and walking on the Green-Path
Automatic Unit Tests should be set up prior to refactoring to ensure that routines still behave as expected with the new design. Unit tests can give stability even to large-scale re-orderings. The re-ordering in software design is then an iterative cycle in which a small program transformation is performed, and the software system is then checked for correctness before another small transformation is performed and tested. Through many small steps, each followed by tests, the software moves from its previous state to the desired target state, and new design changes can emerge. For this very iterative process to be feasible, the tests must run very quickly and cover the areas which shall be refactored and its boundaries. Without a proper Unit Tests, refactoring is like balancing on a high wire without a safety net. Refactoring is only carried out if the Unit Test suite, our safety net, is completely in the green phase and all tests are passing. This is the only way to ensure that refactoring does not break anything.
Software Design is emergent
Emergence must be empirically gained, because it has always been shaped according to what was known and necessary. Thus, the emergent design is the sum of requirements resulting from changed intentions and refactoring. But this does not mean that the design also meets the global optimum. And this is the moment when Technical Debt can rise. If design is emergent and disoriented, this can lead to costly refactoring cycles that have to be performed later! A good, sustainable emergent design must be created oriented. And this orientation must be available where the design is created. Here it stands on two pillars:
- On one hand, the self-organized team needs to know which goals it wants to achieve. Quality criteria, non-functional requirements, existing experience and competencies play a decisive role. In other words, the team must agree –with the business side– what it intends to achieve with the chosen software design and why it wants to achieve it.
- On the other hand, the self-organized team must have a common understanding of what architectural options are available, which ones to choose and when, and how the chosen strategy works. Documentation alone is not enough –this is quickly forgotten as a work artefact in the emergent creation of architecture. It needs a real, internalised image of architecture, its motivations, its options and alternatives.
After all, an emergent design usually depends strongly on non-functional requirements and how and where our design evolves. However, especially in Agile methods, the fact that changes in functional requirements can and should always occur when performing a refactoring cycle addressing design changes with conscious foresight to factors known at the time should be considered.
Through technical excellence of programmers, appropriate refactoring practices, design patterns and the SOLID principles significant design changes can be realised and the emergent design gain maturity as more facts are learned throughout the development cycles.