Vastly different implementations? Sounds like a bad candidate for a common abstract parent class, don't you think?
Otherwise, you have to be more precise than AwesomeUserControl
. What are the exact controls which share the same logic? What logic is shared?
As an illustration, here's a copy-paste from an answer I wrote a few days ago:
Either two classes are related (for example both Cat
and Dog
can be fed, and it takes the same steps to feed them both, except the food which will change), in which case, create an inheritance (in my example, a parent class Pet
),
Or two classes are unrelated, and just rely on the same method. For example, WebRequest
class may rely on GetSlug
ยน to find the slug of the current request; Customer
may also rely on GetSlug
to normalize the name of the customer for further search purposes. However, WebRequest
and Customer
are totally unrelated, and it makes no sense to create a common object for those classes.
Here, the solution would be to call a common method from those two classes, by instantiating the third class containing this method. For example, both RequestUri
and CustomerName
may become objects implementing ISlug
interface.
It doesn't always make sense to add functions to the base class, as suggested in some of the other answers. Adding too many special case functions can result in binding otherwise unrelated components together.
For example I might have an Animal
class, with Cat
and Dog
components. If I want to be able to print them, or show them in the GUI it might be overkill for me to add renderToGUI(...)
and sendToPrinter(...)
to the base class.
The approach you are using, using type-checks and casts, is fragile - but at least does keep the concerns separated.
However if you find yourself doing these types of checks/casts often then one option is to implement the visitor / double-dispatch pattern for it. It looks kind of like this:
public abstract class Base {
...
abstract void visit( BaseVisitor visitor );
}
public class Sub1 extends Base {
...
void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}
public class Sub2 extends Base {
...
void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}
public interface BaseVisitor {
void onSub1(Sub1 that);
void onSub2(Sub2 that);
}
Now your code becomes
public class ActOnBase implements BaseVisitor {
void onSub1(Sub1 that) {
that.makeASandwich(getRandomIngredients())
}
void onSub2(Sub2 that) {
that.contactAliens(getFrequency());
}
}
BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
base.visit(visitor);
}
The main benefit is that if you add a subclass, you'll get compile errors rather than silently missing cases. The new visitor class also becomes a nice target for pulling functions into. For example it might make sense to move getRandomIngredients()
into ActOnBase
.
You can also extract the looping logic: For example the above fragment might become
BaseVisitor.applyToArray(bases, new ActOnBase() );
A little further massaging and using Java 8's lambdas and streaming would let you get to
bases.stream()
.forEach( BaseVisitor.forEach(
Sub1 that -> that.makeASandwich(getRandomIngredients()),
Sub2 that -> that.contactAliens(getFrequency())
));
Which IMO is pretty much as neat looking and succinct as you can get.
Here is a more complete Java 8 example:
public static abstract class Base {
abstract void visit( BaseVisitor visitor );
}
public static class Sub1 extends Base {
void visit(BaseVisitor visitor) { visitor.onSub1(this); }
void makeASandwich() {
System.out.println("making a sandwich");
}
}
public static class Sub2 extends Base {
void visit(BaseVisitor visitor) { visitor.onSub2(this); }
void contactAliens() {
System.out.println("contacting aliens");
}
}
public interface BaseVisitor {
void onSub1(Sub1 that);
void onSub2(Sub2 that);
static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {
return base -> {
BaseVisitor baseVisitor = new BaseVisitor() {
@Override
public void onSub1(Sub1 that) {
sub1.accept(that);
}
@Override
public void onSub2(Sub2 that) {
sub2.accept(that);
}
};
base.visit(baseVisitor);
};
}
}
Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());
bases.stream()
.forEach(BaseVisitor.forEach(
Sub1::makeASandwich,
Sub2::contactAliens));
Best Answer
In general I prefer
composition
overinheritance
. That has several reasons:Humans are bad at dealing with complexity. And dealing with high inheritance trees is complexity. I want light structures on my brain, which I could overlook easily. The same goes for dozens of types even derived from one base class.
You are able to switch moving parts out. You could have a simple device, which does
runOperatingSystem()
, instead of making two differentdevice
-classes, you make only one and give it anOperating System
. If you want to test behavior, you could inject amockOperatingSystem
and see, if it does, what it should.You are able to extend behaviour, simply by injecting more behavioral components.
In terms of
abstraction
, you are better off, designing a generic device type: In Python the design would look like the followingYou have a generic
device
which runs anoperating system
. Every userinteraction is delegated to thisOS
. Perhaps you want to test only the browsing call dependent on any operating systen, you could easily swap it out.The next step for this design would be, to create a
configuration
-object, which takes the common parameters (CreationDate
,HardwareId
and so on). Inject thisconfiguration
and the appropriateOS
via constructor injection and you are done.You define common behaviour in a contract (
interface
), which determines, what you could do with a phone and the operation system deals with the implementation.Translated to everyday language: If you text with your phone, there is no difference in doing it with an iPhone, Android or Windows in that respect, that you are texting, although the mechanisms from OS to OS differ. How they deal technically with it is uninteresting for the device. The OS runs the
texting app
, which itself takes care of its implementation. It is all a question of abstracting commonalities.On the other hand: this is only one way of doing it. It depends on you and your model of the domain.
From the comments:
This depends on what exactly you want to model. To extend the given example of
texting
:Say, you simply have some basic jobs, you want the device to do, you define an
API
for that; in our case simply the methodsendSMS(text)
.You have then theDevice
, where the message"send text"
is called upon. Thedevice
in turn delegates that call to the used operating system, which does the actual sending.If you want to model more than a handful of
services
yourdevice
offers, you have to make bloatedAPI
.This is a sign, that your abstraction level is too low.
The next step would be to abstract the concept of an app out of the system.
In principle, you have a
device
which interacts with the inner logic of anapp
, which runs on anoperating system
. You haveinput
, which is processed and changes thedisplay
of the device.In terms of MVC, e.g. your keyboard is the
controller
, the display is theview
and there is themodel
within the app, which is modified. And since the view observes the model it reflects any changes.With such an abstract concept, you are very flexible to build / model a lot of use cases.
As said above:it is all a matter of abstraction. The more power you want, the more abstract your model has to be.
Taking the example further, you have to develop several abstractions / patterns, which help in this case.
Mediator
-Pattern: The operating system acts as a mediator, i.e. it takes signals in form ofcommands
, sends it to the app and takes in responsecommands
to e.g. update the viewCommand
-Pattern: The command pattern is the form of abstraction, which is used to describe the communication flow between components. Say, the user pressesA
, than this could be abstracted asKeypressed
-command with a value ofA
. Another command would beupdate display
with the value ofkeypress.value
or in this caseA
.MVC
The display as theview
, the keyboard as thecontroller
and in between the (app-)model.Your imagination is your limit.
This is the kind of stuff, OOP was invented for originally: simulating independent components and their interaction