Writing good object-oriented code under AnyEvent
Asked Answered
S

4

8

We're building a large application with complex logic, which is composed of modules. I used to build larger scale methods of simpler methods, for instance,

# fig. 1   
package Foo;
sub highlevel {
    my ($self, $user, $event) = @_;
    my $session = $self->get_session($user);
    my $result = $self->do_stuff($session, $event);
    $self->save_session($session);
    return $result;
};

(this is simplified of course). Results are returned, exceptions are thrown, everyone's happy.

Now, we're making a move to AnyEvent. My module is NOT the uppermost level, so I can't do just

# fig. 2
my $cv = AnyEvent->condvar;
# do stuff
return $cv->recv;

Most AE modules I've seen so far work like this:

# fig. 3
$module->do_stuff( $input, 
    on_success => sub { ... }, 
    on_error => sub { ... }
);

So I'm done rewriting the lower-level methods and tried to proceed with highlevel() and...

# fig. 4
package Foo;
sub highlevel {
    my ($self, $user, $event, %callbacks) = @_;
    my $done = $callbacks{on_success};
    my $error = $callbacks{on_error};
    $self->get_session( $user,
        on_error => $error,
        on_success => sub {
             my $session = shift;
             $self->do_stuff( $session, $event,
                  on_error => $error,
                  on_success => sub { 
                       my $result = shift;
                       $self->save_session( $session,
                            or_error => $error,
                            on_success => sub { $done->($result); }
                       );
                  }
              );
          }
     );
 };

Not exactly beautiful. I call it "the Infinite ladder".

Now the next thing I could come up with was an ad-hoc state machine where highlevel() is broken up into _highlevel_stage1(), _highlevel_stage2() etc. But that doesn't satisfy me as well (it's unmaintainable, and thinking of good names instead of stageXX gives me headache).

We're already looking into a full-blown state machine to drive the whole app, but having to add a transition for every interaction looks a bit too generous to me.

So the question is: What are the best practices for writing modules that implement business logic (fig. 1) for running within an AnyEvent app (fig. 3)?

Sobriety answered 15/6, 2012 at 7:28 Comment(0)
C
3

You may want to encapsulate it in a Future object using the Future module. That adds syntactical sugar to make this cleaner.

Cyclist answered 28/11, 2014 at 12:0 Comment(0)
I
8

Executive Summary: you either want inversion of control (threads with Coro that block) or a state machine.

You could use Coro, which can convert the infinite ladder into linear code (by inversion of control), e.g. using Coro::rouse_cb/rouse_wait, or some of the Coro::AnyEvent functions:

   do_sth cb => sub { ...

becomes (if the callback is only called once):

   do_sth cb => Coro::rouse_cb;
   my @res = Coro::rouse_wait;

Your only other option is to use a state machine, e.g. using an object (there are many ways to implement state machines, especially in Perl):

my $state = new MyObject;

do_something callback => sub { $state->first_step_done };

And in first_step done, you register a callback for $self->next_state_done etc.

You can also look into some cpan modules, such as AnyEvent::Blackboard or AnyEvent::Tools - I haven't used them myself, but maybe they can help you.

As for condvars, I am personally not so hot about using them as the only means for an API - I prefer callbacks (as condvars are valid callbacks this allows both), but condvars do allow you to raise exceptions in the consumer (via the croak method).

Irrawaddy answered 15/6, 2012 at 20:35 Comment(1)
Thanks for clarification. This leaves me with even more questions than I had initially, but at least now I can go and read myself.Sobriety
P
3

Well, one thing I can think of is using slightly modified Chain of Responsibility pattern:

my $params = {
  user => $user,
  event => $event,
  session => undef
};

my @chain = ('get_session', 'do_stuff', 'save_session', 'emit');

for my $index (0..$#chain) {
  my $current = $chain[$index];
  my $next    = $chain[$index + 1] || undef;
  $self->{$current}($params, 
    on_error => sub { $self->error($params) },
    on_success => sub { $self->{$next}($params) },
  );
}

It's a bit rough, but I hope it shows the point. )

Pasteurizer answered 15/6, 2012 at 13:4 Comment(0)
C
3

You may want to encapsulate it in a Future object using the Future module. That adds syntactical sugar to make this cleaner.

Cyclist answered 28/11, 2014 at 12:0 Comment(0)
S
1

Visiting this question 7 years later, I seem to know the right answer: monad. Coroutines, Future, Promise, async/await, and (a sane implementation of) a finite state machine are all subtypes of that.

Of course, the curse of the monad prevents me from explaining it to others.

If I had to do it again today, I would've gone with some sort of promise objects.

Sobriety answered 11/8, 2019 at 13:42 Comment(3)
"some sort of promise objects" is exactly what Future provides.Absolutely
@Absolutely yes, and I changed the selected answer based on that ground. Although I'm not working on the project I was asking this for, 7 years is a ton of time.Sobriety
Ah, I didn't realize you had changed they accepted answer.Absolutely

© 2022 - 2024 — McMap. All rights reserved.