What is the difference between a has-a relationship and an is-a relationship, and how do you decide which one you have?
An is-a relationship is a type relationship. It says that one thing can be used wherever the other is expected, which is exactly the substitutability guarantee that inheritance provides. A has-a relationship is a possession relationship. It says that one object holds another and delegates work to it, which is composition.
public class Main {
public static void main(String[] args) {
Car car = new Car(new Engine("v8"));
car.start();
System.out.println(car.engine().type());
}
}
class Engine {
private final String type;
Engine(String type) { this.type = type; }
void start() { System.out.println("engine running"); }
String type() { return type; }
}
class Car {
private final Engine engine;
Car(Engine engine) { this.engine = engine; }
void start() { engine.start(); }
Engine engine() { return engine; }
}The car is the clear has-a case. It holds an engine, the engine has its own lifecycle, and the car delegates starting to it. A car is not an engine, and a code path that accepts a car cannot accept an engine. Nothing about the car's type hierarchy promises it behaves like an engine.
The test for is-a is substitutability, and it is a strong test. Ask whether the child could replace the parent everywhere the parent is used, without any code breaking. If yes, you have an is-a and inheritance is appropriate. If the answer is no, or if it needs a cast or a type check, then it is not really an is-a.
The trap to warn about is a hierarchy built on the word is loosely. A manager is-a employee in one direction and an employee is-a person, but a manager is not always interchangeable with every employee. When the substitutability test fails, model the relationship with a has-a instead, and let the objects collaborate.