Article
· Jul 21, 2021 5m read

Design By Contract (DbC)

This time I don’t want to write about a brilliant feature of IRIS (of which it has many), but instead about a feature that is sorely missing.
Today, talking about OOP is not sexy. Although almost all modern programming languages implement some kind of OOP,  discussions about fundamental issues of software development are not very common between real-world implementors of such technologies as developers are. In fact, Computer Science as a whole isn’t a mainstream topic between developers, which I think it should be.
In this InterSystems Developers Community,  most posts are about questions (How do I … ?) of how to use the technology or practical solutions to a problem (How can I … ?).
But not many are about fundamental computer science concepts and how they are implemented (or not) in our favorite Data Platform.
 
How can one be using a fundamental concept such as OOP, but not discussing it how it is being implemented in your favorite platform?
It is the same with everybody using (and having opinions about) REST API’s without even knowing what REST means or have read Roy Fielding’s dissertation.
Or using the concept of an ESB and know nothing about Dave Chappel’s book where he first introduced the term Enterprise Service Bus.
(Mis)Using the term ‘agile’ without ever having read the Agile Manifesto.
 
One of my all-time favorite books about computer science and OOP is Bertrand Meyer’s book Object-Oriented Software Construction (2nd Ed) (OOSC), a classic about software quality, robustness, extensibility, and reusaibility, and why OOP is a solution for many of these issues.
Every developer who is using classes, objects, methods, and inheritance should read this book.
 
Based on the work of DataTree, InterSystems has made an excellent OOP implementation with Cache and the Cache Object Script extensions to Mumps. Great multiple inheritance (of which I will probably write a different article) and polymorphism, not only in the programming language but also in the database. In my opinion, the only (but unique) combination that truly implements OOP: code and data in one place.
 
But what IRIS lacks and which is described in detail in OOSC,  is a feature that lays a concise robustness layer upon OOP which is called Design by Contract.
 
What we often see in code (methods) is:
method getMail(id) → Mail
  If not id return “Id is required”
 
This is what we call ‘defensive programming’, the method being called is arming itself against ‘illegal’ invocations.
But the ‘contract’ of the method is obvious: if you give me an (valid) id I will return the associated email message, this code, executed at every invocation, shouldn’t be necessary. This way the responsibility is being shifted from the caller to the method being called.
 
DbC is all about that, the ‘contract’ of a class or a method or an object instance of that class states precisely what is expected as input and what is promised as output as long as the caller adheres to the contract of the method (or the class as a whole).
 
DbC is defensive programming on steroids, without the runtime penalty of checking everything: you describe all constraints at design time.
DbC has two runtime modes: development and production.
In development mode, all constraints and checks are executed and reported.
In production mode, they will only be executed in case of an exception.
 
How does DbC do that?
 
At a class level, you define ‘invariants’: conditions that need to be met at all times (entering constructor methods excluded).
At a method level, you define pre-conditions and post-conditions.
Before even executing a single line of code, the pre-conditions must be met.
After having executed the method’s code the post-conditions must be met, for that for every property there is an ‘old’ value, the value the property had before the method was executed.
And for both entering as well as exiting the method, the invariants must still be valid.
 
In development mode, these conditions will all be executed. At that level you can choose how to handle unfulfilled conditions: throw an error or just report the event.
 
In production mode, the conditions will not be executed, but in the advent of an exception within the method, both invariants and pre-conditions will be checked and are part of the exception info, after that an error is being raised at all times.
You can also define a ‘rescue’ method, code that is being executed when an exception occurs, from setting database values, closing files and connections, executing repair code, etc. You can even set a ‘try-again’ flag.
Of course, the latter you can also achieve with IRIS standard $ZTrap or Try-Catch mechanisms.
 
Why should we want DbC in IRIS Objectscript?
I think the code will be much more robust and, having not to worry about performance penalties at runtime, the conditions can (should) be quite elaborate.
Having constructed the right set of conditions, unit-tests would hardly be needed, the ‘tests’ will be run at every invocation of a method. And, in contrary to separate unit-test frameworks, the conditions are right in the code, it’s documentation too.
 
If DbC is so great why isn't it widely applied?
Coming back to my introduction: ignorance.
As developers, in general, don’t ‘do’ Computer Science, they will never be able to grasp the essence of fundamental computer engineering concepts. But, even worse, educators, professional training programs, and ‘experts’ are hardly aware of the DbC concept.
(Unit) Testing is a far more easy concept to understand than writing a ‘contract’ for classes or code.
I also expect it to be quite difficult to build into an existing language or compiler.
There are a few languages that have DbC built-in, most prominent is Eiffel, a language, not entirely surprising, designed by Bertrand Meyer.
 
