Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.8 #87

Open
creynders opened this issue Jan 27, 2015 · 7 comments
Open

v0.8 #87

creynders opened this issue Jan 27, 2015 · 7 comments

Comments

@creynders
Copy link
Contributor

So I started on the next minor/major version. I'd been thinking about 0.7.2 and what needed to be done: updating the tests, documentation and a number of patches here and there to get things better, until I suddenly realised I should be focusing on getting the next major version out instead of trying to modify 0.7 towards what I'd really want.

So, here's what's happening: I'm trying to

  • drop the hard dependency on backbone. ATM it looks like if Backbone's present then a number of sugary things and functionality will be added, but if it's not things will still work as they should, except maybe you'll have to do one or two things manually.
  • make it event framework agnostic. This one's rather tough and obviously not entirely possible. However I have a few ideas on how to approach this.
  • shy away from MVC concepts in the API. This means: no longer wireView or wireCommand. Of course it will still be possible to wire views, commands et cetera. But just as we have no wireModel I think it's beneficial to make Geppetto a more flexible library which can be used in a MVC, MVVM, MyOwnCustomWhateverConcept w/o having the feeling of trying to fit a square peg in a round hole. As implied in Bringing Geppetto to life. #86 this could be potentially a powerful approach and selling point. To achieve this it's necessary to make the Geppetto concepts dedicated and not adhere to another paradigm. Obviously the role of the documentation will be to show the way on how to use it in an MVC or similar context.
  • move towards a fluent API. Yeah, it's hipster, but it's hip for a reason. If done well it seriously increases readability. I've done many sketches, some of which found their way into Fluent interface #73 but most didn't survive the drawing board. After a LOT of weighing, meditating, magic-8-balling and head banging I've decided to go for the fully descriptive, symmetrical fluent interface, which will turn out to be something like this:
loginContext.wire(15)
    .as.value("timeout duration");

loginContext.wire(ServiceRequest)
    .as.producer("serviceRequest");

loginContext.wire(AuthService)
    .as.singleton("authService")
    .using.wiring({
        timeout : "timeout duration",
        base : "serviceRequest"
    });

loginContext.wire(LoginCommand)
    .to.event("loginview:authentication:requested")
    .using.wiring("authService");

mainContext.wire(LoginView)
    .as.factory("loginPanel")
    .using.parameters({
            model : new AuthModel()
        })
        .and.handlers({
            "authservice:authentication:completed": "close",
            "authservice:authentication:failed": function(){
                this.show("ERROR!");
            }
        })
        .and.context(loginContext);

mainContext.wire(MainView)
    .using.wiring({
        loginRegion: "loginView"
    });
mainContext.trigger("maincontext:startup:requested");

Forgive me the convoluted example, but I tried to cover as much ground as possible. I think most of it is pretty self-explanatory if you're familiar with the current version. Obviously everything that's done using the context API here, can be configured inside your components as well: wiring, handlers, et cetera.
As you can see views are no longer wired as "view", but as factory. And wireClass is replaced with as.producer which is a better fitting term, IMO.
There's a new concept as well, "providers":

function QueueProvider(){
    this.queue = [];
};

QueueProvider.prototype.provide = function(ItemClass){
    var item = new ItemClass()
    this.queue.push(item);
    return item;
}

var requesterQueue = new QueueProvider();

context.provide("queue")
    .using(requesterQueue.provide);

context.wire(Requester)
    .as.queue("requester");

context.wire(SomeService)
    .using.wiring({
        requester: "requester"
    });

Now, what's this all about? ATM Geppetto contexts allow for registering objects as singletons, values, ... but I wanted to allow developers to create their own types. E.g. the above example would allow maintaining a list of all Requester instances. The provide method receives an ItemClass, which in this case is Requester. It creates a new instance and stores it in a queue.
The SomeService class is totally oblivious to whether it's receiving a singleton instance, or a new instance, or (in this case) a new instance which is stored in a queue.
As you can see the Context#provide method accepts a string which is used to expand the context API. I passed 'queue' to it, which adds a queue member to wire.as.
This allows for extremely powerful and versatile concepts and patterns. E.g. I can imagine use cases for an "object pool" or "cyclic" provider for instance.

  • better lifecycle management and less intrusive constructor handling ATM the wired constructors are wrapped, but I'm going to change that, since some people had a really hard time accepting that cough @mmikeyy cough. We'll resolve dependencies through a (wrapped) initialize method which allows for a better life cycle as well: constructor: pre-resolution, initialize: post-resolution.

