Home > Mobile >  How could one refactor the implementation of two different types of sub classes while keeping both,
How could one refactor the implementation of two different types of sub classes while keeping both,

Time:10-29

I have a single base abstract class that expects to be extended, which has defined a sole public method that uses a functionality expected from a subclass. Here, only fullSoundSentence is meant to be "public" - to be used by the outside world.

class Animal {
    constructor(nickname){
        this.nickname = nickname
    }
    fullSoundSentence(){
        //expects this.composesound to exist
        return `${this.nickname} ${this.composesound()}!`
    }
}

I then have many classes extending it providing some core functionality (here they simply return a string but in reality they need access to this, modify properties of the object and so on)

class Dog extends Animal {
    doessound(){
        return "barks"
    }
}
class Cat extends Animal {
    doessound(){
        return "meows"
    }
}

Another family of subclasses then uses this functionality, all in a different way. Kind of "modes" in which we use the Dog and Cat functionalities. Theese modes provide the function expected by the abstract base method (fullSoundSentence), but they need to be a specific animal.

class LoudAnimal extends Animal {
    composesound(){
        return `loudly ${this.doessound()}`
    }
}
class CalmAnimal extends Animal {
    composesound(){
        return `calmly ${this.doessound()}`
    }
}

Now, both cats and dogs can be both calm and loud. = any animal can be in any mode

Q: How do i create a calm dog or a loud cat and so on?

  • I could create by hand classes for each combination (CalmDog, LoudDog, CalmCat, ...) but if there are more animals and more modes, this is terrible

  • I could make the modes just object with functionalities that the abstract Animal would expect as an argument for example, but this is messy, especially if the mode also needs to modify properties and so on

Isn't there some better way?? I'm looking for ways to do this in JavaScript or TypeScript.

CodePudding user response:

The next provided approach introduces a toolset of (a) context aware functions and function based mixins (which implement behavior that can not be provided in a meaningful way via a sub-typed class-hierarchy) and (b) an Animal base class, some more specific animal sub types and also a factory in order to show that mixin-based composition can be applied at any time during and at any level of an object creation process (or an object's lifetime).

All implemented functionality got derived from the OP's example code, decomposed and re-assembled while keeping the animal type specific property and method names.

From the OP's originally provided Animal and Dog/Cat extends Animal code, in order to justify the existence of a base Animal class, and also in order to DRY both subtypes (Dog and Cat), such a basic type has to not only feature its nickname but also its sound property and its accompanying doessound method. Dog and Cat both rely on super delegation and are good enough for just providing the default values.

The rest is up to either a mix of further sub typing and mixin composition or to e.g. a factory which does instantiate a class and does apply specific behavior to an instance via mixin composition as well.

Why is that?

Both code lines ...

class LoudAnimal extends Animal { composesound () { /* ... */ } }

... and ...

class CalmAnimal extends Animal { composesound () { /* ... */ } }

... make no sense at all.

A calm/loud animal type is nothing which should/could be described in a meaningful way like in form of an entity which features a nickname and a sound property and a doessound method whereas a dog/cat can be described and recognized by these animal specific keys.

But there can be loud dogs and/or calm cats and/or vice versa. Thus, such types need to acquire specific behavior/traits like being calm/loud or expressing itself calmly/loudly. And this is what mixins are for.

// sound-specific functions/methods (behavior) utilized by mixins.

function getBoundBehaviorDrivenSound(behavior) {
  // expects `doessound` to exist but does
  // handle it forgiving via optional chaining.
  return `${ behavior } ${ this?.doessound?.() }`;
}

function getFullSound() {
  // expects `nickname` and `composesound` to exist but
  // handles the latter forgiving via optional chaining.
  return `${ this.nickname } ${ this?.composesound?.() }!`;
}


// sound-specific function-based mixins (applicable behavior).

function withSoundingLoud() {
  this.composesound =
    getBoundBehaviorDrivenSound.bind(this, 'loudly');
  return this;
}
function withSoundingCalm() {
  this.composesound =
    getBoundBehaviorDrivenSound.bind(this, 'calmly');
  return this;
}

function withFullSound() {
  this.fullSoundSentence = getFullSound;
  return this;
}


// base-class and sub-typing

class Animal {
  constructor({
    nickname = 'beast',
    sound = 'grmpf',
  }) {
    Object.assign(this, { nickname, sound });
  }
  doessound() {
    return this.sound;
  }
}

class Dog extends Animal {
  constructor(nickname = 'Buster') {

    super({ nickname, sound: 'barks' });
  }
}
class Cat extends Animal {
  constructor(nickname = 'Chloe') {

    super({ nickname, sound: 'meows' });
  }
}


// further sub-typing and mixin based composition.

class LoudDog extends Dog {
  constructor(nickname) {

    super(nickname);

    // mixin at creation time at instance/object level.
    withSoundingLoud.call(this);

    // withFullSound.call(this);
  }
}

// factory function featuring mixin based composition.

function createFullySoundingCalmCat(nickname) {
  // mixin at creation time at instance/object level.
  return withFullSound.call(
    withSoundingCalm.call(
      new Cat(nickname)
    )
  );  
}


const smokey = createFullySoundingCalmCat('Smokey');
const cooper = new LoudDog('Cooper');

console.log({ smokey, cooper });

console.log('smokey.doessound() ...', smokey.doessound());
console.log('cooper.doessound() ...', cooper.doessound());

console.log('smokey.composesound() ...', smokey.composesound());
console.log('cooper.composesound() ...', cooper.composesound());

console.log('smokey.fullSoundSentence() ...', smokey.fullSoundSentence());
console.log('cooper?.fullSoundSentence?.() ...', cooper?.fullSoundSentence?.());


// ... one more ...

class FullySoundingLoudDog extends LoudDog {
  constructor(nickname) {

    super(nickname);
  }
}
// prototype level aggregation / "class level mixin".
withFullSound.call(FullySoundingLoudDog.prototype);

const anotherDog = new FullySoundingLoudDog;

console.log({ anotherDog });
console.log('anotherDog.doessound() ...', anotherDog.doessound());
console.log('anotherDog.composesound() ...', anotherDog.composesound());
console.log('anotherDog.fullSoundSentence() ...', anotherDog.fullSoundSentence());
.as-console-wrapper { min-height: 100%!important; top: 0; }
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

  • Related