My design principles
This old text from around 2007 documents my journey back then. While I don't do things in exactly in that way anymore, it might be of interest to somebody out there.This document does not introduce any generally new ideas about software design. It shows my personal focal points: Where I differ from most people and what I think should be done differently in most software projects.
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.
Program to an interface
One of the principles that is quoted often but rarely followed is "program to an interface, not an implementation".Consequences
A consequence of this principle is that all types should be interface types rather than concrete or abstract class types. (This applies to languages with an explicit interface type like Java or C#, but it can easily be derived to other programming languages.) This includes the type of a reference during declaration, a return type or a method parameter type. The only exception to this rule is when you have to adjust to an API that violates the rule already. This exception applies to almost any rule in this text.Common justifications for not using an interface type
"There will always be only one implementing class. We can as well use it as the type."
General problems with this way of thinking are:- A design where it seems to be evident that there will always be only one implementation of an interface might indicate that the design is not a good, object oriented design in that it uses inheritance, encapsulation and polymorphism as a primary basis to solve problems.
- There is absolutely no doubt that software evolves, changes, and that situations occur that are not exactly predictable. However, how to design in such a way that those unexpected changes can well be integrated into the existing design is well known these days, paid with the blood of thousands of software developers and software projects that had to be redesigned. And using a concrete class type instead of an interface type, maybe with the thought that this can still be changed in a later version, is one of the common reasons for the need of a total redesign (see [Gamma95], Chapter 1, "Designing for Change").
- Only at a first glance it seems like the approach of using a class type unless one "needs" an interface is a less strict way of following the principle of programming to an interface. Actually it is the exact opposite. In situations where the use of a certain pattern like a Strategy mandates the use of an interface type using an interface type is implicit. The rule does not mean this case. It is there for those situations where without the experiences made in the early OO era about why projects typically fail it would not be obvious that using an interface type will pay off later.
"The API is exposed only to myself / our team."
If someone (a person, a development team, ...) is his own client, then this changes nothing about the rightness of following the proven principles of good object oriented software design. The quantity of the damage done by violating them is lower in that case, but the quality of damage is still the same: Wanting to violate the principles might be an indication that the design is not a good, object oriented design in that it uses inheritance, encapsulation and polymorphism as a primary basis to solve problems, and the need for a complete redesign might be the consequence, rather than a smooth evolution of the software.Only classes can truly be immutable
In many situations it is appropriate to use immutable objects (see Bloch01, Item 13: "Favor Immutability" - highly recommended!). Immutable objects are simple, thread-safe, can be shared freely. Thus they can well be used as building blocks for other objects without much things to be considered.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).
Solutions to the problem of broken immutability
Use an immutable final class type
This solves the problem at hand. It is the safest, compiler-enforced guarantee that clients will use the type as intended. Classes that take a final class Dog can rely on its immutability. The disadvantages are ... well, this is what the entire article is about.Make a defensive copy of the passed parameter
This means not to rely on the immutability of Dog, and not reusing the instance. Safe, but all the advantages of using immutable objects are gone. In most cases, this defensive copy will be redundant.public void addDog(Dog d) { d = make copy of d; add d to dogs; }
Make a contract for clients to follow
The provider of the Dog interface could specify in the documentation that no mutable subinterface is to be used for certain other parts of the API. Then clients can use an other way of making a mutable version of Dog, or it can be provided along with the API. Generally this common problem indicates that it is often best to make the mutable companion type of the immutable type not as a subinterface of the immutable version (as the BadMutableDog above) but rather as something that implements all its interface, plus some interfaces for the mutability:
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.
Only abstract classes allow the addition of methods later (Java / C#)
This argument is correct, but overestimated. (You should be very well familiar with it, because there is a lasting discussion and plenty of dispute about it. See for example [Bloch01] or the interview of artima with Erich Gamma.)In most cases in which the desire for adding a method to a type arises, it is one of the following situations:
-
The value can be computed from return values of other methods
of the same type. Example: A
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)
- It is totally independent from the other methods of the type and expresses an additional attribute to the state. For example in my banking software it might be a problem in 3050 that a bank ID and an account ID are not good enough to identify an account; there has to be a planet ID as well now. Sorry customer, didn't foresee that! But how would it help now to have Account as an abstract class instead of an interface? Sure, we could add a getPlanetID()-method to it without breaking clients. But clients would have their own implementations of the interface / abstract class already, and they would not magically change to create the Account with the correct planet ID anyway. So in this case we have a problem either way.
Less fundamental implications
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.
Encapsulate instance control
Basic instance control
An API should expose no instantiable classes. Static factory methods should be exposed instead of public implementations and public constructors. A typical factory class in Java would be public or package level, final, and have a private constructor that throwsUnsupportedOperationException
. 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.)
-
It is typical that a software evolves in such a way that it
becomes necessary to change the way instances are created.
Examples:
-
Let there be a term interpreter which interprets terms
like "3*x+7" to an int value, given several different
x-values. When performance becomes important at a later
version it might pay off to compile the expression to a
state machine and cache those instances in a
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. -
In the collection framework of the Java core API the
List
implementations have been designed in such a way that by concatenating two existingList
s 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.)
-
Let there be a term interpreter which interprets terms
like "3*x+7" to an int value, given several different
x-values. When performance becomes important at a later
version it might pay off to compile the expression to a
state machine and cache those instances in a
-
The alternative would be not only to expose a constructor, but
also its class. Thus clients of an API would be bound to a
particular implementation. Future changes are now
complicated.
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").
Further references
See "Effective Java" [Bloch01], Item 1 for a discussion of this topic where the conclusion differs from mine.Abstract Factories
In many situations it pays off to further abstract the creation process. The static factory method would then not directly return the product. Instead it would return the actual factory to return the product (see also Gamma95, "Abstract Factory" and "Factory Method"). The static factory method would not take the parameters for the object to be created. Those parameters are instead being passed to the create method of the factory instance it returns. The benefits provided by this way support the thesis that extensive use of static methods can be a symptom for not really using an object oriented design. For object creation it is still better to expose a static factory method than to expose a constructor, but at least this method takes no parameters, as it does not create the product itself.
Example
For a GUI framework, let there be buttons, windows and things like that. It is not at all unlikely that alternative look and feels are being introduced later. Depending on the look and feel that is selected, it would be necessary to use a different method to create the button, window and so on. This check for the current look and feel would be distributed all over the client code that uses this API. With the additional abstraction described above, clients can create the appropriate factory in one place, and then use this factory to create the buttons, windows and so on.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.
General Applicabiliy
There is a lot of dispute about when this design should be applied: Only when the concrete need for different product factories is there, or also when this need might arise in the future? And is the latter always the case; is it always possible that this additional abstraction might become necessary later? This discussion is similar to the one about when to use an interface as the type (see above).Patterns
This variant of an Abstract Factory is the best basis for many creational patterns.Abstract factories as method arguments
Passing an abstract factory to a method is an alternative to the Factory Method described in "Design Patterns" (Gamma95). The Factory Method pattern utilizes a callback method that relies on an abstract, not purely virtual class.
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).
Singleton
The problem with the Singleton pattern is that while it does at least not expose a constructor, it exposes an instantiable class. Replace it with a static factory method or with an abstract factory. (The method that returns the actual product would then only create it once and always return the same. Wether this is part of the documented contract of the API or just the current implementation depends on the requirements.)Further References
In "Design Patterns" (Gamma95) there is a similar discussion in the chapters about the Abstract Factory pattern and the Factory Method pattern. I just disagree with a lot of what they write, which is why I decided for a fresh start in my arguments instead of building upon that. Those chapters are more complete, though.I have seen this design for the first time in the ContractualJ API.
References
Gamma95: "Design Patterns - Elements of Reusable Object-Oriented Software"; Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides; 1995 Eddison-WesleyBloch01: "Effective Java - Programming Language Guide"; Joshua Bloch; 2001 Sun Microsystems
Copyright © - Kiel, Germany 2006 - 2023 - Kai Witte. All Rights Reserved. Alle Rechte vorbehalten.