Diagnosing Java code: Designing Easy Code Maintenance

Source: Internet
Author: User
Tags contains data structures empty expression extend final inheritance variable
Design this month, Eric Allen explains how avoiding and controlling irrational changes is the key to keeping code robust while making code easier to maintain. He focused on concepts such as functional style code writing, and methods for labeling fields, methods, and classes to handle and prevent variability. Eric also explains the role of unit testing and refactoring in this task, and provides two tools to help with refactoring. Share your views on this article with the author and other readers in the relevant forums. (You can also click discussion at the top or bottom of this article to access the Forum.) )
Effective debugging comes from good programming. Designing easily maintainable programs is one of the most difficult challenges programmers face, partly because programs are not usually maintained by programmers who write code. In order to effectively maintain such programs, new programmers must be able to quickly understand how the program works, and if the programmer can understand the small parts of the entire program alone, it is easy to understand how the program works.

By discussing variability, scalability, private methods, final methods, final classes, local code, unit tests, and refactoring issues, we will briefly describe some of the ways in which programs are written to help make programs easier to understand and maintain.

Variability and the availability of decoding
First of all, the variability issue is discussed. If, during the calculation of a program, the data processed by each of its parts is not changed by other, remote parts of the program, it is easy to understand each part of the program separately.

Too much information
For example, consider a program that uses an instance of a container class to modify its component links. Each time a container passes a method from a part of a program to another part of the program, and each time a new expression is invoked (where the container is passed as a parameter), the container may change from the control of the calling method.

Before we begin to understand how each method invoked by the calling method modifies the container, we cannot really make sure that we understand the invocation method, and thus our ability to diagnose errors is even worse. If each of these invoked methods calls other modification methods sequentially, the amount of code that the programmer must read in order to understand a single method increases rapidly and is too much to control.

For this reason, it is advantageous to use different classes for variable containers and immutable containers. In immutable versions, the container's fields can be marked final.

Recourse to function style
Writing code to construct new data is called a function style relative to modifying old data, because the program's method is similar to a mathematical function, and its behavior is described separately according to the output returned by each input.

The advantage of the often overlooked function style is that it is fairly easy to understand individual components of the program individually. If the data manipulated by the method is never changed by any of the operations performed in its principal, then the programmer understands that the method must do is to understand the results of those operations returning. In contrast to a scenario that calls several other methods in one of the previous methods, several other methods in that scenario modify the data structure that the method operates on.

A fairly good feature of the Java language is that it allows us to declare when we want to make a data immutable by using the final keyword (a pseudo instruction as a type inspector).

Using the final keyword to avoid change is a good way to "pin" the method behavior of the class. Each time you modify a field, it is possible to change the behavior of the method that references the field. In addition, mark the field as final so that other programmers who read the program immediately know that no matter how large the entire program is, never modify the field. For example, consider the following class hierarchy, which represents an immutable list.

Listing 1. Class hierarchy that represents an immutable list

Abstract class List {...}
Class Empty extends List {...}
Class Cons extends List {
Private final Object A;
Private final List rest;
}



All the fields in these classes are marked final. Is it enough to make sure that instances of these classes are immutable? Not quite enough. Of course, it is important to remember that the component of the field itself may not be final, even if the field is marked final. When those components change, any part of the program that references those components may be modified regardless of whether the field itself changes. In the example above, although the constituent elements of the list cannot be modified, we must check that the elements themselves do not contain non-final fields that may be modified.

In this case, although the list may contain mutable elements, we can see that the sequence of elements stored in a given list is immutable for the following reasons: the instance of the Empty list (that is, the list of zero length) does not contain any elements at all; The Cons (non-empty list) instance contains two fields, all final. The first field contains the first element of the list, it cannot be modified, and the second field contains a list that contains all the remaining elements. If the contents of this list are not variable, then the inclusion list is immutable.

But the list contained in this second field is one less than the length of the containing list, so if we know that all the lists of length n are immutable, then we know that the list of length n + 1 is immutable. Because we already know that a zero-length list is immutable, we also know that the list of lengths 1, 2, 3 is also immutable.

It is tedious to keep track of a data structure similar to this, but it is worthwhile to do so when you can determine the global nature of this structure, such as immutability.

Control changes
The best strategy to prevent unexpected changes is to avoid all changes as much as possible. We should use it only when there is a reason to change (for example, when the code structure is greatly simplified). When changes can be avoided, the benefits are enormous (in terms of lower maintenance costs and enhanced robustness).

