Sunday, June 28, 2009

State The Obvious

Some use humor: Waterfall Conference

Others state it bluntly: Anti-If Campaign

I have joined Anti-IF Campaign

As you can see in the code on the Anti-If Campaign website, the condition checking code violates the Tell, Don't Ask principle.  'Tell Don't Ask' is the basis for the argument that getters and setters violate object encapsulation.

The Anti-If Campaign immediately brought to mind the Loan Payment Posting 'hierarchy' as it is called which I'll demonstrate below.  In the first version of the Loan class, you'll see duplication and a lot of IF statements.  A second version will then be shown which will attempt to implement a cleaner version of the same functionality.
package com.mortgage.model;

public class Payment {
   double tenderedAmount;

   public Payment(double tendered) {
      tenderedAmount = tendered; 
   } 

   double getTenderedAmount() {
   return tenderedAmount;
   } 
}
package com.mortgage.model;
 
public class Loan {
   double interestDue;
   double principalDue;
   double feesDue;
   double escrowDue;
 
   public Loan(double interest, double principal, double fees, double escrow) {
      interestDue       =  interest;
      principalDue      =  principal;
      feesDue           =  fees;
      escrowDue         =  escrow;
   } 
   /* 
     Post a customer's payment to the accounts due.
     Note the high cyclomatic complexity. 
   */
   double applyPayment( Payment pmt ) {
      double paymentAmountRemaining = pmt.getTenderedAmount(); 
      if (paymentAmountRemaining >= interestDue) {
         paymentAmountRemaining  -= interestDue;
         interestDue = 0;
      } else { 
         interestDue -= paymentAmountRemaining;
         paymentAmountRemaining = 0;
      }
 
      if (paymentAmountRemaining >= principalDue) {
         paymentAmountRemaining  -= principalDue;
         principalDue = 0;
      } else {
         principalDue -= paymentAmountRemaining;
         paymentAmountRemaining = 0;
      }
 
      if (paymentAmountRemaining >= feesDue) {
         paymentAmountRemaining  -= feesDue;
         feesDue = 0;
      } else {
         feesDue -= paymentAmountRemaining;
         paymentAmountRemaining = 0;
      }
 
      if (paymentAmountRemaining >= escrowDue) {
         paymentAmountRemaining -= escrowDue;
         escrowDue = 0;
      } else {
         escrowDue -= paymentAmountRemaining; 
         paymentAmountRemaining = 0;
      }
      return paymentAmountRemaining; 
   } 
 
   public String toString() {
      return "Loan: InterestDue=" + interestDue 
           + ", principalDue=" + principalDue 
           + ", feesDue=" + feesDue 
           + ", escrowDue=" + escrowDue;
   } 
}
package com.mortgage.model; 

import org.junit.* ;
import static org.junit.Assert.* ;

public class LoanTest { 
   @Test
   public void testPartiallyAppliedPayment_CoversInterestPrincipalAndFees() {
      Loan loan = new Loan( 500.00, 200.00, 100.00, 150.00 );
      assertTrue( loan.interestDue == 500.00 );
      assertTrue( loan.principalDue == 200.00 );
      assertTrue( loan.feesDue == 100.00 );
      assertTrue( loan.escrowDue == 150.00 );
      Payment pmt = new Payment( 800.00 );
      double remainder = loan.applyPayment( pmt );
      assertTrue( remainder == 0.00 );
      assertTrue( loan.interestDue == 0.00 );
      assertTrue( loan.principalDue == 0.00 );
      assertTrue( loan.feesDue == 0.00 );
      assertTrue( loan.escrowDue == 150.00 );
   }
}
Recognizing the repeated behavior applied to similar fields indicates that both behavior and data lack encapsulation, we can create a class to contain these fields and behavior.  The Account class will be added to the solution to encapsulate an accounting ledger and then the Loan class references to the primitive types will be replaced with references to instances of the Account class.  Loan.applyPayment() is reduced from 35 lines of code to 5 lines and it is implemented with only imperative statements, no conditionals!
package com.mortgage.model;
 
public class Account {
   double amountDue;

   public Account(double amount) {
      this.amountDue = amount;
   }
   /* 
     repeating IF statements have been eliminated. 
   */
   double applyPayment( double paymentAmount ) {
      if (paymentAmount >= amountDue) {
         paymentAmount -= amountDue;
         amountDue = 0;
       } else {
         amountDue -= paymentAmount; 
         paymentAmount = 0;
      }
      return paymentAmount; 
   }
}
package com.mortgage.model;
 
