microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.65k stars 12.44k forks source link

Proposal: delegateof #19151

Open lifenautjoe opened 7 years ago

lifenautjoe commented 7 years ago

delegateof Proposal

delegate (noun)

A person sent or authorized to represent others, in particular an elected representative sent to a conference.


Motivation

A popular and widely applied practice for creating flexible and reusable object oriented code is delegation.

Delegation involves two objects handling a request: a receiving object(delegator) delegates operations to its delegate.

An example of this is the Strategy Pattern.

Problem

In order for the delegate pattern to be successful, the delegate must be able to have access to the same context as the delegator.

While the delegator context can be passed to the delegate in the form of raw parameters (delegateOperation(delegateRequiredParam1,delegateRequiredParam2,delegateRequiredParam3)) this approach restricts the amount of responsibility that can be delegated in a nice, scalable way.

To remove such restriction, the delegate should receive a reference to the delegator object and have the possibility of accessing a wider interface than the one of a foreign caller.

Example

Given

class Player {
    protected xPos: number;
    protected yPos: number;

    constructor(private movementStrategy: PlayerMovementStrategy) {
    }

    protected startAnimation(animationId: string) {
        // Code to start an animation
    }

    protected setMovementStrategy(movementStrategy: PlayerMovementStrategy){
        this.movementStrategy = movementStrategy;
    }

    move(direction: Direction) {
        this.movementStrategy.move(this, direction);
    }
}

class WalkingMovementStrategy implements PlayerMovementStrategy {
    move(player: Player, direction: Direction) {
        player.startAnimation('someWalkingAnimation');
        switch (direction) {
            case Direction.UP:
                player.setMovementStrategy(flyingStrategy);
                break;
            case Direction.Right:
                player.xPos = player.xPos + 1;
            // Rest of cases    
        }
    }
}

class FlyingMovementStrategy implements PlayerMovementStrategy {
    move(player: Player, direction: Direction) {
        player.startAnimation('flyingAnimation');
        switch (direction) {
            case Direction.Up:
                player.yPos = player.yPos + 10;
                break;
            // Rest of cases    
        }
    }
}

interface PlayerMovementStrategy {
    move(player: Player, direction: Direction): void;
}

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

Then

const coolPlayer = new Player(new WalkingMovementStrategy());
coolPlayer.move(Direction.Up);
 Property startAnimation is protected an is only accessible within the class Player.

Things to note

  1. Changing the startAnimation member accessibility to public solves the problem but exposes a method intended for internal usage. Same goes for the other attributes xPos, yPos.

  2. The delegate must change more than 1 thing of the delegator object, hence making returning a value to apply the changes possible but ugly.

Something like this

const result = movementStrategy.move(player, direction);
console.log(result);
// { nextAnimation: 'walkingAnimation', newYPos : 2 }

And what about extensibility? What happens if a movement strategy wants to trigger multiple animations?

Solution

Add the functionality: objectA delegateof objectB

What does it do?

delegateof allows objectA to have access to the protected members of objectB.

Example

class Player{
    constructor(private movementStrategy: PlayerMovementStrategy){}
    protected startAnimation(animationId: string){
        // start the animation
    }
    move(direction: Direction){
        this.movementStrategy.move(this, direction);
    }
}

class WalkingMovementStrategy delegateof Player implements PlayerMovementStrategy{
    move(player: Player, direction: Direction){
        // No member accesibility errors yay
        player.startAnimation('yayAnimation');
    }
}

Closing thoughts

While the keyword suffices for the OOP style, I am not sure how could this be implemented for plain objects or if it should even be attempted to.

Example

const playerMovementStrategy = {
    move(player: Player, direction: Direction){
        // How to make this work here... or do we even need this?
        player.startAnimation('yayAnimation');
    }
}

Eager to hear your thoughts on it,

Joel.

Edit on member privacy

@andy-ms mentioned privacy as a concern. If we would like to retain the privacy of protected and private members we could introduce a new accessor delegated which functions as protected but allows classes which declare themselves as delegates of the class to be able to access the member.

class Player{
    constructor(private movementStrategy: PlayerMovementStrategy){}

