Interface (Interface) and Class (Class)?
Once, I attended a meeting of a Java user group. At the conference, James Gosling (the father of Java) gave the initiator speech. In that memorable Q&A segment, he was asked: "If you refactored Java, what would you change?". "I want to get rid of classes" he replied. After the laughter subsided, it was explained that the real problem was not the class itself, but the implementation of the inheritance relationship. Interface inheritance (implements relationship) is better. You should avoid implementing inheritance as much as possible.
Loss of flexibility
Why should you avoid inheritance? The first problem is that explicitly using concrete class names locks you into a specific implementation, making underlying changes unnecessarily difficult.
In the current agile programming method, the core is the concept of parallel design and development. You start programming before you design the program in detail. This technique differs from the traditional approach - where design should be completed before coding begins - but many successful projects have proven that you can develop high-quality code more quickly than with traditional step-by-step methods. method. But at the core of parallel development is flexibility. You have to write your code in a way so that newly discovered requirements can be merged into existing code as painlessly as possible.
Rather than implementing features you may need, you only need to implement features you clearly need, and be moderately tolerant of change. If you don't have this kind of flexible, parallel development, it's simply impossible.
Programming for Inteface is the core of flexible structures. To illustrate why, let's take a look at what happens when they are used. Consider the following code:
f()
{
LinkedList list = new LinkedList();
//...
g( list );
}
g(LinkedList list)
{
list.add(...);
g2(list)
}
Assume a need for fast query is raised so that this LinkedList cannot be resolved. You need to use HashSet instead. In existing code, changes cannot be localized, because you need to modify not only f() but also g() (which takes a LinkedList parameter), and any code that g() passes the list to. Rewrite the code as follows:
f()
{
Collection list = new LinkedList();
//...
g( list );
}
g( Collection list )
{
list.add( ... );
g2( list )
}
Modify the Linked list like this hash, may simply use new HashSet() instead of new LinkedList(). that's all. There is nothing else 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 iterate over Collection derivation, just like you can get key-value pairs from a Map. In fact, you can write iterators that generate data instead of traversing a Collection. You can write iterators that get information from the test framework or file. This allows for tremendous flexibility.
Coupling
For implementing inheritance, a more critical issue is coupling---annoying dependence, that is, the dependence of one part of the program on another part. Global variables provide a classic example of why strong coupling can cause trouble. For example, if you change the type of a global variable, then all functions that use that variable may be affected, so all of this code will have to be inspected, changed, and retested. Moreover, all functions that use this variable are coupled to each other through this variable. That is, if a variable value is changed at a time when it is difficult to use, one function may incorrectly affect the behavior of another function. This problem is significantly hidden in multi-threaded programs.
As a designer, you should strive to minimize coupling relationships. You can't eliminate coupling altogether, because method calls from objects of one class to objects of another class are a form of loose coupling. You can't have a program without any coupling. However, you can minimize some coupling by following OO rules (most importantly, the implementation of an object should be completely hidden from the objects that use it). For example, an object's instance variables (fields that are not constants) should always be private. I mean for a certain period of time, without exception, continuously. (You can occasionally use protected methods effectively, but protected instance variables are an abomination.) For the same reason you shouldn't use get/set functions --- they just feel overly complex for being public to a domain (despite the return modifier The reason for accessing functions of objects rather than primitive values is in some cases, and in that case the returned object class is a key abstraction in the design).
Here, I am not bookish. In my own work, I find a direct correlation between the rigor of my OO approach, rapid code development, and easy code implementation. Whenever I violate central OO principles, such as implementation hiding, I end up rewriting that code (usually because the code is not debuggable). I don't have time to rewrite the code, so I follow those rules. Am I concerned about purely practical reasons? I'm not interested in clean reasons.
The fragile base class problem
Now, let’s apply the concept of coupling to inheritance. In an implementation system that uses extends, the derived class is very tightly coupled to the base class, and this tight coupling is undesirable. Designers have applied the nickname "fragile base class problem" to describe this behavior. Base classes are considered fragile because you modify the base class seemingly safely, but when inheriting from a derived class, the new behavior may cause the derived class to become dysfunctional. You can't tell whether changes to a base class are safe by simply inspecting the base class in isolation; rather, you must also look at (and test) all derived classes. Furthermore, you must check all code that is also used in base and derived class objects, because this code may be broken by the new behavior. A simple change to a base class can render the entire program inoperable.
Let’s examine the problem of fragile base classes and base class coupling. The following class extends Java's ArrayList class to make it behave 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 problems. Consider when a user balances inheritance and uses ArrayList's clear() method to pop the stack:
Stack a_stack = new Stack();
a_stack.push("1");
a_stack. push("2");
a_stack.clear();
This code compiles successfully, but because the base class does not know about the stack pointer stack, the stack object is currently in an undefined state . The next call to push() puts the new item at index 2. (the current value of stack_pointer), so the stack effectively has three elements - the bottom two are garbage. (Java's stack class has this problem, don't use it).
The solution to this annoying inherited method problem is to override all ArrayList methods for Stack, which can modify the state of the array. , so overwrite the correct operation of the Stack pointer or throw an exception. (The removeRange() method is a good candidate for throwing an exception).
This method has two disadvantages. First, if you cover everything, the base class should really be an interface, not a class. If you don't use any inherited methods, there is no point in implementing inheritance. Second, and more importantly, you cannot have a stack support all ArrayList methods. For example, the annoying removeRange() does nothing. The only reasonable way to implement a useless method is to make it throw an exception, since it should never be called. This method effectively turns compilation errors into runtime errors. The bad thing is that if the method is simply not defined, the compiler will print a method not found error. If a method exists but throws an exception, you won't find out about the calling error until the program is actually running.
A better solution to this base class problem is to encapsulate the data structure instead of using inheritance. This is the new and improved version of Stack:
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] );
}
}
So far, so good, but considering the fragile base class problem, let's say you want to create a variable in the stack and use it in a section Tracks the maximum stack size during the cycle. A possible implementation might look like this:
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 worked just fine, at least for a while. Unfortunately, this code exploits the fact that push_many() operates by calling push(). First of all, this detail doesn't seem like a bad choice. It simplifies the code, and you can get the derived version of push() even when Monitorable_stack is accessed through a Stack reference, so that high_water_mark is updated correctly.
The above is the detailed content of Inheritance example analysis in Java. For more information, please follow other related articles on the PHP Chinese website!