At InterSystems the trend seems to be slowly moving away from ObjectScript, especially with the upcoming Embedded Python feature, which is a brilliant move.
But when ObjectScript could have DbC built-in, the language would get a tremendous boost for designing robust applications.
It would be interesting to learn what the ObjectScript core developers have to say about this matter.
Discussion (5)2
Log in or sign up to continue

Thank you @Herman Slagman !
I like your suggestion and would fully support it if ISC would take that turn.
You mentioned ignorance and you are right. I'm full with you.
Though I still see another more dangerous hurdle to pass:
Understanding the concept. Applying the concept. Teaching the concept.
I just fear there might not be enough qualified developers available to execute it.
Seeing the actual situation on my side of the globe I wouldn't expect to find enough experts.  

Very good point @Herman Slagman 
To illustrate your point, in ObjectScript :

Include %occErrors

Class MyApp.Account Extends %Persistent
{

Property Balance As %Numeric;

/// Deposit money into the account
Method Deposit(amount As %Numeric) As %Status
{
    // Preconditions
    If amount < 0 {
        Return $$$ERROR($$$GeneralError, "Deposit amount must be non-negative")
    }

    // Store the old balance
    Set oldBalance = ..Balance

    // Update balance
    Set ..Balance = oldBalance + amount

    // Postconditions
    If (..Balance '= $$$NULLOREF) && (..Balance '= (oldBalance + amount)) {
        Return $$$ERROR($$$GeneralError, "Postcondition failed: Balance calculation error")
    }

    Quit $$$OK
}

/// Withdraw money from the account
Method Withdraw(amount As %Numeric) As %Status
{
    // Preconditions
    If amount < 0 {
        Return $$$ERROR($$$GeneralError, "Withdrawal amount must be non-negative")
    }
    If (..Balance = $$$NULLOREF) || (..Balance < amount) {
        Return $$$ERROR($$$GeneralError, "Insufficient funds")
    }

    // Store the old balance
    Set oldBalance = ..Balance

    // Update balance
    Set ..Balance = oldBalance - amount

    // Postconditions
    If (..Balance '= $$$NULLOREF) && (..Balance '= (oldBalance - amount)) {
        Return $$$ERROR($$$GeneralError, "Postcondition failed: Balance calculation error")
    }

    Quit $$$OK
}

/// Invariant: Balance should always be non-negative
Method CheckBalanceInvariant() As %Status
{
        Set tSC = $$$OK
        If ..Balance < 0 {
            Set tSC = $$$ERROR($$$GeneralError, "Balance invariant violated: Balance is negative")
        }
        Quit tSC
}

/// Class method to test the Account class
ClassMethod TestAccount() As %Status
{
    // Create a new instance of Account
    Set account = ##class(MyApp.Account).%New()
    
    // Initialize the balance
    Set account.Balance = 0
    
    // Test depositing a positive amount
    Set tSC = account.Deposit(100)
    If $$$ISERR(tSC) {
        Write "Deposit failed: ", $system.Status.GetErrorText(tSC), !
        Quit tSC
    }
    Write "Deposit succeeded: Balance after deposit: ", account.Balance, !
    
    // Test depositing a negative amount (should fail)
    Set tSC = account.Deposit(-50)
    If $$$ISERR(tSC) {
        Write "Deposit of negative amount failed as expected: ", $system.Status.GetErrorText(tSC), !
    } Else {
        Write "Deposit of negative amount unexpectedly succeeded", !
        Quit $$$ERROR($$$GeneralError, "Deposit of negative amount unexpectedly succeeded")
    }
    
    // Test withdrawing a valid amount
    Set tSC = account.Withdraw(50)
    If $$$ISERR(tSC) {
        Write "Withdrawal failed: ", $system.Status.GetErrorText(tSC), !
        Quit tSC
    }
    Write "Withdrawal succeeded: Balance after withdrawal: ", account.Balance, !
    
    // Test withdrawing more than the available balance (should fail)
    Set tSC = account.Withdraw(200)
    If $$$ISERR(tSC) {
        Write "Withdrawal of more than available balance failed as expected: ", $system.Status.GetErrorText(tSC), !
    } Else {
        Write "Withdrawal of more than available balance unexpectedly succeeded", !
        Quit $$$ERROR($$$GeneralError, "Withdrawal of more than available balance unexpectedly succeeded")
    }
    
    // Check balance invariant (should succeed)
    Set tSC = account.CheckBalanceInvariant()
    If $$$ISERR(tSC) {
        Write "Balance invariant violated: ", $system.Status.GetErrorText(tSC), !
        Quit tSC
    }
    Write "Balance invariant holds true", !
    
    // Intentionally set balance to negative value to trigger balance invariant failure
    Set account.Balance = -10
    
    // Check balance invariant (should fail)
    Set tSC = account.CheckBalanceInvariant()
    If $$$ISERR(tSC) {
        Write "Balance invariant violated as expected: ", $system.Status.GetErrorText(tSC), !
    } Else {
        Write "Balance invariant unexpectedly held true", !
        Quit $$$ERROR($$$GeneralError, "Balance invariant unexpectedly held true")
    }
    
    Write "Account operations completed successfully", !
    Quit $$$OK
}

Storage Default
{
<Data name="AccountDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Balance</Value>
</Value>
</Data>
<DataLocation>^MyApp.AccountD</DataLocation>
<DefaultData>AccountDefaultData</DefaultData>
<IdLocation>^MyApp.AccountD</IdLocation>
<IndexLocation>^MyApp.AccountI</IndexLocation>
<StreamLocation>^MyApp.AccountS</StreamLocation>
<Type>%Storage.Persistent</Type>
}

}

