简体   繁体   中英

How do I get a message sender in SmallTalk (Pharo)?

I'm having trouble getting the sender of a message in SmallTalk. What I want to accomplish is to modify the return value of a method (A) from another method (B) which is called by the first one (A). Again... A calls B, and I want B to return a value from A's context.

Example code:

This would be A:

A

| aResult aPartialResult |

aPartialResult := self B.

"do things with aPartialResult"

^aResult.

And this would be B:

B

| aResult |

[ aResult := "do something" ]
                        on: Exception
                        do: ["make A return something"].
^aResult.

The thing is that I want to the exceptions that could be raised in B to be handled in B too. That's why I don't just raise an exception in B to handle it in A and easily return from there.

I thought I would be able to do this using thisContext, but the sender is nil. It wouldn't hurt to also get an answer on why is that...

Thanks in advance!

Guillermo, Exception handling can seamlessly replace a few bad ideas here:

  1. using thisContext (which is almost never necessary and usually a bad idea)
  2. passing strings around eg '1|', UserInterface invalidCartIdErrorMessage
  3. Using return: with those strings to pass errors along

Also, retrieveCart:onErrorReturnFrom: is doing too much. With all the error handlers, the actual logic gets lost.

So, the first thing I would to is create Error subclasses to represent your domain concepts eg AddBookError, CartExpiredError, InvalidCartError

Then you just set the error message, maybe like:

CartExpiredError>>initialize

    super initialize.
    self messageText: '1|', UserInterface cartHasExpiredErrorMessage.

The next thing (really two steps) is to replace the raw dictionary methods with private accessors, which can use your new Error classes, like so:

timestampFor: aCartId

    ^ cartCreationDateAndTime at: aCartId ifAbsent: [ InvalidCartError signal ].

and

cartNumber: aCartId 

     ^ carts at: aCartId ifAbsent: [ InvalidCartError signal ].

Cart>>add: aQuantity booksWithISBN: aBookISBN

    fail ifTrue: [ AddBookError signal ].

Now, retrieveCart:onErrorReturnFrom: can become:

retrieveCart: aCartId

    | aCartCreationDateAndTime |
    aCartCreationDateAndTime := self timestampFor: aCartId.
    Time now > (aCartCreationDateAndTime + 30 minutes) ifTrue:  [ CartExpiredError signal ].
    ^ self cartNumber: aCartId.

And finally, the greatly-simplified A becomes:

add: aQuantity booksWithISBN: aBookISBN toCart: aCartId

     | aCart |
    [aCart := self retrieveCart: aCartId.
    aCart add: aQuantity booksWithISBN: aBookISBN]
        on: Error
        do: [ :e |  ^ e messageText ].
    ^ '0|OK'.

This can still be cleaned (eg Make a superclass for all the Error classes that prepends '1|' to the messageText), and obviously you will have to work this simplified version into your actual project, but can you start to see how exceptions can make your life easier?

Here is a working mockup of the code, with passing tests on github

nb The one other thing I noticed was aCartCreationDateAndTime. It would seem more natural to have this be a property of the cart, but maybe that doesn't make sense in the actual application...

A simple way is that A pass a Block with a return inside to B, like :

A
   | aResult aPartialResult |
   aPartialResult := self BonSpecialConditionDo: [:partial | ^partial].
   ...snip...

Then

BonSpecialConditionDo: aBlock
    | partialResult |
    partialResult := self doSomethingPartial.
    ^[self doSomething]
        on: SomeException
        do: [:exc | aBlock value: partialResult]

Beware, catching Exception is considered dangerous (you catch too many things).

EDIT: I just removed an un-necessary return ^ inside the handler

EDIT: doing it with superpowers (but don't kill a fly with a hammer)

B
    | partialResult |
    partialResult := self doSomethingPartial.
    ^[self doSomething]
        on: SomeException
        do: [:exc | thisContext home sender home return: partialResult ]

I thought you could access thisContext through the Exception exc (this is the instance variable handlerContext), but there does not seem to be any convenient message for accessing this internal state...

Got it!

Here's A:

A

| aResult aPartialResult |

aPartialResult := self BOnErrorReturnFrom: thisContext.

"do things with aPartialResult"

^aResult.

And this would be B:

BOnErrorReturnFrom: aContext

| aResult |

[ aResult := "do something" ]
                        on: Exception
                        do: [aContext return: "whatever you want :)"].
^aResult.

It wasn't so hard after I realized that the keyword "thisContext", when used in a BlockClosure, doesn't return the ContextPart in which the block was declared but something else (still not sure what it is, though).

In response to Sean:

What I'm attempting to do with this is to avoid repetition of code. The object which implements A and B is the inner (model side) part of a REST interface, so I want it to return a string and only a string (I don't want exceptions nor anything different than a string object to get through). In my specific problem, A (sorry for breaking the Smalltalk message naming convention, but I think that changing that now will lead to further confusion...) receives a cart ID and will do something with that cart. A has to retrieve the cart using that ID, make some validations on the cart and, in case of error, return an ad hoc message (always as a string). This retrieval-and-validation code will be repeated in each message that has to retrieve a cart.

This is the proper code I finally got:

This is A: (Pay no attention to the #try message. It just assures that no exception gets out without being converted to a string. If anyone knows how to do this in a better way, please tell me how!)

add: aQuantity booksWithISBN: aBookISBN toCart: aCartId

    | aCart aContext |

    aContext := thisContext.

    ^self try: [

        aCart := self retrieveCart: aCartId onErrorReturnFrom: aContext.

        [aCart add: aQuantity booksWithISBN: aBookISBN]
                                                    on: Error
                                                    do: [ :anError | ^'1|', (self formatAsResponse: anError messageText) ].

        ^'0|OK'.
    ].

This is B:

retrieveCart: aCartId onErrorReturnFrom: aContext

    | aCartCreationDateAndTime aCart |

    [aCartCreationDateAndTime := cartCreationDateAndTime at: aCartId asInteger.]
                                                                                    on: KeyNotFound 
                                                                                    do: [ aContext return: ('1|', (UserInterface invalidCartIdErrorMessage)).].

    (systemClock now > (aCartCreationDateAndTime + 30 minutes)) 
                                                            ifTrue:  [aContext return: ('1|', (UserInterface cartHasExpiredErrorMessage))].

    [aCart := carts at: aCartId asInteger.]
                                    on: KeyNotFound
                                    do: [ aContext return: ('1|', (UserInterface invalidCartIdErrorMessage))].

    ^aCart.

A

| aResult aPartialResult |

aPartialResult := self B.

"do things with aPartialResult"

^aResult.

B

| aResult |

[ aResult := "do something" ] on: Exception do: ["make A return something"]. ^aResult.

i would write it as

A

| aResult aPartialResult |

aPartialResult := self B. aPartialResult ifNotNil:[

"do things with aPartialResult"].

^aResult.

B

| aResult |

[ aResult := "do something" ] on: Exception do: [:e|^nil]. ^aResult.

well then thats just me!!!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM