Behavior Parameterization 

Copping with changing requirements and code duplication in Java

In this article we will see in a trivial example a simple way to reduce code duplication while going through the concept of behavior parameterization.

In summary, behavior parameterization is characterized as passing pieces of code as parameters to define the behavior of an execution flow.

OK, it sounds nice, but let's see it in action to visualize how this powerful concept can help us produce code closer to the problem statement while being clean, short and expressive.

The classic examples we find everywhere are the filtering and/or sorting cases for a given set. Here, we will use the filtering of a predefined set in a list of numbers to illustrate how we can apply this technique in Java.

Although it might be seem as a restricted case, we are interested here on the number of ways we can define methods that filter, and more generally, methods that “do something” and this “thing” can be done in several ways in lower levels of abstraction.  

Let’s start with a class that is able to filter a list of numbers to print the positive and negative subsets.


import java.util.*;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.toList;

public class BehaviorParameterization {

	private List< Integer > numbers;

	private BehaviorParameterization( ) {

		numbers = IntStream.rangeClosed( -10, 10 )
						   .boxed( )
						   .collect( toList( ) );
	}

	public static void main( String[] args ) {
		BehaviorParameterization example = new BehaviorParameterization();
		System.out.println( example.numbers );
		System.out.println( example.filterPositives() );
		System.out.println( example.filterNegatives() );
	}

	private List< Integer > filterNegatives( ) {

		List< Integer > numbersFiltered = new ArrayList<>( );
		for ( Integer number : numbers ) {
			if ( number < 0 ) {
				numbersFiltered.add( number );
			}
		}
		return numbersFiltered;

	}

	private List< Integer > filterPositives( ) {

		List< Integer > numbersFiltered = new ArrayList<>( );
		for ( Integer number : numbers ) {
			if ( number > 0 ) {
				numbersFiltered.add( number );
			}
		}
		return numbersFiltered;

	}
}   

It is simple, we generate a list of numbers, and code two methods that test which numbers are in the subset we want, returning that subset as a list. It is so simple that we do not even care about the amount of code duplication in this short class.

However, as always, we then have to face a reality that will hunt software engineers for ever: the change of requirements. We cannot escape, requirements are added and removed all the time. 

Let's simulate such case with a new need, filtering the numbers that are non negatives and non positives.


	private List< Integer > filterNonPositives( ) {

		List< Integer > numbersFiltered = new ArrayList<>( );
		for ( Integer number : numbers ) {
			if ( number <= 0 ) {
				numbersFiltered.add( number );
			}
		}
		return numbersFiltered;

	}


	private List< Integer > filterNonNegatives( ) {

		List< Integer > numbersFiltered = new ArrayList<>( );
		for ( Integer number : numbers ) {
			if ( number >= 0 ) {
				numbersFiltered.add( number );
			}
		}
		return numbersFiltered;

	}    

Still short, but let's image other ways of filtering, such as in a range. Let's also imagine that we now have a new code style that discouraged loops for filtering in favor of using streams or a scenario where we seek to optimize the filtering of elements in all methods, one by one.

Wouldn't be nice if we could have a method that could filter? In a higher level of abstraction, all those methods are doing the same thing: filtering.

One could think that an interface could exist to create a common idiom for this class' use cases and the behavior could be defined by implementing this interface for each case so we could have a generalized filter method. In this line of thought, we seek to move our minds to a higher abstraction level for a common behavior; thus, interface +  generalized method + implementations may sound like a reasonable path.

