Social Icons

Pages

Wednesday 17 August 2016

Is Mastering the Art of 'Embracing the Change in Software Design' Too Difficult?

Change is inevitable and we're supposed to be able to change on the fly, but is that too difficult?

We as software developers often find ourselves in situations where making a small change in the code becomes a nightmare for us. Though we acknowledge the fact that "Change is inevitable", we haven't been able to master the art of changing the Code and or the design with ease. 

Why? Have we neglected the design principles, or have we rejected the thought of evolving the design, or have we ignored the refactoring hints which we might have seen during development?

Well, it is difficult to answer but it is a mix of everything. Not believing that our design does evolve, ignoring refactoring hints, not challenging ourselves to find better solutions, etc.

Let's try and see if we can find any answer:

public class Employee{
   private EmployeeTypeE employeeType;
   public employee(EmployeeTypeE employeeType){ //other details ommitted
     this.employeeType = employeeType;
   }
   public EmployeeTypeE getEmployeeType(){ 
     return employeeType;
   }  
}

public class PayRollService(){
  public double performAppraisal(final Employee employee){
      double salary = employee.getSalary();        
      if ( employee.getEmployee() == EmployeeTypeE.CONTRACTOR ){
        salary = salary + (salary) * 0.1;            
       } 
       return salary;
    }
}

