Most good designers avoid using implementation inheritance (extends relationships) Just as they do without plague ). In fact, 80% of the Code should be completely written using interfaces instead of extends. The book "Java Design Patterns" describes in detail how to use interface inheritance to replace implementation inheritance. This article describes why the designer did this.
Extends is harmful; it may not be for Charles Manson, but should be avoided whenever possible. The book "Java Design Patterns" has spent a lot of time discussing how to use interface inheritance instead of implementing inheritance.
A good designer mostly uses interfaces in his code, rather than a specific base class. This article discusses why designers choose this and also introduces some interface-based programming basics.
Interface and class )?
Once, I attended a Java User Group meeting. During the meeting, jams Gosling (father of Java) made a speech by the initiator. In that memorable Q & A part, someone asked him: "What do you want to change if you reconstruct java ?". "I want to discard classes" he replied. After the laughter subsided, it explained that the real problem was not because of the class itself, but the implementation of inheritance (extends relationship ). The implements relationship is better. You should avoid inheritance as much as possible.
Lost flexibility
Why should you avoid inheritance? The first problem is that you use specific class names to fix specific implementations and add unnecessary difficulties to underlying changes.
In the current Agile programming method, the core is the concept of parallel design and development. Before you design the program in detail, you start programming. This technology is different from the traditional method-the traditional method is that the design should be completed before coding starts-but many successful projects have proved that you can develop high-quality code more quickly, compared with the traditional step-by-step method. However, at the core of parallel development, flexibility is advocated. You have to write your code in a certain way, so that the newly discovered requirements can be easily merged into the existing code as much as possible.
It is better than implementing the features you may need. You only need to implement the features you need clearly, and be moderately tolerant of changes. Without such flexible and parallel development, it would be impossible.
Inteface programming is the core of flexible structure. To explain why, let's take a look at what happens when they are used. Consider the following code:
F ()
{Shortlist list = new shortlist ();
//...
G (list );
}
G (sort list List)
{
List. Add (...);
G2 (list)
}
Now, we assume that a demand for fast query is raised, so that this sort list cannot be solved. You need to replace it with hashset. In existing Code, changes cannot be localized, Because you not only need to modify F (), but also need to modify g () (it has the argument list parameter), and g () any code that passes the list. Rewrite the Code as follows:
F ()
{Collection list = new collections list ();
//...
G (list );
}
G (collection list)
{
List. Add (...);
G2 (list)
}
In this way, the linked list can be modified to hash, but new hashset () can be simply used to replace New sorted list (). That's it. There are no other places to modify.
As another example, compare the following two pieces of code:
F ()
{Collection C = new hashset ();
//...
G (c );
}
G (collection C)
{
For (iterator I = C. iterator (); I. hasnext ())
Do_something_with (I. Next ());
}
And
F2 ()
{Collection C = new hashset ();
//...
G2 (C. iterator ());
}
G2 (iterator I)
{While (I. hasnext ())
Do_something_with (I. Next ());
}
The G2 () method can now traverse the derivation of collection, just like the key-value pair you can get from map. In fact, you can write iterator, which generates data instead of traversing a collection. You can write iterator, which obtains information from the framework or file of the test. This provides great flexibility.
Coupling
For implementation inheritance, a more critical issue is coupling-annoying dependency, which is the dependency of one part of the Program on another part. Global variables provide a classic example to illustrate why strong coupling causes problems. For example, if you change the type of a global variable, all functions that use this variable may be affected, so all the code will be checked, changed, and retested. In addition, all functions that use this variable are coupled with each other through this variable. That is, if a variable value is changed when it is difficult to use, one function may not affect the behavior of another function correctly. This problem is significantly hidden in multi-threaded programs.
As a designer, you should try to minimize coupling. You cannot eliminate coupling together, because the method call from an object of a class to an object of another class is in the form of loose coupling. You cannot have a program without any coupling. However, you can minimize coupling by following oo rules (the most important thing is that the implementation of an object should be completely hidden in the use of its objects ). For example, the instance variable of an object (not a member field of a constant) should always be private. I mean in a certain period of time, there are no exceptions and constant. (You can occasionally use the protected method effectively, but the protected instance variable is an annoying thing) for the same reason, you should not use the get/set function-they only make people feel too complicated for a public domain (although the access function that returns the modified object instead of the basic type value is in in some cases, the reason is, in that case, the returned object class is a key abstraction during design ).
Here, I am not angry with the book. In my own work, I found a direct relationship between my oo methods, fast code development and easy code implementation. Whenever I violate the center's OO principles, such as implementation hiding, I overwrite the code in the result (generally because the Code cannot be debugged ). I don't have time to rewrite the code, so I follow those rules. What do I care about? I am not interested in the reason for cleaning.
Vulnerable base issues
Now let's apply the concept of coupling to inheritance. In an extends inheritance implementation system, the derived class is very closely coupled with the base class, and this close connection is not expected. The designer has applied the nickname "vulnerable base-class problem" to describe this behavior. The base class is considered fragile because you modify the base class when it looks safe, but when it inherits from the derived class, new behavior may cause function disorder in the derived class. You cannot simply check the methods of the base class in isolation to identify the changes of the base class. Instead, you must check (and test) All the derived classes. Besides, you must check all the code, which is also used in the base class and derived class objects, because the code may be broken by new behaviors. A simple change to the basic class may cause the entire program to be unable to operate.
Let's check for vulnerable base-class and base-class coupling issues. The following class extends uses the Java arraylist class to make it run like a stack:
Class Stack extends arraylist
{Private int stack_pointer = 0;
Public void push (Object article)
{Add (stack_pointer ++, article );
}
Public object POP ()
{Return remove (-- stack_pointer );
}
Public void push_many (object [] Articles)
{For (INT I = 0; I <articles. length; ++ I)
Push (Articles [I]);
}
}
Even a simple class like this has a problem. When a user balances inheritance and uses the clear () method of arraylist to pop up a stack:
Stack a_stack = new stack ();
A_stack.push ("1 ");
A_stack.push ("2 ");
A_stack.clear ();
This code has been compiled successfully, but because the base class does not know about the stack pointer stack, this stack object is currently in an undefined state. Next for the push () call, place the new item at index 2. (The current value of stack_pointer), so the stack effectively has three elements-the two below are garbage. (The java stack class has this problem. Do not use it ).
The solution to this annoying Inheritance Method problem is to overwrite all the arraylist methods for the stack, which can modify the array status, so overwriting the correct operation Stack pointer or throwing an exception. (Removerange () method is a good candidate for throwing an exception ).
This method has two disadvantages. First, if you cover everything, this base class should be actually an interface rather than a class. If you do not need any inheritance methods, you do not have this in implementation inheritance. Second, more importantly, you cannot allow a stack to support all arraylist methods. For example, the annoying removerange () does not work. The only reasonable way to implement useless methods is to make it throw an exception because it should never be called. This method effectively turns compilation errors into running errors. The bad way is that if the method is not defined, the compiler will output an error not found in the method. If a method exists, but an exception is thrown, you can detect call errors only when the program is actually running.
A better solution to this basic class problem is to encapsulate the data structure instead of inheritance. This is the new and improved stack version:
Class Stack
{
Private int stack_pointer = 0;
Private arraylist the_data = new arraylist ();
Public void push (Object article)
{
The_data.add (stack_poniter ++, article );
}
Public object POP ()
{
Return the_data.remove (-- stack_pointer );
}
Public void push_many (object [] Articles)
{
For (INT I = 0; I <O. length; ++ I)
Push (Articles [I]);
}
}
It has been good till now, but considering the fragile base class problem, we say that you want to create a variable in the stack and use it to track the maximum stack size within a period of time. A possible implementation may be as follows:
Class monitorable_stack extends Stack
{
Private int high_water_mark = 0;
Private int current_size;
Public void push (Object article)
{
If (++ current_size> high_water_mark)
High_water_mark = current_size;
Super. Push (Article );
}
Publish object POP ()
{
-- Current_size;
Return super. Pop ();
}
Public int maximum_size_so_far ()
{
Return high_water_mark;
}
}
This new class runs well, at least for a while. Unfortunately, this code explores the fact that push_many () runs by calling push. First, this detail does not seem to be a bad choice. It simplifies the code and you can get the version of the derived class of push (). Even when monitorable_stack is accessed through the stack reference, high_water_mark can be correctly updated.