Even if there is a reason to change the data, it is best to try to control that change so as to limit the damage that might occur. Iterators and streams are excellent examples of data structures explicitly designed to control change by allowing us to use a range of elements in a regular, well-defined form, rather than explicitly modifying one of those elements.

Private method
Just as setting a field to final helps limit the external impact on field values, setting them as private helps limit the impact they have on other parts of the program. If the field is private, then we can be sure that other parts of the program are not directly related to it. If we remove the field and replace the internal representation of that type of data, we simply care about correcting the internal methods of the class to properly access the new data.

In the previous example, note that the fields of class Cons are private. In this way, we can control how to access those elements by reading methods (getter) and similar methods. If the future maintainers of our list sometimes want to modify the internal representation of the list (for example, you can argue that an array based list might be more efficient on some platforms), then programmers can do so without having to modify or even view any of the clients of those lists. As long as he rewrites the getter, he can take the appropriate action on the new data.

Final method, final class and understanding local code
In contrast to marking a field as final, marking the method as final is often blamed for inconsistent with OO design goals because it prevents inheritance polymorphism. But when trying to understand the behavior of a large program, this helps to understand what methods have not been overridden.

It is true that good OO design now involves using a lot of inheritance. In fact, inheritance is at the heart of many OO design patterns. But that does not mean that we should allow every method we write to be rewritten. Typically, programs will implicitly rely on some key methods that are not rewritten. By marking such a method final, we will allow other programmers to better understand the expression behavior that invokes the method.

In addition, marking a class as final will greatly improve the scalability of the decoder. It really helps to get a rudimentary idea of which classes in your program will never be quilt-class. In fact, I think that only classes that should not be marked final are classes that are really quilts in the program, and classes that intentionally subclass from external components (as part of programming).

One might think that this concept would bind future code maintainers so that they would not be able to extend the code. I don't think that's going to limit them. If the future maintainers of the program need to extend the code to include subclasses that did not exist before, how do you become a "maintainer" of the code if they have access to the source code (if they do not have access to it)? , it is not difficult to delete the final keyword on the corresponding class and recompile.

At the same time, the added keyword acts as an automated validation document on the important invariants of the program ("Automatic verification" because if the document is corrupted, the program will not even compile). By forcing developers to consciously choose when to delete such invariants, we can help reduce the introduction of errors.

Unit Testing and change
Unit tests can always help you understand code that has side effects. If a set of unit tests fully demonstrate the role of the method in the program, the programmer can understand each method more quickly by reading its unit tests. Of course, whether a unit test really covers all the roles is a big problem. An effective range analysis tool similar to clover can provide some level of help here.

However, note that unit testing itself is much simpler than writing strict function methods. To test a strict function method, all it involves is calling these methods with a variety of representative inputs and checking their output (and making sure they can be thrown when an exception should be thrown).

When testing methods that modify the state of a data structure, we must first perform such actions as are required to put the input data into the state that the method expects, and then, after calling the method, to check that the data that the client expects is correctly executed.

Encapsulating with refactoring tools
These tips are useful when writing new code, but what happens when you have to maintain an old code that is almost impossible to decode? Refactoring, refactoring, or refactoring.

While refactoring old code can be time-consuming, it's worth it, especially if all the tools that support refactoring now support Java code. There are a number of powerful tools for automatically refactoring Java code that can automatically save key invariants.

A very versatile tool for refactoring Java code is the idea development environment. This environment provides automatic support for a considerable number of Martin Fowler refactoring modes. Another very useful tool I found was codeguide, an IDE from Germany. Although the list of automatic refactoring is small compared to idea, it shows an extremely powerful feature-continuous compilation. When you enter new code, Codeguide analyzes it and tells you whether the project is incomplete (which, of course, produces a very short delay that prevents the error signal from being emitted for each keystroke).

Although continuous compilation has a negative impact on the response, it is well worth the wait in some contexts. For example, you can enter final in front of the field and immediately see if the project is incomplete. If none, then you know that the field has not been modified anywhere in the program. Again, you can enter private before the field, and immediately obtain a list of all external accesses to the field (in the wrong form).

Another excellent feature of Codeguide is that it provides seamless support for JSR-14 experimental extensions with generic types (plans are officially added to Java 1.5).

While writing code for the sake of decoding can take a lot of time and effort, it can help increase the lifetime and robustness of your code and significantly improve the quality of life of those programmers who are facing the maintenance Code task. Finally, it's time consuming to refactor old code so that it's easier to maintain, but you'll know it's worth it when you have to fix the next error.


Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.