Thinking about OOP in this way places the wrong emphasis. Even if you fix your initial mishap of having a Vehicle with a car and a bike inside of it, the 'improved' model of the Vehicle superclass with the wheel and bodyColor fields is probably broken (depending on the actual domain you are working in). For instance bike wheels and car wheels are very different, and a sleigh is a vehicle without any wheels. Things that look similar at first glance are not always good candidates to pull up into a super class.
The problem is that we are tempted to assume that if two examples of a category share some common traits, that these traits are present for all the members of this category (BTW this tendency to generalize might also be the cause of many problems in society).
The power of OOP does not come from extracting super classes like the Vehicle and 're-using' some fields. Direct subclassing is a very drastic measure that should not be applied lightly, because it means all present and future subclasses MUST inherit the complete contract (data and behavior) of the super class. As soon as you want to add a new subclass and are forced to make exceptions you know the model is wrong. And at that point changing it will impact all the existing sub classes. In fact, direct subclassing is nearly always a violation of the open-closed principle, the second in the set of SOLID design principles.
A much more flexible approach is to extract interfaces to encapsulate certain aspects of a category of classes:
interface Wheeled {
String getWheels();
}
class Car implements Wheeled {
private String wheels;
@Override
public String getWheels() {
return wheels;
}
}
class Bike implements Wheeled {
private String wheels;
@Override
public String getWheels() {
return wheels;
}
}
This would allow treating Cars and Bikes the same, if you are only interested in wheels:
List<Wheeled> wheeledVehicles = new ArrayList<>();
wheeledVehicles.add(new Car());
wheeledVehicles.add(new Bike());
String firstWheels = wheeledVehicles.get(0).getWheels());
If you are interested in body color, or even wheels AND body color, you are free to play with interfaces:
interface Wheeled {
String getWheels();
}
interface Coloured{
String getBodyColour();
}
interface WheeledAndColoured extends Wheeled, Coloured {}
Which allows:
List<WheeledAndColoured> wheeledAndColouredVehicles = new ArrayList<>();
wheeledAndColouredVehicles.add(new Car());
wheeledAndColouredVehicles.add(new Bike());
WheeledAndColoured first = wheeledAndColouredVehicles.get(0);
String firstWheels = first.getWheels());
String firstColour = first.getBodyColour());
If you really want to, you can extract the wheels field into a super class, but the immediate gain is limited. Private fields are an implementation detail, and pulling them into a super class is certainly not the type of re-use that will make a big difference. At the same time, it would create a very strong dependency between those classes and make future changes harder.
Maybe if you need to implement some 'complex' shared logic related to wheels, it is time to create the Wheels class:
class Wheels {
private String type;
private int number;
private BigDecimal price;
public BigDecimal getReplacementCosts() {
return price.multiply(BigDecimal.valueOf(number));
}
}
Now the Car class can collaborate with the Wheels class as one of its fields:
class Car implements Wheeled {
private Wheels wheels;
@Override
public String getWheels() {
return wheels.getType();
}
public BigDecimal getMaintenanceCosts() {
return wheels.getReplacementCosts();
}
}
Notice that, because we didn't tie in Car and Bike with a common super class, we are not forced to change either the Wheeled interface, nor the Bike class. If you want to add the Wheels logic to Bike then you can easily do so, but you are not forced to. You would be, if the wheels fields was in the shared super class Vehicle.
So the motto is: Favor collaboration over extension because it's way more flexible.