Object Oriented modelling is very subjective, but the only thing I can thing of here is the old Inheritance vs. Composition discussion:
https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose
Based on your argumentation, I believe you often try to extract a superclass from two or more classes with similar code, so they can all share the same inherited methods instead of just duplicate them. Although technically this gives you exactly what you want, you should also take care of the inheritance semantics since it will denote an is-a relationship (i.e., car is-a vehicle, dog is-a mammal, report-screen is-a read-only-screen). Since Java doesn't offer multiple inheritance, you may end up limited and confused if you class hierarchy grows.
So, before starting to extract superclasses for reuse, have in mind that you can also extract this code-i-want-to-reuse units to be part-of other classes (composition).
Sorry for my conceptual example, but here it goes:
Both Dog and Lion are mammals and hunters. They should naturally inherit Mammals superclass (with a lot of reusable code among mammals). As not all mammals hunt, we don't want to define a new hunt() method on Mammals class.
At this very point, you may be thinking of creating a new inheritance level: Mammals <- HuntingMammals. But think: if you continue doing this for every distinctive aspect of animals, you'll have dozens of classes in a tricky and puzzled hierarchy. Besides that, we also know that some reptiles and birds also hunt, so, we'd better isolate all the hunting thing elsewhere.
As a healthy alternative to inheritance, we can define a separate Hunter class. To reuse it's contents, all we need to do is put a Hunter object as member of both Dog and Lion (a field). If we need to treat dogs and lions as hunters together (polymorphically), we can define a CanHunt interface to group them.
Check the example below:
class Hunter {
void hunt(){
System.out.println("i'm hunting...");
}
}
interface CanHunt{
Hunter getHunter();
}
class Dog extends Mammals implements CanHunt{
...
Hunter hunter = new Hunter();
@Override
Hunter getHunter(){
return hunter;
}
...
}
class Lion extends Mammals implements CanHunt{
...
Hunter hunter = new Hunter();
@Override
Hunter getHunter(){
return hunter;
}
...
}
And here we have a polymorphic sample code that ask both dog and lion to do their hunting stuff:
...
List<CanHunt> hunters = new LinkedList();
hunters.add(new Dog());
hunters.add(new Lion());
for(CanHunt h:hunters){
h.getHunter().hunt(); //we don't know if it's a dog or a lion here...
}
...
I hope this simple example gives you some inspiration. And it can get quite complex if we keep evolving it towards a more detailed though flexbile design. For instance, Hunter class could be abstract with different implementations, since dog hunts differentlty from lions, but they share some common behaviour.