Well, that's about it FTM. As always, @geekdave please pull the reigns if I'm off into cuckoo-land.

@creynders
Copy link
Contributor Author

I'm so excited about the progress I've made in branch next

In a nut shell: the wiring API's up and running. Not set in stone obviously, but pretty definite already.
So, there's a number of changes and new concepts. (Dropped factory for constructor)

// v0.7
context.wireView("SomeView", SomeView);
context.wireClass("someClass", SomeClass);
context.wireValue("someValue", "some value");
context.wireSingleton("someSingleton", SomeSingleton);
context.getObject("someSingleton");
context.hasWiring("someSingleton");


//v0.8
context.wire(SomeView).as.constructor("SomeView"); //will resolve to a constructor function
context.wire(SomeClass).as.producer("someClass"); //will resolve to an instance of SomeClass
context.wire("some value").as.value("someValue"); //will resolve to "some value"
context.wire(SomeSingleton).as.singleton("someSingleton"); //will resolve to "some value"
context.get("someSingleton"); //will return the SomeSingleton instance
context.has("someSingleton"); //will output `true`

//however, then things start happening.
//ALL `.as.<provider>` (and some other) methods accept a "string", an object, an array, 
//parameters or any mix of those

context.wire(SomeSingleton).as.singleton("SingletonA", "SingletonB", "SingletonC"); //equals:
context.wire(SomeSingleton).as.singleton(["SingletonA", "SingletonB", "SingletonC"]); //equals:
context.wire(SomeSingleton).as.singleton({ a: "SingletonA", b: "SingletonB", c: "SingletonC"} ); //equals:
context.wire(SomeSingleton).as.singleton([["SingletonA"]], "SingletonB", {c: "SingletonC"});

//now what does this mean?
//They all share the same singleton instance
context.get("SingletonA") ===  context.get("SingletonB") === context.get("SingletonC");

//sometimes it's more beneficial to have separate singletons though, i.e. a "multiton"
context.wire(SomeSingleton).as.multiton("SingletonA", "SingletonB", "SingletonC");
//will create three separate singletons

//both of the above open up a ton of possibilities, where classes use the same dependencies, 
//however they're named differently which boosts reuse, flexibility et cetera.

//but there's more: sometimes you want to create an instance (to configure it for example)
//at wiring-time, but you want its dependencies to be resolved lazily, that's possible now too.
var foo = new FooClass(); //it has its dependencies declared as a `wiring` object inside the prototype
context.wire(foo).as.unresolved("foo");
context.get("foo"); //will resolve its dependencies

//oh, and `get` and `has` are just as flexible as the `.as.<provider>` methods:
var result = context.get("SingletonA", ["someClass"], {view:"SomeView"});
console.log(result);
/*
output:
{
    SingletonA: <SomeSingleton instance>,
    someClass: <SomeClass instance>,
    view: <SomeView instance>
}
*/

//but if you `get` a single key, it returns a single value
context.get("SingletonA");

//the same goes for `has`
var result = context.has("SingletonA", ["someClass"], {view:"SomeView"}, "notfound");
console.log(result);
/*
output:
{
    SingletonA: true,
    someClass: true,
    view: true,
    notfound: false
}
*/

//And if you simply want to be sure all keys have been wired:
var result = context.has.each("SingletonA", ["someClass"], {view:"SomeView"}, "notfound");
console.log(result);//outputs: false (since "notfound" is not wired)

//the same flexibility applies to `release`
context.release.wires("SingletonA")
context.release.wires("SingletonA", ["someClass"], {view:"SomeView"});
context.release.all();

