1

I'm actually dealing with this in Typescript, but I'll write this question in Javascript. An answer for either would be great.

I have two objects that essentially have the same interface. I'd like to create a 3rd object that has the same interface, and each function would just call the methods of the two objects in contains. Is there a way to programmatically create all the functions so I don't have to type them all out?

c = console.log;  // shorthand
obj1 = function() {}
obj1.prototype = {
  foo: (x) => { c("1" + x) },
  bar: (x, y) => { c("1" + x + y) }
}
obj2 = function() {}
obj2.prototype = {
  foo: (x) => { c("2" + x) },
  bar: (x, y) => { c("2" + x + y) }
}

obj3 = function() {
  this.o1 = new obj1()
  this.o2 = new obj2()
}
obj3.prototype = {
  foo: function(x) {
    this.o1.foo(x);
    this.o2.foo(x);
  },
  bar: function(x, y) {
    this.o1.bar(x, y);
    this.o2.bar(x, y);
  }
}

I'm looking for a way to write obj3 without having to manually write out each of the member functions, since I have a good number of them.

dragonx
  • 14,963
  • 27
  • 44

4 Answers4

2

You can use a Proxy object:

function multiproxy(objects) {
    return new Proxy(objects[0], {
        get: function(target, name) {
            var that = this;

            return function() {
                var result;

                for (let obj of objects) {
                    result = obj[name].apply(that, arguments);
                }

                return result;
            };
        }
    });
}

So in your case, you'd do:

var instance3 = multiproxy([new obj1(), new obj2()]);

Now, instance3 behaves like obj1, but calls the same methods on obj2.

This won't handle non-function properties of your objects, but that's not too difficult to add in.

Blender
  • 289,723
  • 53
  • 439
  • 496
0

You need to copy the objects like:

// Shallow copy
this.o1 = jQuery.extend({}, obj1);

or

// Deep copy
this.o1 = jQuery.extend(true, {}, obj1);

Difference between shallow copy and deep copy

Alternatively, pure JavaScript version:

this.o1 = Object.assign({}, obj1);

You need to study the details by reading Object.assign(). There could be unintended effects if you did not deep copy but shallow copy the object.

After an hour:

After many experiments I realized that the constructor function of obj3 was unable to create new objects, so I decided the move out the cloning part. From my observation this code is working as intended:

c = console.log;  // shorthand
obj1 = function() {}
obj1.prototype = {
  foo: (x) => { c("1" + x) },
  bar: (x, y) => { c("1" + x + y) }
}
obj2 = function() {}
obj2.prototype = {
  foo: (x) => { c("2" + x) },
  bar: (x, y) => { c("2" + x + y) }
}

obj3 = function() {
  this.o1;
  this.o2;
}
obj3.prototype = {
  foo: (x) => {
    obj3.o1.foo(x);
    obj3.o2.foo(x);
  },
  bar: (x, y) => {
    obj3.o1.bar(x, y);
    obj3.o2.bar(x, y);
  }
}

obj3.o1 = $.extend(true, {}, obj1.prototype); // jQuery or
obj3.o2 = Object.assign({}, obj2.prototype); // Pure JavaScript
obj3.prototype.foo(5);
obj3.prototype.bar(3,4);
Community
  • 1
  • 1
mertyildiran
  • 6,477
  • 5
  • 32
  • 55
  • 1
    And how does this help with forwarding method calls to both `o1` and `o2`? – Thilo May 12 '17 at 02:08
  • @Thilo I think deep copy should achieve that? Am I wrong? What will be the actual behavior? – mertyildiran May 12 '17 at 02:12
  • I had a bug in the original question in using => functions in obj3 that screwed up the this pointer. However, that wasn't the intent of the question. I think Blender and ハセン were getting closer to the heart of it. – dragonx May 12 '17 at 04:54
  • @dragonx what was the intend of your question, please explain more, I want to help. What do you wanna see in your console.log? – mertyildiran May 12 '17 at 04:58
  • My code works, I just want an alternative way to write obj3 where I don't have to manually write foo() and bar(). – dragonx May 12 '17 at 05:17
  • @dragonx But I'm getting `Uncaught TypeError: Cannot read property 'foo' of undefined` when I try to execute `obj3.prototype.foo(5);` using your code? Did you mean [inheritance](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)? [Inheritance in TypeScript](https://www.typescriptlang.org/docs/handbook/classes.html#inheritance). Your words defining inheritance in OOP. – mertyildiran May 12 '17 at 05:36
  • The right way to call it would be `x = new obj3(); x.foo(5);` – dragonx May 13 '17 at 10:16
0

You can access methods using dictionary syntax:

var a = "Hello world";
a.toUpperString(); // returns "HELLO WORLD"
a["toUpperString"](); // same as above

So a general approach would be to first implement a lower level method:

Say you have object a which holds references to b and c

function callMethod(a, methodName) {
    // You might want to add some error checking
    a.b[methodName]()
    a.c[methodName]()
}

Now you can call callMethod(a, "something") and it would call a.b.something() and a.c.something()

This is not a full answer but I think it should give you enough to get started.

Since prototypes are themselves plain dictionaries, you can easily list method in the prototype of one object and add methods to the prototype of another object.

ハセン
  • 377
  • 3
  • 5
0

Blender showed how this could be done with a Proxy, however I was interested in getting usable typings out of this without a ton of extra work / casting.

Here is a solution which performs in nearly the same way with the benefit of avoid problems with support for Proxy in IE / iOS 9.

function ClassMerger<T extends Object>(...classes: T[]): Partial<T> {
    // Helper to create a function that calls a method on all passed instances
    let call = (method: keyof T) => (...args: any[]) => {
        for (let instance of classes) {
            if (typeof instance[method] == 'function') {
                // Typescript's type checking fails here for some reason.
                (instance[method] as Function).apply(instance, args);
            }
        }
    }

    let merged: Partial<T> = {};

    // Loop through each class, adding all methods to the merged object
    for (let instance of classes) {
        for (let method in instance) {
            if (typeof instance[method] == 'function' && !merged[method]) {
                merged[method] = call(method);
            }
        }
    }

    return merged;
}

// Usage (with your example objects):
let merged = ClassMerger<{foo: (a: string) => void}>(new obj1(), new obj2());
if (merged.foo) {
    merged.foo('Test'); // No complaints
}
// Property 'bar' does not exist on type 'Partial<{foo: (a: string) => void}>'.
merged.bar('A', 'B');

Notes:

The function returns Partial<T> since if you have properties in your interface, they will not be added to the merged object. If your interface doesn't have any properties, you can cast the returned object to an instance of T.

This method requires that you pass instances of a class rather than creating the classes for you. This allows for a much simpler method and lets you provide classes which accept different constructor arguments easily.

Gerrit0
  • 7,955
  • 3
  • 25
  • 32