public class Loan {
   Account interestAccount;
   Account principalAccount;
   Account feesAccount;
   Account escrowAccount;
 
   public Loan(double interest, double principal, double fees, double escrow) {
      interestAccount   =  new Account(interest);
      principalAccount  =  new Account(principal);
      feesAccount       =  new Account(fees);
      escrowAccount     =  new Account(escrow);
   } 
    /* 
     Post a customer's payment to the accounts due.
     Note that the cyclomatic complexity is now zero. 
   */
   double applyPayment( Payment pmt ) {
      double remainder = interestAccount.applyPayment(pmt.tenderedAmount);
      remainder = principalAccount.applyPayment(remainder);
      remainder = feesAccount.applyPayment(remainder);
      remainder = escrowAccount.applyPayment(remainder);
      return remainder; 
   } 
 
   public String toString() {
      return "Loan: InterestDue=" + interestAccount.amountDue
           + ", principalDue=" + principalAccount.amountDue 
           + ", feesDue=" + feesAccount.amountDue 
           + ", escrowDue=" + escrowAccount.amountDue;
   } 
}
   @Test
   public void testPartiallyAppliedPayment_CoversInterestPrincipalAndFees() {
      Loan loan = new Loan( 500.00, 200.00, 100.00, 150.00 );
      assertTrue( loan.interestAccount.amountDue == 500.00 );
      assertTrue( loan.principalAccount.amountDue == 200.00 );
      assertTrue( loan.feesAccount.amountDue == 100.00 );
      assertTrue( loan.escrowAccount.amountDue == 150.00 );
      Payment pmt = new Payment( 800.00 );
      double remainder = loan.applyPayment( pmt );
      assertTrue( remainder == 0.00 );
      assertTrue( loan.interestAccount.amountDue == 0.00 );
      assertTrue( loan.principalAccount.amountDue == 0.00 );
      assertTrue( loan.feesAccount.amountDue == 0.00 );
      assertTrue( loan.escrowAccount.amountDue == 150.00 );
   }
And you know I'm not leaving out my ANT build!
<project name="Loan" default="test" basedir=".">

 <property name="version"    value="1.0.0"/>
 <property name="buildfile"  value="${ant.project.name}-${version}.jar"/>
 <property name="src.test"   location="src/test/java"/>
 <property name="src"        location="src/main/java"/>
 <property name="build"      location="build/main/"/>
 <property name="build.test" location="build/test/"/>
 <property name="dist"       location="dist"/>
 <property name="junit.jar"  location="/Users/greghelton/dev/lib/junit/junit-4.6.jar"/>

 <path id="classpath.base">
 <fileset dir="${build}" includes="*"/>
 </path>

 <path id="classpath.test">
   <path refid="classpath.base" />
   <pathelement location="${build}" />
   <pathelement location="${build.test}" />
   <pathelement location="${junit.jar}" />
 </path>

 <target name="clean">
   <delete dir="${build}"/>
   <delete dir="${build.test}"/>
   <delete dir="${dist}"/>
 </target>
 
 <target name="init" depends="clean">
  <mkdir dir="${build}"/>
  <mkdir dir="${build.test}"/>
  <mkdir dir="${dist}"/>
 </target>

 <target name="compile" depends="init">
  <javac srcdir="${src}"
      destdir="${build}"
      classpathref="classpath.base"/>
 </target>

 <target name="compile.tests" depends="compile">
  <javac srcdir="${src.test}"
      destdir="${build.test}"
      classpathref="classpath.test"/>
 </target> 

 <target name="test" depends="compile.tests">
   <junit printsummary="yes" fork="yes" haltonfailure="yes" >
  <classpath refid="classpath.test" />
     <formatter type="brief"/>
     <test name="com.mortgage.model.LoanTest"/>
   </junit>
 </target>

 <target name="jar" depends="compile">
   <jar jarfile="${dist}/${buildfile}" basedir="${build}"/>
 </target>

</project>

Both version compile and test successfully and the classes are similar in what they do, so similar that the tests are nearly identical but, the DRY version of the Loan class has significantly less code and is easier to understand and maintain. I leave it to the reader to imagine how from each version of the Loan class the accounting transaction could be constructed.

I first developed my DRY solution of the Loan Payment Posting hierarchy in RPG (eliminating the repeating IFs) while developing the solution for my article published in the November 2006 System i Magazine. I'm happy that the Anti-If Campaign prompted me to revisit that solution in Java.