I believe that there is a way to objectively define "Good" and "Bad" Object-Oriented design techniques and that, as a community we can determine what these are. This is an academic exercise. If done with seriousness and resolve, I believe it can be of great benefit to the community as a whole. The community will benefit by having a place we can all point to to say, "This technique is 'Good' or 'Bad' and we should or should not use it unless there are special circumstances."
For this effort, we should focus on Object-Oriented principles (as opposed to Functional, Set-based, or other type of languages).
I'm not planning on accepting one answer, instead I'd like the answers to contribute to the final collection or be a rational debate of the issues.
I realize that this may controversial, but I believe we can iron something out. There are exceptions to most every rule and I believe this is where the disagreement will fall. We should make declarations and then note relevant exceptions and objections from dissenters.
I'd like to take a stab at defining "Good" and "Bad":
"Good" - This technique will work the first time and be a lasting solution. It will be easy to change later and will pay the time investment of its implementation quickly. It can be consistently applied and easily recognized by maintenance programmers in the future. Overall, it contributes to the good function and lowers cost of maintenance over the life of the product.
"Bad" - This technique may work in the short term, but soon becomes a liability. It is immediately difficult to change or becomes more difficult over time. The initial investment may be small or large, but it quickly becomes a growing cost, eventually becoming a sunk cost and must be removed or worked around constantly. It is subjectively applied and inconsistent and may be a surprise or not easily recognizable by maintenance programmers in the future. Overall, it contributes to the ultimate increasing cost of maintaining and/or operating the product and inhibits or prevents changes to the product. By inhibiting or preventing change, it becomes not just a direct cost, but an opportunity cost and a significant liability.
As an example of what I think a good contribution would look like, I'd like to propose a "Good" principle:
[Short description]
[Code or some other type of example]
[Explanation of what problems this principle prevents]
[Why, where, and when would I use this principle?]
[When wouldn't I use this principle, or where might it actually be harmful?]
[Note any dissenting opinions or objections from the community here]
Separation of Concerns
While functionality can be gained by inheriting from a utility class, in many cases it can all be gained using a member of said class.
Boost.Noncopyable is a C++ class that lacks a copy constructor or assignment operator. It can be used as a base class to prevent the subclass from being copied or assigned (this is the common behavior). It can also be used as a direct member
Convert this:
class Foo : private boost::noncopyable { ... };
To this:
class Foo {
...
private:
boost::noncopyable noncopyable_;
};
Java introduced the synchronized
keyword as an idiom to allow any object to be used in a threadsafe manner. This can be mirrored in other languages to provide mutexes to arbitrary objects. A common example is data structures:
class ThreadsafeVector<T> : public Vector<T>, public Mutex { ... };
Instead, the two classes could be aggregated together.
struct ThreadsafeVector<T> {
Vector<T> vector;
Mutex mutex;
}
Inheritance is frequently abused as a code-reuse mechanism. If inheritance is used for anything besides an Is-A relationship, overall code clarity is reduced.
With deeper chains, mixin base classes greatly increase the likelihood of a "Diamond of Death" scenario, wherein a subclass ends up inheriting multiple copies of a mixin class.
Any language that supports multiple inheritance.
Any case where the mixin class provides or requires overloading members. In this case, inheritance usually implies an Is-Implemented-In-Terms-Of relationship, and an aggregate will not be sufficient.
The result of this transformation may lead to public members (e.g. MyThreadSafeDataStructure
may have a publicly-accessible Mutex
as a component).