At this stage of development, some code smells are visible:

  • Does an employee need to provide a getter for EmployeeTypeE?
  • Should PayRollService be asking for the salary ? (TELL DON'T ASK?)
  • salary = salary + (salary) * 0.1  --> salary is neither a collecting variable not a looping variable, but is re-assigned.
  • peformAppraisal is using most of the data of Employee class. Should this method not move to Employee?

But, we as developers choose to ignore them. No Refactoring! Change is inevitable! And a change comes in. "We need to return the new salary and bonus for CONTRACTOR and EMPLOYEE."

public class PayRollService(){
  public double[] performAppraisal(final Employee employee){
      double salary = employee.getSalary();  
      double bonus = 0.0;
      if ( employee.getEmployee() == EmployeeTypeE.CONTRACTOR ){
        salary = salary + (salary) * 0.1;            
      } 
      else if ( employee.getEmployee() == EmployeeTypeE.EMPLOYEE ){
        salary = salary + (salary) * 0.4 + calcuationBasedOnRatingReceived();            
        bonus = 1_00_000;
      } 
      return new double[] {salary, bonus};
    }
}
And, the effective change goes this way:

We decided to:

  • Use an array for returning multiple values.
  • Use another simple if condition to handle the calculation for EMPLOYEE
  • Use magic number 1_00_000, letting the readers of the code guess what and why of 1_00_000
  • We had options and we probably chose the most illogical of all.
We could have attempted:

  • State or Strategy to handle EmployeeType
  • Making Employee an abstract class
  • Creating an abstraction say, PerformanceAppraisal containing a hike and bonus

abstract class Employee {
  public abstract PerformanceAppraisal performAppraisal();
  protected double getHikePercentage(){
    return 0.0;
  }
  protected double getDefaultBonus(){
    return 0.0;
  }
  protected double getSalary() { return salary; }
  //static method to instantiate appropriate employee or a separate factory class
}
public class PermanentEmployee extends Employee {
  protected PermanentEmployee() { 
  }
  public PerformanceAppraisal performAppraisal() {    
    double hike = getSalary() 
      + (getSalary() * getHikePercentage()) 
      + calcuationBasedOnRatingReceived();
    return new PerformanceAppraisal(hike, getDefaultBonus());
  }
  protected double getHikePercentage() {
    return 0.04;
  }
  protected double getDefaultBonus(){
    return 1_00_000;
  }
}
public class ContractorEmployee extends Employee {
  protected ContractorEmployee() { 
  }
  public PerformanceAppraisal performAppraisal() {    
    double hike = getSalary() + (getSalary() * getHikePercentage());
    return new PerformanceAppraisal(hike, getDefaultBonus());
  }
  protected double getHikePercentage(){
    return 0.01;
  }
}

Looks better than the previous approach.

Now, we will be able to handle new employee types and calculate hike/bonus for the newly added employee types. Further refactoring can be done in performAppraisal method to clearly specify how is hike computed, by moving  (getSalary() * getHikePercentage()) in a separate method.

While performing refactoring we tried to capture a few things:

  • Each employee type should be able to compute the salary using its own algorithm.
  • Code which is shared between Permanent and Contrator is moved up to the parent class.
  • Visibility of the methods and constructors was kept to minimum.
  • Magic numbers were given a name in form of a method. (Can be further refactored to use Constants).

[Assumption: Unit tests were present and were executed after each refactoring step]

I believe there are a few reasons we tend to write code which can not even speak for itself:

Lack of Awareness

I do not have an idea of what Clean Code is all about and I do not understand SOLID / DRY / KISS etc.

Mindset

I believe if/else is simple enough to understand and can be managed easily

Tendency to Ignore Code Smell(s)

I do not understand code smells and how does it impact me or my code. How much difference does it make to keep a method small or a single method which has 200 lines

Time Pressure

With the amount of time made available for development, I can not write the code which is understood by everyone. 

Design is Not Evolutionary

Once the design is done, it's done and can not be changed! Any change in the design is risky and can break everything. It is working properly in prod and should not be touched. 

I think we need to appreciate the fact that any piece of code is read more often than it is modified. We need to learn to criticize our own code, challenge our own development, understand various design principles, refactoring techniques and apply the same in development, learn to take small steps in development and accept the fact "Change is INEVITABLE".


An Attempt at Test Driven Development

"Let the tests drive the development" - Writing tests before code may sound weird at first but it does lead to a better code design, code with rich tests and a better software in the end.

Test Driven Development is based on the following ideas -
  1. Write a failing test case before writing production code
  2. Do not write any production code more than what is sufficient to pass a failing test case
  3. Do not write any test case more than what is sufficient to fail a running production code
TDD Life-cycle
Let's build a simple "Calculator" service which will provide a functionality to add two numbers.
In "Test First Development" we would write a simple failing test before we write any production code.
STEP 1:
@Test
public void addPositiveIntegersExpectValidSummation(){
   int sum = new CalculatorService().add(2, 10);
   Assert.assertEquals (12, sum);
}
At this stage this case will fail with a compilation error "CalculatorService is undefined".
So, we have written a failing a test case (Compilation error is also a failing test case), we can proceed further to write our actual code.
STEP 2:
public class CalculatorService{
   public int add(int op1, int op2) {
      return 0;
   }
}
This code is sufficient to clear the compilation error. Let's attempt to run the test case. It fails. No surprises there.
"Expected output = 12, obtained 0".
We now have a reason to change the code. But the code change should be minimum, just enough to pass the failing test case.
STEP 3:
public class CalculatorService{
   public int add(int op1, int op2) {
      return 12;
   }
}
Let's run the test case again. It passes this time. But, the solution which we have provided for the add method is is not going to work for all the cases.
Shall we change the code at this stage ? Well the answer is NO, (Based on 2nd principle of TDD).
We need to find a reason to change the code. Do we have a failing test at this stage ? The answer is NO. So we need to have one.
STEP 4:
@Test
public void addNegativeIntegersExpectValidSummation(){
   int sum = new CalculatorService().add(-2, 10);
   Assert.assertEquals (8, sum);
}
After we are done with the above step, let's attempt to run the test case.
It fails. "Expected output = 8, obtained 12". Since, we have a failing test case, we can proceed to change the code.
STEP 5:
public class CalculatorService{
   public int add(int op1, int op2) {
      return op1 + op2;
   }
}
Let's run the test cases again. This time both of them work and we are done with the functionality as well.
Let's look back at the tests and see if we can do any form of refactoring -
STEP 6:
@Test
public void addPositiveIntegersExpectValidSummation(){
   int sum = new CalculatorService().add(2, 10);
   Assert.assertEquals(12, sum);
}
@Test
public void addNegativeIntegersExpectValidSummation(){
   int sum = new CalculatorService().add(-2, 10);
   Assert.assertEquals(8, sum);
}
Duplication, right ? Well, we should get away with duplicate code (DRY - DO NOT REPEAT YOURSELF).
private CalculatorService calculatorService;
@Before
public void init(){
   calculatorService = new CalculatorService();
}
@Test
public void addPositiveIntegersExpectValidSummation(){
   int sum = calculatorService.add(2, 10);
   Assert.assertEquals(12, sum);
}
@Test
public void addNegativeIntegersExpectValidSummation(){
   int sum = calculatorService.add(-2, 10);
   Assert.assertEquals(8, sum);
}
Looks OK, appears to be following AAA principle (Arrange, Act and Assert). Let's get away with the temporary variable sum, it does not seem to be adding too much value.
STEP 7:
@Before
public void init(){
   calculatorService = new CalculatorService();
}
@Test
public void addPositiveIntegersExpectValidSummation(){
   Assert.assertEquals(12, calculatorService.add(2, 10));
}
@Test
public void addNegativeIntegersExpectValidSummation(){
   Assert.assertEquals(8, calculatorService.add(-2, 10));
}
We are done, finally!! Let's run the cases again to see them run successfully.
Successful implementation of TDD depends on -
  1. Writing a failing test before the actual code
  2. Writing the code just enough to pass the failing test
  3. Running all the tests after the code is changed
  4. Writing a new test after all the existing test cases have passed
  5. Refactoring after the test cases have passed
  6. Re-run all test cases after refactoring
How does TDD benefit us ?
  1. TDD ensures that we take small steps in development which is in line with the idea of Baby Steps in Extreme programming
  2. It lets the developers perform continuous refactoring, leading to a better code design
  3. TDD results in rich test cases
  4. Since TDD builds the software in small steps, it leads to continuous evaluation of the code changes. Any test case failure will be identified very early in the development cycle
  5. TDD does take anxiety away
To summarize, in TDD, test code and production code go hand in hand but test code precedes the production code.