Javascript: How to reuse method for creating child instances without creating circular dependencies

Viewed 323
abstract class Fruit {
    private content: Fruit[] = [];

    addChild() {
        // Pick one at random (using this as an example instead of the actual criteria that determines this)
        const type = pickOne(['apple', 'banana', 'cherry']);

        switch (type) {
            case 'apple':
                this.content.push(new Apple());
            case 'banana':
                this.content.push(new Banana());
            case 'cherry':
                this.content.push(new Cherry());
        }
    }
}

class Apple extends Fruit { }

class Banana extends Fruit { }

class Cherry extends Fruit { }

How can I restructure this without creating circular dependencies so that:

  1. Each class is in a separate file
  2. The addChild() method is available on all children without duplicating the code

I've read that it is generally a bad pattern for the base class to know anything about child classes but I'm not sure what a better pattern would look like.

Example that might make more sense

Edit: Removed type as an argument

2 Answers

You need to split the abstract class from the factory class (create new instances):

// fruit.ts
abstract class Fruit {
    private content: Array<Fruit> = [];

    addChild(child: Fruit) {
        this.content.push(child);
    }
}

// fruit-factory.ts
class FruitFactory {
    create(type: 'apple' | 'banana' | 'cherry'): Fruit {
        switch (type) {
            case 'apple':
                return new Apple();
            case 'banana':
                return new Banana();
            case 'cherry':
                return new Cherry();
        }
    }
}

// apple.ts
class Apple extends Fruit { }

// banana.ts
class Banana extends Fruit { }

// cherry.ts
class Cherry extends Fruit { }

The Factory design pattern is useful to isolate and reuse the code that creates an object.

In your more detailed example, the loadChildren does not have to be tied to the List abstract class. This makes the List abstract have 3 responsibilities: being a list, loading data from an external source and creating new entities.

A guiding principle for maintainable code is the single responsibility principle where each class have a single responsibility.

I suggest that you split your List object in 3 classes, based on the identified responsibilities: abstract class List, class ListFactory and class DataFetcher.

This will allow you to define a good interface for each of these classes.

You can simply pass the constructor instead of a string:

abstract class Fruit {
    private content: Fruit[] = [];

    addChild<F extends Fruit>(C: new (...args: any) => F) {
        this.content.push(new C());
    }
}

class Apple extends Fruit { }

class Banana extends Fruit { }

class Cherry extends Fruit { }
Related