2

Good [your_time_of_day],

Today I have been learning about composition and factory functions in javascript (es6). I understand that composition should be preferred over inheritance and have come to agree with that (at least in javascript). Then, I realised that I have a situation where I should be using composition...

Question first:

Is there a way I can change my complex, inherited structure so classes are composed of functions without having ridiculous numbers of decorators? Have I missed something about composition in general (I feel like I have)?

Situation

I have a base class AudioPlayer:

class BaseAudioPlayer {
    public track;
    protected seekBar;

    public togglePlay() {
        //
    }

    public seek(time) {
       //some seek methods using this.seekBar
    }
}

And a few player classes would extend from this like so:

class MainAudioPlayer extends BaseAudioPlayer {
    public loadTrack(track) {
        //This is horrible
        this.track = track;
    }

    public setSeekBar(seekBar) {
        //This is horrible
        this.seekBar = seekBar
    }
}

Please bare in mind I actually have a lot more methods in parent and child classes and there are many methods in some children that are not in others. Of course, there is no multiple inheritance involved but I see at some point that might become a possibility with multiple alike child players (bad!).

I could use many decorators like @playable() @seekable() etc. but then I see that, eventually, the number of mixins would become huge. I guess I could also use factory functions in a similar manner but see the same problem.

Full disclosure: I am using Angular2 and have cut back the code a lot to keep any discussion about which design pattern to use and not about an implementation in a specific framework.

Update 1:

As @JocelynLecomte commented, my question may be unclear.

  • The MainAudioPlayer (and other players) inherit from BaseAudioPlayer since all audio players must have togglePlay, seek, and a few other methods (angular2 specific so not included here).

  • Currently, there are three classes that inherit from BaseAudioPlayer: MainAudioPlayer, DetailAudioPlayer and CardAudioPlayer. In the future there may be more and each has there own specific methods.

  • Inheritance was used to avoid duplication and all players are BaseAudioPlayers. However, all players also have togglePlay and seek methods.

  • I'd like to use composition since I could see a possibility of a player that does not have a seek method or something along those lines in the future.

  • It seems to that using composition would lead to a lot of boiler plate code in all player classes and I'd like to avoid this.

    • Could have many decorators (@playable, @seekable)
    • Could use a base player service (as in @amuse 's answer) and have redundant methods.
Jacob Windsor
  • 6,750
  • 6
  • 33
  • 49
  • isn't it `class MainAudioPlayer extends BaseAudioPlayer` ? To have a good answer to your design problem, I think you should give more info on what you're trying to achieve and what are the current responsabilities of your classes – Jocelyn LECOMTE Dec 01 '16 at 13:49
  • @JocelynLECOMTE Yes, youre right. I will change it now, was simply a mistake when renaming classes from my actual code. What kind of info could I give? I explained that I am trying to use composition rather than inheritance – Jacob Windsor Dec 01 '16 at 19:20
  • 1
    why do you make a distinction between `BaseAudioPlayer` and `MainAudioPlayer` ? Do you have a lot more of inherited classes to add new features, since you mention you could end with a lot of decorators ? – Jocelyn LECOMTE Dec 01 '16 at 19:29
  • @JocelynLECOMTE please see my update – Jacob Windsor Dec 01 '16 at 21:31

2 Answers2

2

I think if you want to reuse the base method in base class,you may want to use composition instead of inheritance (ie:define BasePlayerComponent as a property of MainAudioPlayer):

class MainAudioPlayer{
    constructor(){
    this.basePlayerComponent=new BasePlayerComponent();
    }
    public loadTrack(track) {
        //This is horrible
        this.track = track;
    }

    public setSeekBar(seekBar) {
        //This is horrible
        this.seekBar = seekBar
    }

    public togglePlay() {
        this.basePlayerComponent.togglePlay();
    }

    public seek(time) {
        this.basePlayerComponent.seek(time);
    }
}
ggrr
  • 7,737
  • 5
  • 31
  • 53
  • Hmm, yes this makes sense but this seems to be a lot of boilerplate. Especially since I will have a lot of methods in the base class. Is there a more compact way to do this? – Jacob Windsor Dec 01 '16 at 19:18
1

Coming up with the best fitting composition approach for the OP's given scenario, really depends on how data is considered being hidden and accessed. The base architecture of course should be a good mix of base/sub types (classes, inheritance) and function based mixins.

Thus the approach shown with the next given example code is a direct result of what the OP did provide within the BaseAudioPlayer, a public track and a protected seekBar. Changing visibility and read write access of such properties will have a big impact of how all classes and mixins then need to be refactored accordingly.

Here is, what I have come up with so far ...

function withTrackManagement(stateValue) {          // composable fine grained behavioral unit of reuse (mixin/trait).
  var
    defineProperty = Object.defineProperty;

  // writing the protected `track` value.
  function loadTrack(track) {
    return (stateValue.track = track);
  }
  // public `loadTrack` method (write access).
  defineProperty(this, 'loadTrack', {
    value     : loadTrack,
    enumerable: true
  });
}

function withSeekBar(stateValue) {                  // composable fine grained behavioral unit of reuse (mixin/trait).
  var
    defineProperty = Object.defineProperty;

  // writing the protected `seekBar` value.
  function setSeekBar(seekBar) {
    return (stateValue.seekBar = seekBar);
  }
  // public `setSeekBar` method (write access).
  defineProperty(this, 'setSeekBar', {
    value     : setSeekBar,
    enumerable: true
  });
}


class BaseAudioPlayer {                             // base type.
  constructor(stateValue) {
    var
      defineProperty = Object.defineProperty;

    // reading the protected `track` value.
    function getTrack() {
      return stateValue.track;
    }

    function togglePlay() {
      //
    }
    function seek(time) {
      // some seek methods using `stateValue.seekBar`
    }

    // public protected `track` value (read access).
    defineProperty(this, 'track', {
      get       : getTrack,
      enumerable: true
    });

    // public `togglePlay` method.
    defineProperty(this, 'togglePlay', {
      value     : togglePlay,
      enumerable: true
    });
    // public `seek` method.
    defineProperty(this, 'seek', {
      value     : seek,
      enumerable: true
    });

  }
}

class MainAudioPlayer extends BaseAudioPlayer {     // composite type ... extended class with mixin/trait composition.
  constructor(stateValue) {
    stateValue = (
         ((stateValue != null) && (typeof stateValue == "object") && stateValue)
      || {}
    );
    super(stateValue);

    withTrackManagement.call(this, stateValue);
    withSeekBar.call(this, stateValue);
  }
}


var mainPlayer = (new MainAudioPlayer);

console.log("mainPlayer : ", mainPlayer);
console.log("mainPlayer.track : ", mainPlayer.track);

console.log("(mainPlayer.track = 'foo bar') : ", (mainPlayer.track = 'foo bar'));
console.log("mainPlayer.track : ", mainPlayer.track);

console.log("mainPlayer.loadTrack('favourit track') : ", mainPlayer.loadTrack('favourit track'));
console.log("mainPlayer.track : ", mainPlayer.track);

console.log("mainPlayer : ", mainPlayer);


class DetailAudioPlayer extends BaseAudioPlayer {   // composite type ... extended class with mixin/trait composition.
  constructor(stateValue) {
    stateValue = (
         ((stateValue != null) && (typeof stateValue == "object") && stateValue)
      || {}
    );
    super(stateValue);                              // - extending/sub-typing.

  //withSpecificBehavior.call(this, stateValue);    // - composition.
    withTrackManagement.call(this, stateValue);     //
                                                    //
  //withOtherBehavior.call(this, stateValue);       //
  }
}

class CardAudioPlayer extends BaseAudioPlayer {     // composite type ... extended class with mixin/trait composition.
  constructor(stateValue) {
    stateValue = (
         ((stateValue != null) && (typeof stateValue == "object") && stateValue)
      || {}
    );
    super(stateValue);                              // - extending/sub-typing.

  //withSpecificBehavior.call(this, stateValue);    // - composition.
    withSeekBar.call(this, stateValue);             //
                                                    //
  //withOtherBehavior.call(this, stateValue);       //
  }
}
.as-console-wrapper { max-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37