//Oh, and wirings get merged.
function Foo(){
    this.wiring = "a";
}

//either:
context.wire(Foo).as.singleton("foo").using.wiring("b");
var foo = context.get("foo"); //instance with "a" and "b" resolved

//or:
var foo = new Foo();
context.resolve(foo, "b"); //foo has its dependencies "a" and "b" resolved

//ah yes, wiring declarations are just as flexible:
function Foo(){
    this.wiring = "a"; //resolves "a" into member "a"
    this.wiring = ["a", "b"]; //resolves "a" and "b" into members "a" and "b"
    this.wiring = { foo: "a", baz : "b"};//resolves "a" and "b" into members "foo" and "baz"
    this.wiring=["a", {baz:"b"}];//resolves "a" and "b" into members "a" and "baz"
}

Now on to tackle the rest.
I rewrote all tests, they're inside specs.

@mmikeyy
Copy link
Contributor

mmikeyy commented Jan 30, 2015

wow! You're on fire these days! I'm getting excited too!! 😃

I'm anxious to see a working version that I can test! I have several projects that I want to convert to Geppetto, some quite big. Any idea how long you're giving yourself to 'tackle the rest'?

@creynders
Copy link
Contributor Author

@mmikeyy hard to say, since I can only work on this very irregularly. Also, there's still a ton to do.

The two major jobs are

  1. documentation: will need to be rewritten almost from scratch and it will be a LOOOT of work
  2. messaging: I've been thinking about this a lot and its hard. ATM I'm swaying towards not dropping backbone and expand on the Events system it provides, something like this:
//we want to be able to let components react to context events
wire(SomeView).as.producer("someView")
    .and.on("some:context:event").execute("render")

//or if you want to setup a bunch at once:
wire(SomeView).as.producer("someView")
    .and.on({ "some:context:event" : "render" });

//but we also want the reverse, i.e. components should easily be able to communicate to the context
wire(SomeView).as.producer("someView")
    .and.relay("some:view:event") //sends "some:view:event" through the context
//but we want event translation as well
wire(SomeView).as.producer("someView")
    .and.relay("some:view:event").as("another:context:event"); //when view triggers "some:view:event" the context translates it to "another:context:event"
//again with a map
wire(SomeView).as.producer("someView")
    .and.relays({ "some:view:event":"another:context:event" })
//or an array
wire(SomeView).as.producer("someView")
    .and.relays([ "some:view:event" ])


//then the context needs to be able to do stuff with context events
//relay them to other contexts
context.relay("some:view:event").to.all(); // this is to every other context
//or to its parent
context.relay("some:view:event").to.parent(); // this is to its parent context

//but we also want to be able to translate events
context.relay("some:view:event").as("another:context:event")
//to other contexts if necessary
context.relay("some:view:event").as("another:context:event").to.all()
context.relay("some:view:event").as("another:context:event").to.parent()

//all components have their `trigger` method which dispatches to direct listeners, 
//(or context listeners if configured that way) but they also have 
//a `relay` method which functions as described above

//the context also dispatches with `trigger` which is exaclty like in Backbone
context.trigger("event", ...params);
//except it's enhanced, allowing you to dispatch to other contexts
context.trigger("event", ...params).to.all(); //dispatches within 'context' but also to all other contexts
context.trigger("event", ...params).to.parent(); //dispatches within 'context' but also to parent context

Just to be clear, I'm totally focusing on how everything works at wiring-time, but everything you can configure outside the components will be available inside the components as well. I.e. it will cater to both styles.

@creynders
Copy link
Contributor Author

Then, on to commands and listening directly:

//if you want run a default handler:
wire(SomeView).as.producer("someView")
    .and.on("some:context:event").execute(); //calls the instance's `execute` method

//which leads us to commands, changed my mind about not having them in the API.
context.wire(SomeCommand).as.command().on("some:context:event"); //which is (almost) equivalent to:
context.wire(SomeCommand).as.producer("someCommand")
    .and.on("some:context:event").execute();
