Search code examples
oopsmalltalksqueakdesign-by-contract

Enforcing and contract methods in Squeak


So I created a class that enforces every method (message) that is sent to it's class instance.

i.e that code:

|a|
a := Animal new.
a makeSound: 'bark'

should lead to a call to "doesNotUnderstand" (even though it exists in the class) and it should check wehther the post and pre conditions are there, i'll explain: If a method looks like this:

    makeSound: aNoise
 "%self assert: [canMakeNoise = true]%"
 "@self assert: [numOfLegs >= 0]@"
 numOfLegs := -1

it means that asside from that main method, there is also a method called: PREmakeSound which is implementation is:

self assert: [canMakeNoise = true]

and onther method called POSTmakeSiund which is implemented as follows:

self assert: [numOfLegs >= 0]

---My question is - because of the fact that every method call is calling doesNotUnderstand, whenever I want to actually activate the method (after I cheked whatever i needed) how can I activate it as is? hope my problem is clear...


Solution

  • Maybe using method wrappers would work better than using #doesNotUnderstand:?

    Create a class PrePostMethod with a compiledMethod instance variable. You can then install instances of PrePostMethod in the method dictionary of a class instead of instances of CompiledMethod.

    When the VM looks-up a message and gets this PrePostMethod instance instead of a CompiledMethod, it doesn't know what to do with it. Consequently, it will send "run: aSelector with: arguments in: receiver" to that PrePostMethod object. This is where you can perform custom action like checking pre-post condition.

    For example:

    PrePostMethod>>run: aSelector with: arguments in: receiver
        | result |
        self checkPrecondition: receiver
        result := compiledMethod run: aSelector with: arguments in: receiver
        self checkPostCondition: receiver.
        ^ result
    

    As Sean suggests, an alternative solution is to change the way these methods are compiled. You could transform the AST of the method before compilation, or change the compilation process itself. For example, with the AST transformation approach you can transform:

    makeSound: aNoise
        "%self assert: [ self canMakeNoise]%"
        "@self assert: [ self numOfLegs >= 0]@"
        numOfLegs := -1
    

    into:

    makeSound: aNoise
        self assert: [ 
            "The preconditions goes here" 
            self canMakeNoise ]
        ^ [ "Original method body + self at the end if there is no return"
            numOfLegs := -1.
            self ] ensure: [ 
                "The postcondition goes here" 
                self assert: [ self numOfLegs >= 0 ] ]
    

    One the one hand, these solutions would be more tedious to implement but one the other hand, they are more performant.

    HTH