( We are trying to keep all in one file here to force a short example for this article, one could think of other ways, like an interface and separated classes to implement each case and/or the usage of inheritance with abstract methods, default methods, interface static methods... but let's not lose track of what we are trying to see in this article... )


	private List< Integer > filterPositives( ) {

		List< Integer > numbersFiltered = new ArrayList<>( );
		for ( Integer number : numbers ) {
			if ( number > 0 ) {
				numbersFiltered.add( number );
			}
		}
		return numbersFiltered;

	}

	private interface Test {

		boolean isTrue( Integer number );
		
	}  

Again, simple enough, we have a generalized method that can use implementations of a inner interface to check if the number belongs or not in the subset we seek. Here, we are exploring something that is not even remotely new, using an interface and a method able to consume implementations of that interface.

Let's see how we can define the behavior of this generalized method to get an implementation for each use case we need.

First, let's use the plain and old anonymous class concept.


	private List< Integer > filterNegatives( ) {

		return filter( new Test( ) {

			public boolean isTrue( Integer number ) {

				return number < 0;
			}

		} );
	}

	private List< Integer > filterNonNegatives( ) {

		return filter( new Test( ) {

			public boolean isTrue( Integer number ) {

				return number >= 0;
			}

		} );

	}

I know, a lot of code duplication here. But let's look closely. We now have only one method defining how the filtering should occur while the others simply use a template to tell that method about the test we require.

We now think: that is weird! Testing a condition is supposed to be so common, why don't we have a predefined interface for that? Well, we do! Let's try using the Predicate<T> interface.


	private List< Integer > filter( Predicate condition ) {

		List< Integer > numbersFiltered = new ArrayList<>( );
		for ( Integer number : numbers ) {
			if ( condition.test( number ) ) {
				numbersFiltered.add( number );
			}
		}
		return numbersFiltered;

	}

	private List< Integer > filterNegatives( ) {

		return filter( new Predicate( ) {

			public boolean test( Integer integer ) {

				return integer < 0;
			}

		} );
	}

	private List< Integer > filterNonNegatives( ) {

		return filter( new Predicate( ) {

			public boolean test( Integer integer ) {

				return integer >= 0;
			}
			
		} );

	}    

Well, not much of an improvement. True, but at least we did not have to define an interface.

But look! In both cases, they are single method interfaces... processing ... functional ... lambdas!


	private List< Integer > filter( Predicate condition ) {

		List< Integer > numbersFiltered = new ArrayList<>( );
		for ( Integer number : numbers ) {
			if ( condition.test( number ) ) {
				numbersFiltered.add( number );
			}
		}
		
		return numbersFiltered;

	}

	private List< Integer > filterNegatives( ) {

		return filter( integer -> integer < 0 );
		
	}

	private List< Integer > filterNonNegatives( ) {

		return filter( integer -> integer >= 0 );

	}    

OwO, one line for each new case? But, is it really necessary to create all those methods? I mean they are just uber methods for the test condition.

OK, it is true for this trivial example where they are all simple in the same class and not even reused multiple times. It happens... So, let's also get rid of the unnecessary methods, just for fun. 


	public static void main( String[] args ) {

		BehaviorParameterization example = new BehaviorParameterization( );
		System.out.println( example.numbers );
		System.out.println( example.filter( integer -> integer > 0 ) );
		System.out.println( example.filter( integer -> integer >= 0 ) );
		System.out.println( example.filter( integer -> integer < 0 ) );
		System.out.println( example.filter( integer -> integer <= 0 ) );
		
	}

That is it for this article. Although simple, this example shows something very powerful that comes handy when designing larger systems. APIs will become simpler and more expressive, while implementations will tell how they are "doing something" in a easy to read fashion.

This way of designing is encouraging a functional way of thinking that aligned with the sequential style produces code closer to the problem statement.  One does not need to go all the way up to using or forcing the usage of lambdas to use this technique, the trick is elevating the level of abstraction to reuse a piece of code by means of parameterization, not with regards to data, but about what concerns the logic.

Additionally, one does not need to go actually higher in the abstract modeling they are creating in their head, sometimes this is just forcing the expression of a semantically unsound or unnecessary piece of code. Implementing a routine by what it does in a generalized fashion is enough not to force models we do not want, utility classes and methods are easy to code. Finally, we have not mentioned here, but method references are also a great way for parameterization.

It is also worth reminding that the process of behavior parameterization via method parameters is not necessary related to streams, lambdas, method references and not even something new to Java 8+, those concepts do, however, help when implementing with a very expressive syntactic sugar set.


import java.util.*;
import java.util.function.Predicate;
import java.util.stream.*;

import static java.lang.System.out;
import static java.util.stream.Collectors.toList;

public class BehaviorParameterization {

	private List< Integer > numbers;

	private BehaviorParameterization( ) {

		numbers = IntStream.rangeClosed( -10, 10 )
						   .boxed( )
						   .collect( toList( ) );
	}

	public static void main( String[] args ) {

		BehaviorParameterization example = new BehaviorParameterization( );
		out.println( example.numbers );
		out.println( example.filter( BehaviorParameterization::isPositive ) );
		out.println( example.filter( integer -> integer >= 0 ) );
		out.println( example.filter( integer -> integer < 0 ) );
		out.println( example.filter( integer -> integer <= 0 ) );
	}

	private List< Integer > filter( Predicate< Integer > condition ) {

		return numbers.stream().filter( condition ).collect( toList() );

	}

	private static boolean isPositive( Integer number) {

		return number > 0;

	}
}  

More Blog Entries

0 Comments