inheritancednon-virtual-interface

Private inheritance and non-virtual interfaces


So I've been interested in D for a while now and I messed about with it a while ago. I've started to look at it again and I really do like what it is trying to achieve, but I have a qualm about one favourite C++ design option of mine... non-virtual interfaces.

What I like about this design is that it allows for pre and post condition checking, logging and resource management to happen at the "top" of the hierarchy of inheritance. This allows the designer to specify all the common functionality of a group of related classes and to break the customisable parts of the class into very small functions. It also reduces the amount of functionality that needs to be written in a sub-class. Plus, because the virtual extension points are private, it doesn't pollute the interface, or allow users to call the implementation-specific functions directly (that is really key).

Is there a way to achieve this in D?

Example in C++ (untested, uncompiled... just for illustration).

class Radio{
public:
  Radio( std::string id, Station defaultStation, RxChip chip)
    :defaultStation(defaultStation),
     id(id),
     chip(chip){
  }
  void turnOn() {
    log.trace("Radio turned on: id:[%s]", id.c_str());
    doEnableRx();
    doPostEnable();
    setToStation(defaultStation);
  }
  void turnOff(){
    log.trace("Radio turned off: id:[%s]", id.c_str());
    doDisableRx();
    doPowerOff();
  }
  void tune(){
    log.trace("Tuning");
    findAllStations();
  }
  void setToStation(Station target){
    logStationChange(target);
    doSetRxChipPassFilter(target);
  }
  void setChip(RxChip chip) {
    rxChip = chip;
  }
  RxChip getChip() {
    return rxChip;
  }
private:
  // doesn't start with "do" as this is considered a "normal" virtual function.
  virtual void findAllStations(){
    chip.setFrequency(chip.getLowFreq());
    setChipToNextTunedPoint();
    Station stat( chip.getFrequency(),  tunedStations.size() ); 
    tunedStations.push_back(stat);
  }
  virtual bool setChipToNextTunedPoint() {
    if(chip.isTuned()) {
      while( isTuned && chip.getFrequency() < chip.getHighFreq() )
        chip.incrementFreq();
    }
    while( !chip.isTuned() && chip.getFrequency() < chip.getHighFreq() ) 
      chip.incrementFreq();
    return chip.isTuned();
  }

  // "do" functions are considered mandatory extension points for sub-classes
  virtual void doEnableRx() = 0;
  virtual void doPostEnable() = 0;
  virtual void doDisableRx() = 0;
  virtual void doPowerOff() = 0;
  virtual void doSetRxChipPassFilter(Station target) = 0
  {
    //default implementation but it must be specified for use by sub-class.
    chip.setFrequency(target.getLowFreq());
    while( !chip.isTuned() && chip.getFrequency() < station.getHighFreq() ) {
      chip.incrementFreq();
    }
  }

  Station defaultStation;
  std::vector<Station> tunedStations;
  RxChip chip;

}

Solution

  • Sure. For the non-virtual member function, either make it final (so the compiler can optimize out its virtuality), or templatize it, and then it's guaranteed to be non-virtual, since template functions are never virtual. In order to templatize a function with no template parameters, just give it an empty template parameter list. e.g.

    void setChip(RxChip chip) {...}
    

    becomes

    void setChip()(RxChip chip) {...}
    

    And for the virtual function, just make it protected. At present, private and package are never virtual, so if you want a function to be virtual, you need to make it either public or protected, and by making it protected, it's not accessible by the public API. You can't go full-bore and make it private like you would in C++, but arguably, that doesn't really buy you anything anyway, since the overriding function can still be called by the class that it's in. So, all making it private does is make it so that you can't call the base class version (which is normally pure virtual / abstract anyway).

    However, I would point out that if all you want is contracts, D's in and out blocks support polymorphism. So, you may not even need NVI. At that point, you'd just have the base class function have in and out blocks with whatever in contracts and out contracts you want, and they'll get called when the derived function gets called. That just works for assertions that you want for pre and post-conditions, but it obviates the need for NVI in some cases.