Home > Software design >  How can handle multi methods for JavaScript methods chain?
How can handle multi methods for JavaScript methods chain?

Time:03-09

suppose that, I've this Object for methods chain:

let list = {
  numbers: [],
  create: function(list) {
    this.numbers = list;
    return this;
  },
  asc: function() {
    return this.numbers.sort((a, b) => a - b);
  },
  desc: function() {
    return this.numbers.sort((a, b) => b - a);
  },
  unique: function() {
    return this.numbers.filter((item, i, source) => source.indexOf(item) === i);
  }
}

let new_list = list.create([10, 2, 2, 3, 1, 2, 4]).unique();
console.log(new_list);

but how can handle this chain:

list.create([10, 2, 2, 3, 1, 2, 4]).unique().asc();

I actually want to handle it for non-limited chain... but it works for only one method for now!

CodePudding user response:

  • You need to understand how state works in OOP, in addition to JavaScript's (somewhat unorthodox) this parameter.

    • As well as how some methods of Array (or Array.prototype) in JS will mutate in-place (like sort) while others treat this (the Array) as immutable and instead always return a new array (e.g. filter and map).
  • If you're defining a reusable object type (with properties and methods/functions) then you should define a class instead.

  • BTW, I assume this is just a learning-exercise, otherwise there really isn't a reason to reimplment a list type in JavaScript as Array does it all already.

  • As you want method-chaining (as a builder-pattern, I guess?), it's important to ensure you don't mutate this.numbers in-place, so only call Array.prototype.sort() on a new copy:

Something like this:

class MyNumbersList {
    constructor( arr ) {
        if( !Array.isArray( arr ) ) throw new Error( "Argument `arr` must be an array." );
        this.numbers = arr;
    }

    // Array.prototype.sort will mutate `this.numbers` in-place.
    asc() {
        const copy = Array.from( this.numbers );
        copy.sort((a, b) => a - b);
        return new MyNumbersList( copy );
    }
    desc() {
        const copy = Array.from( this.numbers );
        copy.sort((a, b) => a - b);
        return new MyNumbersList( copy );
    }

    // But `filter` returns a new array.
    // Use a `Set` instead of `indexOf`, otherwise you'll have `O(n*n)` complexity, which is horrible:
    unique() {

        const valuesSet = new Set( this.numbers );
        const copy = this.numbers.filter( n => valuesSet.has( n ) );
        return new MyNumbersList( copy );
    }

    // This is a property getter, so you don't need to use `()` to invoke it.
    // To ensure encapsulation (i.e. to prevent unwanted mutation of `this.numbers`) always return a by-value copy (using `Array.from`), instead of exposing `this.numbers` directly:
    get values() {
        return Array.from( this.numbers );
    }
}

Used like so:

const x = new MyNumbersList( [ 1, 2, 2, 3 ] );
const t = new MyNumbersList( [ 3, 2, 2, 1 ] );
console.log( x.values ); // "1, 2, 2, 3"
console.log( y.values ); // "3, 2, 2, 1"
console.log( x.desc() ); // "3, 2, 2, 1"
console.log( y.asc() ); // "1, 2, 2, 3"
console.log( x.desc().unique().values ); // "3, 2, 1"

All of the "modern" members of Array.prototype do not mutate an array instance in-place (like map and filter), however certain older members dating back to the first editions of JS are considered "Mutator Methods" which require you to be careful.

They're listed in this page. MDN used to have a page listing them too but it seems to be gone now.

CodePudding user response:

but how can handle this chain:

list.create([10, 2, 2, 3, 1, 2, 4]).unique().asc();

In order to chain methods like that, each method (at least before the last method in the chain) must return an object that has the next method on it. Your unique method returns an array, not the list object, so it doesn't have an asc method. In order to have that chain, you'd need unique to return something with asc.

In this specific example, you might use an augmented array. You could directly augment an array instance, but for chains like you're describing, you probably want multiple instances of list (not just one), so that suggests an Array subclass:

class List extends Array {
    static fromList(list) { // Requires either no argument or an iterable argument
        const fromList = new this();
        if (list) {
            fromList.push(...list);
        }
        return fromList;
    }

    asc() {
        const fromList = this.constructor.fromList(this);
        fromList.sort((a, b) => a - b);
        return fromList;
    }

    desc() {
        const fromList = this.constructor.fromList(this);
        fromList.sort((a, b) => b - a);
        return fromList;
    }

    unique() {
        return this.constructor.fromList(new Set(this));
    }
}

const new_list = List.fromList([10, 2, 2, 3, 1, 2, 4]).unique().asc();
console.log(new_list);

That example always creates a new instance of List for each operation, but you can also do it where it mutates and returns the original list:

class List extends Array {
    static fromList(list) { // Requires either no argument or an iterable argument
        const fromList = new this();
        if (list) {
            fromList.push(...list);
        }
        return fromList;
    }

    asc() {
        return this.sort((a, b) => a - b);
    }

    desc() {
        return this.sort((a, b) => b - a);
    }

    unique() {
        const set = new Set(this);
        this.length = 0;
        this.push(...set);
        return this;
    }
}

const new_list = List.fromList([10, 2, 2, 3, 1, 2, 4]).unique().asc();
console.log(new_list);

Alternatively, you might use a builder pattern where you store up the things to do and then have a "go" terminal operation that does all the steps.

Note: Creating augmented array classes like List is fine, but beware that the subclass needs to handle all the signatures of the original Array constructor, which is...eccentric. If you pass new Array a single argument that's a number, it creates a sparse array with that length and no elements in it. If you pass it a single argument that isn't a number, it creates an array with that value as its only element. If you pass it multiple arguments, it creates an array with those elements in it. That's why I used a static fromList method rather than writing our own List constructor.

CodePudding user response:

You could implement a function which returns the numbers and all other functions to return this for chaining.

This approach does not prevent the content to be overwritten from other calls, because of having only one object, with one data set at the same time.

let list = {
  numbers: [],
  create: function(list) {
    this.numbers = list;
    return this;
  },
  asc: function() {
    this.numbers.sort((a, b) => a - b);
    return this;
  },
  desc: function() {
    this.numbers.sort((a, b) => b - a);
    return this;
  },
  unique: function() {
    this.numbers = this.numbers.filter((item, i, source) => source.indexOf(item) === i);
    return this;
  },
  values: function() {
    return this.numbers;
  }

}

let new_list = list.create([10, 2, 2, 3, 1, 2, 4]).unique().values();

console.log(new_list);

To overcome this problem, you could use a class with methods, where every instance keeps the own data.

class List {
    constructor (numbers = []) {
        this.numbers = numbers;
    }

    asc() {
        this.numbers.sort((a, b) => a - b);
        return this;
    }

    create(numbers) {
        this.numbers = numbers;
        return this;
    }

    desc() {
        this.numbers.sort((a, b) => b - a);
        return this;
    }

    unique() {
        this.numbers = this.numbers.filter((item, i, source) => source.indexOf(item) === i);
        return this;
    }

    values() {
        return this.numbers;
    }
}

let new_list = new List([10, 2, 2, 3, 1, 2, 4]).unique().values();

console.log(new_list);

  • Related