Gaston Dombiak & Maximo Prieto's Position paper
LIFIA. Departamento de Informática. Facultad de Ciencias Exactas. Universidad Nacional de La Plata. Argentina (*) also UBA - Universidad de Buenos Aires e-mail: [gaston, maximo ] @sol.info.unlp.edu.ar
Simple designs are easy to maintain but as they become more complex, so does its maintenance. Even designs that were conceived to resist constant reality changes could fail in the long run. This is because the higher the complexity of the design, the bigger the risks that concessions or "innocent" errors become a threat to the design's maintenance. In this paper we will analyze and describe the reasons for poor application maintenance.
A few years ago, a simple but serious issue raised while building software with objects. The dilemma appeared when unmodified application's objects began to experience problems when other objects were modified. Moreover, even after using rules and methodologies learned in books about OO modeling and design, the problem wouldn't go away. Therefore, the search for reasons that could explain the phenomena was pursued.
Some of the reasons found were: wrong assignation of responsibilities (Absence of essence and Flux programming instead of responsibilities programming), unclear definition of object's main responsibility (Absence of essence) and the characteristic of objects being both generic and context-dependent at the same time (Missing objects generating context dependency).
A combination of the reasons stated above together with a complex design created the perfect culture medium for ruining any successful modification or possibility to reuse objects. Moreover, as the complexity of an application tends to grow, the consequences of making any of the mistakes described in this paper become more powerful to the point of becoming devastating. Thus, maintenance costs can only go up.
A list of common mistakes that affect any object's architecture is presented below:
The 'object' is the smallest abstract component available for software construction. Just as bricks are the basic things to build walls, the wall is the 'base' for the roof. In this way, each resulting construction is the base for the next construction - in a higher level of complexity or abstraction. This same process of 'layering' complexities is also present in the object model. The point that we want to highlight is that even though architecture is important, it is rendered useless if its underlying objects (layers/base) are not solid enough.
Many times due to a deficient distribution of responsibilities, designs have monolithic objects. These type of objects lack a clear cut-definition or essence, which allow for many responsibilities to be assigned to them. Since monolithic objects are hard to work with, it is best to split the responsibilities of one monolithic object in many other objects [REB90].
The consequence of absence of essence generates objects with defective contracts or without contracts. Thus, the maintenance of a design with objects that lack of essence is laborious. We can therefore state that any object must have a clear-cut definition or essence, and that the object's responsibilities and collaborators must be derived from its definition [ALV98]. The essential responsibilities of an object must be a direct consequence of the object's essence [ARI]. During the design and development phase, the best way to evaluate the object's healthiness is to pass it through the desert test [ALV98]. Moreover, a highly cohesive model can only be obtained when the object's essence determines all of its collaborators and responsibilities.
During the process of developing software, the architecture and its underlying objects are victims of small concessions that undermine the maintainability and extensibility, by introducing a series of context-dependent decisions.
A common error in implementing these concessions is to embed the limitation in the applications' objects, instead of encapsulating it in a specific object whose main responsibility is to model the requirement without gluing a generic application object with a specific concession. That is, the best way to implement a concession is to model it with objects.
For instance, lets suppose that we have a financial application that is currently being used in Mexico. A year later, after the application behaves "decently", it is decided to install it in Brazil. However, in Brazil, taxes are calculated in a completely different way. Considering we want to maintain only one version of the application, despite the number of countries where this application will be installed, we would need to modify the object model in such a way that it could adapt easily and quickly to each location. The best way to face this kind of requirements is to encapsulate the tax calculation in an object, creating a hierarchy of TaxCalculation where each subclass models different ways that a tax can be calculated. Furthermore, we will need to model the ApplicationLocale class whose main responsibility is to specify the way in which the application will actually behave in each country. In our example, the ApplicationLocale specifies which subclass of TaxCalculation will be used in each country.
According to the example given above, if we are faced with the need of creating a new way of calculating the taxes, we would just need to create a new subclass of TaxCalculation.
Detecting concessions is not an easy task, but the later we found them makes the consequences even worst. This is why it is preferable to spend some time trying to separate what is context-dependent from what is context-free before starting to implement a given design. This enables to achieve highly adaptable models to new circumstances with a minimum effort. There is a technique called "The Desert" [ALV98] that helps designers to achieve context-free objects.
In software development it is important to specify the scope of an application. This might be so since the scope will further influence and define the design's limits, functionalities, and boundaries. However, this kind of scope, a width-scope, does not specify how the design is like inside. There is another kind of scope, which usually relies on the application's designer. This last type of scope is considered a "depth-scope". A "depth-scope" is one that has to do with complexity and adaptability.
Right from a design's conception, its complexity should be kept at a minimum level. In that way, designers can better understand their own design. In order to reach a good balance, the design's complexity should be incremented gradually. Nevertheless, if a design ends up with a very high complexity, it could become hard to predict its behavior when changes occur. Therefore, estimating the impact of a modification can only become harder.
As an example to the stated above, let's suppose that we have to build a machine, which main responsibility is to make additions and display the results. We have two choices: the first one is to "develop" a simple calculator which meets the requirements; the second one is to "develop" a super fast PC with a 17'' monitor full-color with the latest OS and develop a software that not only knows how to make additions but is also able of making other arithmetic operations. The second choice has been exaggerated on purpose to show that complicating things in an excessive way can lead us to lose perspective on main requirements. The bigger the design, the higher the chances for its object to fail.
The design's adaptability or generality has also to be the appropriate according the circumstances and requirements. Making a design too much generic is a way of increasing the level of complexity. However, if a design is rigid, that is not generic, it might mean that it will be hard to reuse it. So it is very important to achieve a reasonable balance in this subject since it has to do with reusability and complexity.
Software development is an activity that many times bears a lot of pressure so that deadlines are met and schedules are maintained as expected. That is why, many times, if an error comes up while the user is testing the application, developers may apply a patch so that the user can continue his or her test. The quality of these patches might not be the best but at least the user is allowed to continue their tests. The pitfall is that if these patches are not reviewed they will eventually end up as the official solution for the bug. Therefore, if this practice goes on for a while, after installing many patches it will make the application hard to maintain. This is so because most patches are applied without performing an impact analysis, or even worse, without understanding what the real problem was. Usually, developers do not distinguish between the real problem and symptoms expressed by the errors.
For instance, when an object does not understand a message the developer creates the corresponding method. This naive approach will potentially break the polymorphism between the objects. A correct approach would be to understand the potential objects that may receive the message. Therefore, for example, polymorphic hierarchies may remain polymorphic after the bug was resolved.
As an object model becomes more complex, it becomes harder to understand its dynamics, the reason for certain objects' existence, what were they created for and how must they interact with other objects. It is important to understand in advance the consequences of any modification and its impact. It is risky to assume that because a bug was solved, the rest of the application will not be affected by the modification. Therefore, the scope of the impact of the modification should not be measured only based on whether the bug was solved or not.
It does not matter how big a change is, we need to be able to understand the objects' behavior, responsibilities and essence so that we can learn what the consequences of making a change are. Not being able to understand these important concepts will only bring instability to the application. However, if we do understand them, we will then be able to "get the big picture", which will help us decide which modification would work best. Using tools that help to visualize and animate a given design [ALV95] is a good way to quickly understand the dynamics of an object model, and therefore to predict the impact of a modification.
Throughout the paper, we saw how maintenance problems were caused by four major issues. Early detection of mistakes helps minimize maintenance costs. Designers can use the mentioned issues as tools to prevent future problems. However, being aware of this type of situation does not ensure the avoidance of maintenance problems.
Objects need to be healthy in order to mature and evolve. Moreover, since healthy objects live in healthy designs, we can conclude that the prevention of errors is crucial to obtain healthy design.
Since software development is a human activity it would be interesting to study the human behavior while developing software. We are quite sure that many surprising discoveries could be found. Many of these discoveries can help us understand the reasons for complex application maintenance.
Good communication among developers is important when building maintainable applications. Characteristics such as the homogeneity in thinking and designing are key as well. Achieving homogeneity in the way people think has to do with how they resolve a design problem. The challenge is how to get a group think homogeneously. Moreover, many other characteristics should be studied.
To participate in this workshop please read the Call for Participation