With the following results :

write ##class(MyApp.Account).TestAccount()
Deposit succeeded: Balance after deposit: 100
Deposit of negative amount failed as expected: ERROR #5001: Deposit amount must be non-negative
Withdrawal succeeded: Balance after withdrawal: 50
Withdrawal of more than available balance failed as expected: ERROR #5001: Insufficient funds
Balance invariant holds true
Balance invariant violated as expected: ERROR #5001: Balance invariant violated: Balance is negative
Account operations completed successfully
1

In Python :
 

class Account:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        if amount < 0:
            raise ValueError("Deposit amount must be non-negative")
        
        old_balance = self.balance
        self.balance += amount
        
        # Postconditions
        if self.balance != old_balance + amount:
            raise ValueError("Postcondition failed: Balance calculation error")

    def withdraw(self, amount):
        if amount < 0:
            raise ValueError("Withdrawal amount must be non-negative")
        if self.balance < amount:
            raise ValueError("Insufficient funds")

        old_balance = self.balance
        self.balance -= amount
        
        # Postconditions
        if self.balance != old_balance - amount:
            raise ValueError("Postcondition failed: Balance calculation error")

    def check_balance_invariant(self):
        if self.balance < 0:
            raise ValueError("Balance invariant violated: Balance is negative")

    @classmethod
    def test_account(cls):
        account = cls()
        
        try:
            # Test depositing a positive amount
            account.deposit(100)
            print("Deposit succeeded: Balance after deposit:", account.balance)
            
            # Test depositing a negative amount (should fail)
            account.deposit(-50)
        except ValueError as e:
            print("Deposit of negative amount failed as expected:", e)
        else:
            raise ValueError("Deposit of negative amount unexpectedly succeeded")
        
        try:
            # Test withdrawing a valid amount
            account.withdraw(50)
            print("Withdrawal succeeded: Balance after withdrawal:", account.balance)
            
            # Test withdrawing more than the available balance (should fail)
            account.withdraw(200)
        except ValueError as e:
            print("Withdrawal of more than available balance failed as expected:", e)
        else:
            raise ValueError("Withdrawal of more than available balance unexpectedly succeeded")
        
        try:
            # Check balance invariant (should succeed)
            account.check_balance_invariant()
            print("Balance invariant holds true")
        except ValueError as e:
            print("Balance invariant violated:", e)
        
        # Intentionally set balance to negative value to trigger balance invariant failure
        account.balance = -10
        
        try:
            # Check balance invariant (should fail)
            account.check_balance_invariant()
        except ValueError as e:
            print("Balance invariant violated as expected:", e)
        else:
            raise ValueError("Balance invariant unexpectedly held true")
        
        print("Account operations completed successfully")

# Run the test
Account.test_account()

With the following results :

python account.py 
Deposit succeeded: Balance after deposit: 100
Deposit of negative amount failed as expected: Deposit amount must be non-negative
Withdrawal succeeded: Balance after withdrawal: 50
Withdrawal of more than available balance failed as expected: Insufficient funds
Balance invariant holds true
Balance invariant violated as expected: Balance invariant violated: Balance is negative
Account operations completed successfully