//you could provide a name to the "normal" commands as well:
context.wire(SomeCommand).as.command("someCommand").on("some:context:event");


//the above means you can do:
context.wire(SomeCommand).as.singleton("someCommand")
    .and.on("some:context:event").execute(); //creates a singleton command, i.e. it's not dropped after execution

//then, listening directly on a context, the BB way
context.on("some:context:event", function(){
    console.log("I rock!");
});

//or fluently
context.on("some:context:event").execute( function(){
    console.log("And roll!");
});

As you can see I aim for a use-it-as-you-wish API, which allows for being very strict, but also creating exceptions when necessary.
Again, it will be the documentation's task to show the "best" way. And then for "advanced" usage explain what possibilities and exceptions are possible.

@mmikeyy
Copy link
Contributor

mmikeyy commented Jan 31, 2015

OK. I can see that the new Geppetto won't be available any time soon. No problem: we already have something very good...

I think it's good to keep the Backbone event system. Why reinvent the wheel? But then... one could retort that this comment is not surprising coming from a Backbone user, and not everyone uses Backbone. Anyway, I don't remember seeing any complaints about this...

Concerning events, I've been wondering why we always send events to self, to parent and/or to parents. Why not have children too as an option? If I'm not mistaken, the only way to reach children ATM is to dispatch to all.

Anxious to test a functional version!

@creynders
Copy link
Contributor Author

So, time for an update. I just pushed into the next branch a LAAARGE part of Geppetto.Events.
Again, aiming for readability and versatility. Most of what I wrote above is still correct.

Usage:

var dispatcher = new Geppetto.Events();
//equals
var dispatcher = Geppetto.Events();
//equals
var dispatcher = _.extend({}, Geppetto.Events); //As you would do with Backbone.Events

//dummy handler
var handler = function(){
  console.log(arguments);
}

//fully Backbone.Events compatible:
dispatcher.on("event", handler, someObject);
dispatcher.off("event");
someObject.listenTo(dispatcher, "event", handler);
//et cetera

//But it's enhanced:

dispatcher.on("event").have(someObject).execute(handler); // in scope of `someObject`
//you can attach functions directly to events as well
dispatcher.on("event").execute(handler);

//if your listener has a method "foo" for instance you can use these too:
dispatcher.on("event").have(someObject).execute("foo");

//when hanging on a geppetto context you can use keys for lazy creation:
context.on("event").have("loginService").execute("signin"); // loginService will only be created when event is first dispatched

//"execute" is the default handler, which leads to the new way of wiring commands (Yes, changed my mind again
context.on("event").have(MyCommand).execute();

//BTW: you can pass multiple functions to `execute`:
//as parameters
context.on("event").execute(f1, f2, f3)
//as an array
context.on("event").execute([f1, f2, f3])
//as an object
context.on("event").execute({ a: f1, b : f2, c: f3 });

//`trigger` works as in Backbone
context.trigger("event", a, b, c); //handling function will receive `a,b,c`
//and you can use `dispatch` to have the old Geppetto style
context.dispatch("event", { a: a, b: b, c: c});
//handling function receives following object
var received = {
  eventName : "event",
  eventData : {
    a: a,
    b: b,
    c: c
  }
}

//`listenTo` works as in Backbone:
context.listenTo(someObj, "event", handler);
//but is also enhanced:
context.listenTo(someObj).on("event").execute(handler);
//and aliased to `allow`
context.allow(someObj).on("event").execute(handler); 

//`once` and `listenToOnce` work as in BB AND are enhanced as well
context.once("event").execute(f1, f2, f3)

//And if you prefer a really explicit style, you can do this:
var when = Geppetto.Events;
when(context).dispatches("event").execute(handler);
when(context).dispatches("event").have(someObject).execute(handler);
//completely equal to:
context.on("event").execute(handler);
context.on("event").have(someObject).execute(handler);

I'm not entirely satisfied with have and allow, but really can't come up with anything better.

@creynders
Copy link
Contributor Author

488 tests already!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants