The most fundamental object oriented design principles are to program to an interface, not an implementation, and to encapsulate object creation from clients. These are two of the most important messages of "Design Patterns" [Gamma95], stated in chapter 1 and applied in chapter 2 and 3 (although not consequently). The primary reason for bad design in production software still lies in violation of these principles, rather than in any more advanced, fancy design issues.
If such an immutable object is based on a final class like java.lang.String, then there is nothing to fear. If it is based on an interface then there can always be clients that make a mutable extension of it! Other implementations that rely on the immutability of the passed object might start to behave in an unexpected way.
In the following example the DogUser implementation depends on the immutability of Dog:
DogUser and Dog are both part of the same library / framework. Now consider a client of this API who extends Dog:
The client can pass instances of BadMutableDog implementations to DogUser#addDog and later change the state of the passed argument. Here is an example of a client implementation that would most likely not be expected by DogUser:
DogUser dogUser = ...;
BadMutableDog d = ...;
d.setName("Dog 1");
dogUser.addDog(d);
d.setName("Dog 2");
dogUser.addDog(d);
Now DogUser has only one reference to a Dog
named "Dog 2" if he uses some kind of set (no duplicate
elements), or two, both named "Dog 2", if its container
allows duplicate elements. In many cases this may not be what the
API or the client of the API had in mind (in many cases it will be
exactly what they had in mind -> no problem there, no discussion
here).
public void addDog(Dog d) {
d = make copy of d;
add d to dogs;
}
This solution combines all advantages of immutable objects with the advantages of using only interface types. Its major drawback is that in most common object oriented programming languages such a contract cannot be checked by the compiler. It can only be documented. If clients violate the contract anyway then the results can be strange, problems might occur sporadically and might be hard to debug.
In most cases in which the desire for adding a method to a type arises, it is one of the following situations:
Vector3D (linear
algebra) needs an additional method to support multiplication
with a matrix, additionally to the ones it already has, like
multiplication with a scalar or with an other vector. In that
case it is not really necessary to add a method. An other type
can be introduced which performs the computation, e. g. a
method in GeometryTools:
public static Vector3D multiply(Matrix m, Vector3D v)
Generally it makes sense that an interface either declares one (at least one) method or that it inherits methods from two (at least two) other interfaces. I follow the general reasons against "marker interfaces": In most cases it is metadata. A marker interface is not an appropriate way to define such metadata. In Java 5.0 for example one would better use an annotation. If the language provides no such way to specify metadata then it is still better to do so in the code documentation instead of using a marker interface.
A class should generally implement exactly one interface if it can be instantiated, zero otherwise. I don't see a fundamental violation of a principle by implementing more than one interface, but usually there is no justification, no need to do so.
Concrete inheritance (inheriting from a class) should be used far less than it is today. An interface inheriting from one or more interfaces is a fundamental construct in an object oriented design, but a class extending an other class is not. It is just a convenience construct most of the time. Strictly spoken, it violates encapsulation: The class is bound to be implemented by inheriting from an other class for all times. There are only very few - if any - good uses of subclassing. Using abstract classes indicates a design flaw in most cases. Hardliners don't use abstract classes at all, and they make all their classes final.
UnsupportedOperationException. It would not implement
any interfaces, and all its methods would be static.
public final class PersonFactory {
private static final class PersonImpl implements Person {
//...
}
private PersonFactory() {
throw
new UnsupportedOperationException("no instances should be created");
}
public static Person createPerson(String name, int age) {
return new PersonImpl(name, age);
}
}
In doing so an API does not expose to its clients when, how and at what quantity objects are created.
All reasons for choosing this design as the general approach rather than only where it appears to solve a concrete problem at hand had been provided in "Design Patterns" [Gamma95] 1995 already. (They did not consequently apply this principle in their examples and patterns, though.)
Map<String,Term>. Now it is good that a
single static factory method decides when instances are
created, and how many of them are created.
List implementations have been designed in
such a way that by concatenating two existing
Lists the references inside the source lists
must be copied to the target list. If static factory
methods for various purposes were exposed by the API
instead of public constructors (and their classes) it
would be easy now to change the implementation. (This
would only be a good idea if there were separate types for
an immutable list and a mutable list, which would be
good for this and other reasons.)
MyClass x = new MyClass(); // argh!
MyType x = new MyClass(); // slightly better
MyType x = MyFactory.createX(); // good
Even if an interface type is used (MyType
x;) the client still programs to an implementation
when he specifies the class to be used explicitly (x = new
MyClass();). This principle ("Encapsulate
instance control") is a consequence of the one mentioned
previously ("Program to an interface").
public final class ButtonFactoryCreator {
private ButtonFactoryCreator() {
throw
new UnsupportedOperationException("no instances should be created");
}
private static final class ButtonFactoryImpl implements ButtonFactory {
private static final class ButtonImpl implements Button {
private final String text;
public ButtonImpl(String text) {
this.text = text;
}
public String getText() {
return text;
}
//...
}
public Button createButton(String text) {
return new ButtonImpl(text);
}
}
public static ButtonFactory createButtonFactory() {
return new ButtonFactoryImpl();
}
}
public interface ButtonFactory {
public Button createButton(String text);
}
public interface Button {
public String getText();
// ...
}
The same structure would apply for other GUI elements like
windows, menu bars and so on.
If we now want to add an additional look and feel, we would
expose an additional version of
ButtonFactoryCreator, e. g.
BrightButtonFactoryCreator. The two exposed
interfaces, ButtonFactory and Button,
would not change.
In this particular example scenario, it might make sense to enforce that the different widget types cannot be mixed. This is not a difficult task, but it is beyond the scope of the thesis.
That requires to expose an abstract, not purely virtual class to clients. This has many apparent drawbacks, which are accepted for the benefits this pattern provides:
By exposing a not purely virtual class instance control is given to the client. The client decides when instances are created, and he does not have much of a choice either. For each distinct behaviour of the Factory Method an appropriate class would be created and instantiated. (The client can work around that by implementing the abstract method in such a way that it calls a method on a type which can be changed using an other method the client adds to the abstract class. That is similar to the alternative I suggest, only that it is unnecessarily complicated, because this approach would solve the initial problem, which was supposed to be solved by the pattern itself.) This is only one of the many drawbacks which are a consequence of the violation of the principles described above.In most situations where this pattern would be used there is a better alternative: The operation that relies on a product to be created by a client (or its object, e. g. by constructor parameter) can be parmaterized by an Abstract Factory type which has the FactoryMethod() to return the product. This way provides more flexibility by exposing less.
To stick with our abstract button example, consider a class that
needs a way to create a button for its own internal use, but
wants clients to specify the class of the button. In the classic
Abstract Factory method this class would have an abstract
method, createButton, which clients would implement. The far
better alternative is to provide a way for clients to give it a
reference to the ButtonFactory:
The "Applicability" section in "Design Patterns" goes too far: For the situation that "a class can't anticipate the class of objects it must create" the Factory Method is not the right pattern to be used. As for the other applicability statements, it failed to describe the actual (higher level) requirements, which have been misinterpreted to "a class wants its subclasses to specify the objects it creates", which is a requirement defect (see the principles this article - and also the GoF book itself - is based on).
I have seen this design for the first time in the ContractualJ API.
Bloch01: "Effective Java - Programming Language Guide"; Joshua Bloch; 2001 Sun Microsystems
Copyright © - 2005 - Kai Witte. All Rights Reserved. Alle Rechte vorbehalten.