Wickerbough
Wickerbough

Reputation: 485

Where should I place variables or methods needed by several (but not all) child classes?

From the perspective of object-oriented best practices, where should I place a variable or method needed in some children of a parent class, but not others?

Ex. Classes Button, Knob, Lever, and Switch inherit from parent class Device. Button, Lever, and Switch need a boolean isOn, but Knob does not. Where would you define isOn?

Lever and Switch need a method Throw() that toggles isOn; Button uses isOn but does not use Throw() to handle it. Does this affect your placement of isOn, and where would you define the Throw() method?

The above is purely an example; let's assume that there are distinct properties of each child class that distinguish it and that there are commonalities that make it reasonable to use the inheritence model discussed.

Upvotes: 1

Views: 174

Answers (3)

aliteralmind
aliteralmind

Reputation: 20163

When only a sub-set of sub-classes share functionality, this can be expressed with an interface that contains the methods in question, which is only implemented by the sub-classes that need them.

public interface OnOffable  {
   boolean isOn();
   void toggleOnOff();
   void turnOn(boolean is_on);
   void turnOn();
   void turnOff();
}

class Switch extends Device implements OnOffable...

If one or more of the functions is moderately complicated, you can create a static utility function that helps prevent redundant code. In this example, however, the "complicated-ness" is the need to keep the on-off state.

In this situation, you can create an OnOffableComposer which (my preference) does not implement OnOffable.

And actually, since this particular interface can be completely implemented (meaning it needs no protected or abstract function), it can actually be a "simple" implementation of it:

public class SimpleOnOffable implements OnOffable  {
     private boolean isOn;
   public class OnOffableComposer(boolean is_on)  {
      turnOn(is_on);
   }
   public boolean isOn()  {
      return  isOn;
   }
   public void turnOn(boolean is_on)  {
      isOn = is_on;
   }
   public void toggleOnOff()  {
      turnOn(!isOn());
   }
   public void turnOn()  {
      turnOn(true);
   }
   public void turnOff()  {
      turnOn(false);
   }
}

Here's how it's used:

public class Switch extends Device implements OnOffable  {
   private final SimpleOnOffable smplOnOff;
   public Switch(boolean is_on)  {
      smplOnOff = new SimpleOnOffable(is_on);
   }
   public boolean isOn()  {
      return  smplOnOff.isOn();
   }
   public void turnOn(boolean is_on)  {
      smplOnOff.turnOn(is_on);
   }
   public void toggleOnOff()  {
      smplOnOff.toggleOnOff();
   }
   public void turnOn()  {
      smplOnOff.turnOn();
   }
   public void turnOff()  {
      smplOnOff.turnOff();
   }
}

Although the composer is "simple", this all demonstrates the concept of choosing composition over inheritance. It allows for much more complicated designs than single inheritance allows.

Upvotes: 3

dev
dev

Reputation: 2979

In general, I would say that if an element of a parent class is needed in some but not all of the children then an intermediate parent should be introduced.

When defining an inheritance hierarchy, it's a logical assumption that the children of a parent should share all properties of that common ancestor. This is akin to the way a biological taxonomy would work, and it helps to keep the code clean.

So let's have a look at the objects in your system (we'll use the "is a" relationship to help us figure out the inheritance hierarchy):

Button, Knob, Lever, and Switch

Each of these might indeed be called "Devices", but when we say "devices" most people will probably think of digital devices like phones or tablets. A better word for describing these objects might be "controls" because these objects help you to control things.

Are all objects Controls? Indeed they are, so every object will have Control as a parent in its inheritance hierarchy.

Do we need to further classify? Well your requirements are to have an on/off status, but it does not make sense for every control to have on/off status, but only some of them. So let's further divide these into Control and ToggleControl.

Is every Control a ToggleControl? No, so these are separate classes of objects. Is every ToggleControl a Control? Yes, so ToggleControl can inherit from Control. Are all objects properly classified and are all parent attributes shared by all children? Yes, so we're done building our inheritance hierarchy.

Our inheritance hierarchy thus looks like this:

                 Control (Code shared by all controls)
                   /  \
                  /    \
              Knob    ToggleControl (Code shared by all controls that can also be toggled - Adds isOn)
                         \
                          \
                        Button, Lever, Switch

Now, to the other part of your question:

Lever and Switch need a method Throw() that toggles isOn; Button uses isOn but does not use Throw() to handle it. Does this affect your placement of isOn, and where would you define the Throw() method?

Firstly, "throw" is a reserved word (at least in Java), so using method names that are similar to reserved words might cause confusion. The method might be better named "toggle()".

Button should (in fact it must) use toggle() to toggle it's isOn since it is a togglable control.

Upvotes: 0

David
David

Reputation: 218960

It sounds like the wrong abstraction all around. At the very least, Knob doesn't belong with the others. I might inject a class between Device and the three closely-related devices. Perhaps something like BinaryDevice:

abstract class Device {}
abstract class BinaryDevice : Device {
    abstract void Activate();
    abstract void Deactivate();
}
class Switch : BinaryDevice {
    void Activate() { // activate the switch }
    void Deactivate() { // deactivate the switch }
}
// same for Lever, which honestly is really just a Switch with a different styling and may not even need to be a separate object
class Button : BinaryDevice {
    void Activate() { // activate the button, then immediately call Deactivate() }
    void Deactivate() { // deactivate the button }
}

Knob can also inherit from Device, but at this point there is no common functionality for a Device so it's not clear why that universal common base class is even necessary. As further functionality is added to the various devices there may indeed be common functionality to push up to the base class. Indeed, there are well established refactoring patterns for dealing with generalization like this.

With the classes above, I imagine there would be error handling for trying to invoke an action in an incorrect state. For example, it's difficult to imagine a scenario where a Button would need anybody to call Deactivate() on it, since it de-activates itself. (Though just as a real-life button can become stuck, so too can this one if the action it invokes hangs for some reason.) But in any event even the Activate() and Deactivate() on the other objects would still need to check state. You can't activate a switch that's already active, for example.

The point is, the clarity of an object model starts to make itself more apparent when terminology and real-world modeling is more carefully considered. A lot of times developers try to shoehorn terminology into a handful of common terms in order to maximize their use of things like inheritance, and unfortunately this often results in the wrong abstraction and a confused domain model.

Build your objects as they naturally exist, then look for patterns which can be abstracted into common functionality. Don't try to define the common functionality first and then force objects to fit that mold.

Upvotes: 0

Related Questions