    delegated startAnimation(animationId: string){
        // start the animation
    }

    move(direction: Direction){
        this.movementStrategy.move(this, direction);
    }

}

class WalkingMovementStrategy delegateof Player implements PlayerMovementStrategy{

    move(player: Player, direction: Direction){
        // No member accesibility errors ;-)
        player.startAnimation('yayAnimation');
    }

}
ghost commented 7 years ago

So, you want a class member to normally not be accessible, except to certain classes. This looks like a duplicate of #7692. Note that in that proposal Player would be the one responsible for declaring that WalkingMovementStrategy can access its non-public members, and not the other way around; this makes more sense to me as it means that outside code can't choose to ignore privacy simply by adding a delegateof declaration.

lifenautjoe commented 7 years ago

@andy-ms Yes.

I do not agree however that it should be the other way around as the idea of implementing such delegation pattern is not only for us to change the strategies we use but for the users to be able to do so too in an easy manner.

In the "Friend" approach in order for a user to override the default strategy they would need to extend the class and override it, which also breaks with the "privacy" of the class.

delegateof not only skips this step completely but if we would like to maintain the privacy of the class we could introduce a new accessor called delegated which functions as protected but allowing classes which declare themselves as delegates of such class to be able to access the member.

delegated startAnimation(animationId: string){
    // I can be called by delegates!
}
dleppik commented 6 years ago

I really think TypeScript could use delegate/proxy support. JavaScript has mixins, and TypeScript doesn't have a good equivalent. I'm less concerned about supporting a "Friend" pattern.

Another way to do this which feels more TypeScripty to me would be to specify a proxied behavior, similar to (Kotlin's delegation.)

A less formal syntax would be to specify a proxied object in the constructor:

interface A {
    giveMeAString: () => string
}

class B implements A {
     constructor(delegate a: A) {}
}

This would be syntactic sugar for:

class B implements A {
    constructor( private a: A) {}

    giveMeAString() { return this.a.giveMeAString(); }
}

The only methods exposed would be ones which are specified by the class definition or interface.

Another example:

class MyJQueryExtension {
    constructor(delegate $: JQuery) {}
}

Note that this isn't just like a JavaScript jQuery mixin, in that we're proxying the object rather than adding methods directly to it.

ghost commented 6 years ago

@dleppik What you're describing would seem to require a type-directed emit -- we would need to know the properties available on a in order to generate delegates to them. So it probably is out of scope of our design goals. It is possible to declare a bunch of new methods on a class without writing code for them, and then use dynamic magic to inject them:

interface A {
    m(): void;
}

// Gives 'C' all methods of 'A' without having to write their bodies
interface C extends A {}
class C {
    constructor(a: A) {
        for (const key in a) {
            const value = (a as any)[key];
            if (typeof value === "function") {
                (this as any)[key] = value.bind(a);
            }
        }
    }
}

const a: A & { name: string } = { m() { console.log("I'm the delegate, " + this.name); }, name: "Della" };
new C(a).m();
alecgibson commented 5 years ago

In case anyone else is looking for something delegate-like, I've come up with something a little bit hacky, but which plays somewhat nicely with the type system. However, it only works with properties, and not methods. But if you're looking for something pretty simple, then this might be of use.

interface IDelegatorConstructor<T> {
  new(delegateInstance: T): T;
}

function Delegator<T>() {
  class D {
    public constructor(delegateInstance: T) {
      const self: any = (<any> this);
      for (const key in delegateInstance) {
        self[key] = delegateInstance[key];
      }
    }
  }

  return <IDelegatorConstructor<T>> D;
}

class Foo {
  public constructor(public foo: string) {}
}

class Bar extends Delegator<Foo>() {
  public bar() {
    return this.foo;
  }
}

const foo = new Foo('foo');
const bar = new Bar(foo);

bar.foo // => 'foo'
bar.bar() // => 'foo'
TurboEncabulator9000 commented 11 months ago

IMO the class with the private members should be the one to decide which classes to delegate to, not the other way around. Otherwise, any class can just help itself to the "delegator's" private members.