stampit-org / stampit

OOP is better with stamps: Composable object factories.
https://stampit.js.org
MIT License
3.02k stars 102 forks source link

Implementing the Decorator Pattern #340

Closed hbarcelos closed 5 years ago

hbarcelos commented 5 years ago

I managed to implement the something that resembles the Decorator Pattern using composers:

const MyStamp = stampit({
  name: 'MyStamp',
  init({ foo }, { stamp, args, instance }) {
    const { configuration, deepConfiguration } = stamp.compose;
    this.foo = foo;
  },
  methods: {
    greet({ greeting = 'Hi' } = {}) {
      console.log(`${greeting} from ${this.foo}`)
    },
  },
  props: { foo: null },
});

const MyStampDecorator = stampit.compose(MyStamp, {
  name: 'MyStampDecorator',
  composers({ stamp }) {
    const { greet } = stamp.compose.methods;

    Object.assign(stamp.compose.methods, {
      greet(...args) {
        greet.apply(this, args);
        console.log('this is new!!!');
      }
    })
  }
});

const instance = MyStampDecorator({ foo: 'bar' });
instance.greet({ greeting: 'Hello' });
// yields:
// Hello from bar
// this is new!!!

However, as stated in the docs:

Composers are a very powerful feature. If you can implement something without composers then it's a good idea to avoid them.

It's also possible to achieve the same behavior with init:

const MyStampDecorator = stampit.compose(MyStamp, {
  name: 'MyStampDecorator',
  init(_, { stamp }) {
    const { greet } = stamp.compose.methods;

    Object.assign(stamp.compose.methods, {
      greet(...args) {
        greet.apply(this, args);
        console.log('this is new!!!');
      }
    })
  }
});

I'm new to Stamps, but AFAIK, the difference between the two implementations is that the former will be more memory efficient, since it will modify the [[Prototype]] of the instance at the time of composition, while the later will create a new object with the greet method on each instantiation.

Am I right about this???

Is there any other/recommended way to implement the Decorator Pattern using stamps?

Any ideas on how to generalize decorating stamp methods?


Edit:

I found out that my implementation using composers does not work as expected if I do another composition:

const AnotherStamp = stampit.compose(MyStampDecorator);

const instance = AnotherStamp({ foo: 'bar' });
instance.greet({ greeting: 'Hello' });
// yields:
// Hello from bar
// this is new!!!
// this is new!!! <-- this second log shouldn't happen 

If I use init instead, it works just fine.

koresar commented 5 years ago

Hi Henrique.

You did it all right. That's the right way to implement the decorator pattern. To fix the little bug you can just mark your wrapper function with a flag or something.

const GreetDecorator = stampit.composers(({ stamp }) => {
    const { greet } = stamp.compose.methods || {};
    if (!greet || greet.wrapped) return;

    function greetWrapper(...args) {
      greet.apply(this, args);
      console.log('this is new!!!');
    }
    greetWrapper.wrapped = true;
    stamp.compose.methods.greet = greetWrapper;
  }
});
koresar commented 5 years ago

Side tip. You can make the method decorator stamp more universal and reusable via static methods.

const MethodDecorator = stampit.statics({
    decorateMethod(name, func) {
      return this.composers(({ stamp }) => {
        const method = stamp.compose.methods || stamp.compose.methods[name];
        if (!method || method.wrapped) return;

        function methodWrapper(...args) {
          method.apply(this, args);
          func.apply(this, args);
        }
        methodWrapper.wrapped = true;
        stamp.compose.methods.method = methodWrapper;
      }
    })
});

And usages:

MyStamp = MyStamp.compose(MethodDecorator).decorateMethod("greet", function () {
  console.log('this is new!!!');
});

The above code is not tested :)

koresar commented 5 years ago

Closing this issue considering all questions are answered. Feel free to reopen any time.

And thanks for the code. :) It sounds like a new Fun With Stamps episode.

hbarcelos commented 5 years ago

Hi @koresar. Sorry for the long delay, things have been pretty crazy this year.

Your suggestions helped me a lot. I think I managed to pull a implementation that works very well for most use cases.

I was about to start writing some documentation before publishing it. As soon as I finish it, I'll post it here.

Thank you very much.

koresar commented 5 years ago

No worries at all mate! I need nothing from you. Just be happy! 😀

On Mon., 16 Sep. 2019, 09:04 Henrique Barcelos, notifications@github.com wrote:

Hi @koresar https://github.com/koresar. Sorry for the long delay, things have been pretty crazy this year.

Your suggestions helped me a lot. I think I managed to pull a implementation that works very well for most use cases.

I was about to start writing some documentation before publishing it. As soon as I finish it, I'll post it here.

Thank you very much.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/stampit-org/stampit/issues/340?email_source=notifications&email_token=AAMMEL72CQOHUL5UBS4LJ23QJ25RNA5CNFSM4GTDLBQ2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6X24PQ#issuecomment-531607102, or mute the thread https://github.com/notifications/unsubscribe-auth/AAMMEL5M36GI3GNBWTHE4HDQJ25RNANCNFSM4GTDLBQQ .