Skip to content

Instantly share code, notes, and snippets.

@virtix
Created January 31, 2011 19:32
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save virtix/804634 to your computer and use it in GitHub Desktop.
layout title published draft draft_message pub_date
post
An Immutable Object Strategy in ColdFusion
true
false
_DRAFT - Subject to change_
February 1, 2011

{% if page.draft } %(draft){{ page.draft_message }} {%endif%}

{{ page.title }}

{{ page.pub_date }} – {{site.location}}

This piece will briefly discuss concepts of immutability—what it is, why you should care, and how you can implement them in ColdFusion. Immutability is also an important piece in functional programming, which is very appealing from a testing perspective—I’ll touch on that towards the end.


What are immutable objects?

Simply stated, once an immutable object is created you can’t change it. There are typically no setter methods or public instance members, and the constructors are strictly controlled. The result are immutable objects. A factory method can also be used to generate or build immutable objects.

Why?

If this concept seems strange, consider these ideas:

  1. If an object’s state doesn’t change, it makes testing much easier.
    • You test the state of the object only after it’s been created. You’re not as focused on testing methods that alter the state of the object, simply because they don’t exist.
  2. Immutable objects are inherently thread safe. [BLOCH]
    • Since no thread can change it’s state, all threads read the exact same data.
    • They can be shared freely.
  3. Your state-based unit tests may be fewer and more reliable.
    • I’m speculating here, but your state-based tests may need only test those methods that create immutable objects. Perhaps, one set of tests that work on one set of creational methods could also be used to test other creational methods. (This would be a good area to look at further)

There are down sides, of course. The most obvious is the cost associated with object creation and garbage collection. For example, consider java.lang.String, a classic immutable object. Once a String is created, it cannot be changed. Period. With software that does a lot of String manipulation, you can end up with a lot of string objects in need of garbage collecting. To answer this problem, StringBuilder was implemented (StringBuffer is the predecessor). So, you may need to create a mutable version of your object that should be used with specific intent.

What does this look like?

In its simplest form, you (1) control how objects are created, and (2) make sure the object’s state can’t be changed. Though there are a number of ways to do this, I’ll present one method using ColdFusion 9.

This example represents a mathematical expression abstraction. An expression will look something like this: (2*4) + (5^2) * 10. The main thing we do is control the default constructor, init. By requiring a string expression as a parameter, we’ve controlled how instances are created.

component name="ImmutableExpression" {

	rawExpression='';
	evaluatedExpression='';

  
  public ImmutableExpression function init(required String expression) {
    if( !structKeyExists(arguments,"expression") ) {
  	 throw(  type="ObjectInstantiationException", 
  	    		message="This component requires a single string parameter to be created." );
    } 
    
    rawExpression = expression;
    
    try{
     evaluatedExpression = evaluate(expression);
    }
    catch(Expression e){
     throw( type="InvalidExpressionException",
            message="This doesn't look like a valid expression: " & expression.toString() );
    }
    
    return this;
  }



 public Numeric function value(){
   return evaluatedExpression;
 }



 public String function getExpression(){
  return rawExpression;
 } 


  
public String function toString(){
   return "Expression : #rawExpression# = #evaluatedExpression#;" ;
 }
 
 
}

The above controls ColdFusion’s default constructor init by requiring a single argument. The ObjectInstantiationException is for clarity, otherwise the operation would fail at rawExpression = expression; and not express our intent.

In our Expression object, we want to maintain the original string representation and the result of evaluating the expression. Once we create an expression, we’re done. Equally as important, any state is maintained by private instance variables. And there are no setters and no direct way to change this object’s state.

A unit test for the constructor could look something like:

component extends="mxunit.framework.TestCase" {
 
 
  function twoExpressionsShouldNotBeTheSame(){
      exp1 = new ImmutableExpression( "3^3" );
      exp2 = new ImmutableExpression( "4*2" );
      assertNotSame( exp1, exp2 );
 }

 /**
  * @expectedException InvalidExpressionException
  */
 function badExpressionShouldBeKickedOut(){
	  exp1 = new ImmutableExpression( "1*b" );
  }

}

Let’s make this a little more interesting …

With these expressions, which are somewhat related to polynomials, perhaps we’d like to add them:

  exp1 = new ImmutableExpression( "3^3" );
  exp2 = new ImmutableExpression( "4*2" );
  exp3 = exp1.add(exp2) ;

The take-home here is in the 3rd line. The intent is to effectively add exp2 to exp1, returning a new ImmutableExpression. exp3.getExpression() will return (3^3) + (4*2) and exp3.value() will return 35

The implementation is straight forward:

...

public ImmutableExpression function add (ImmutableExpression exp){
   return new ImmutableExpression( "(" & rawExpression & ") + (" & exp.getExpression() & ")" ); 
 }

...

Again, the point here is that we choose not to alter the state of an expression once created and so, we create new ImmutableExpression objects when we need to actually have the expression interact with another object, such as add(...). This is in the style if functional programming


Caveat

  • ColdFusion doesn’t offer the ability to annotate a component as final , so you have to check the type being instantiated at runtime to prevent it from being extended. Why? If a component can be extended its methods can be overridden and possibly break your immutable intention.
...

 myComponentType = "sandbox.mutability.ImmutableExpression";
 private void function ensureInstanceType(){
    if ( getMetaData(this).fullName != myComponentType )
    	throw ( type="InheritanceViolationException",
    			message="This component is not intended to be extended." );
 }


...




h2. Wrap up

Creating immutable objects is fairly straightforward and makes writing unit tests more effective, and immutable objects are inherently thread safe. However, care must be taken when creating software that could generate lots of immutable objects, and a mutable version of the object could be added to the API.


 

Functional Programming

In mathematics, a function f(n) always returns a value and never alters the function arguments. So, if you have an add(1,1) function, it will guarantee to return something, hopefully 2, and it will guarantee not to change either of the parameters (1 or 1). With respect to programming, we’re usually passing in variables that are evaluated at runtime to some value. In a functional style, you do not change the state of the variables being passed in. In some functional languages, this constraint is a feature, but in many languages it is not. So, you need to do this yourself by cloning.

cfcomponent {
 
 public any function filter(string predicate, any collection){
   if( isArray(collection) ) return _filterArray(predicate,collection, [] ); 
   if( isStruct(collection) ) return _filterStruct(predicate,collection, {} );  
   return collection;  
 }about software
 
 //slightly different implementation for arrays
 public any function _filterArray(string predicate, array collection, array accumulator){
   for(item in collection){about software
   if( isCollection(item) ) arrayAppend(accumulator, filter(predicate, item, accumulator) );
    else if(refindnocase(predicate, item)) arrayAppend(accumulator, item);
   }
   return accumulator;
 }

//slightly different implementation for structs
 public any function _filterStruct(string predicate, struct collection, struct accumulator){
   var _a = [];
   for(item in collection){
   if( isCollection(collection[item]) ) structInsert(accumulator, item, filter(predicate, collection[item], accumulator) );
    else if (refindnocase(predicate, collection[item])) structInsert(accumulator, item, collection[item]);   
   }
   return accumulator;
 }

 
//full lib at https://github.com/virtix/balisong/raw/master/Balisong.cfc

}



Source code examples: https://gist.github.com/804634

References:
Josh Bloch, Effective Java

Barbar Liskov, Program Development in Java

♫ Inspiration
Korn
Bill Evans

<cfscript>
component name="ImmutableExpression" {
rawExpression='';
evaluatedExpression='';
/**
* Constructor
*/
public ImmutableExpression function init( required String expression ) {
ensureInstanceType();
rawExpression = expression;
try{
evaluatedExpression = evaluate(expression);
}
catch(Expression e){
throw( type="InvalidExpressionException",
message="This doesn't look like a valid expression: " & expression ,
extendedInfo=e.getDetail());
}
return this;
}
/**
* Creates a new ImmutableExpression object, adding a 2nd expression.
*/
public ImmutableExpression function add (ImmutableExpression exp){
return new ImmutableExpression( "(" & rawExpression & ") + (" & exp.getExpression() & ")" );
}
/**
* Creates a new ImmutableExpression object, adding a 2nd expression.
*/
public ImmutableExpression function mul (ImmutableExpression exp){
return new ImmutableExpression( "(" & rawExpression & ") * (" & exp.getExpression() & ")" );
}
/**
* Returns the result of the evaluated expression.
*/
public numeric function value(){
return evaluatedExpression;
}
/**
* Returns the raw string expression.
*/
public String function getExpression(){
return rawExpression;
}
/**
* A string representation of this object.
*/
function toString(){
return "Expression : #rawExpression# = #evaluatedExpression#;" ;
}
myComponentType = "sandbox.mutability.ImmutableExpression";
private void function ensureInstanceType(){
if ( getMetaData(this).fullName != myComponentType )
throw ( type="InheritanceViolationException",
message="This component is not intended to be extended." );
}
}
</cfscript>
<cfscript>
component extends="mxunit.framework.TestCase" {
/**
*@expectedException Application
*/
function emptyConstructorShouldFail(){
exp1 = new ImmutableExpression();
}
/**
* @expectedException InheritanceViolationException
*/
function childOfImmutableInstanceShouldNotBeInstatiated(){
exp1 = new ImmutableExpressionChild( "2+1" );
}
/**
* @expectedException InvalidExpressionException
*/
function badExpressionShouldBeKickedOut(){
exp1 = new ImmutableExpression( "1*b" );
}
function multiplyTwoExpressionsTest(){
exp1 = new ImmutableExpression( "3^3" );
exp2 = new ImmutableExpression( "4*2" );
exp3 = exp1.mul(exp2) ;
debug(exp3.toString());
assertEquals( 216, exp3.value() );
}
function addFourExpressionsTest(){
exp1 = new ImmutableExpression( "3^3" );
exp2 = new ImmutableExpression( "4*2" );
exp3 = exp1.add(exp2) ; //35
exp4 = new ImmutableExpression( "23/2*32" );
exp5 = new ImmutableExpression( "(2*5)*4" );
exp6 = exp4.add(exp5) ; //35
exp7 = exp6.add( exp3 );
debug(exp1.toString());
debug(exp2.toString());
debug(exp3.toString());
debug(exp4.toString());
debug(exp5.toString());
debug(exp6.toString());
debug(exp7.toString());
assertEquals( exp7.value(), 443 );
}
function addTwoExpressionsTest(){
exp1 = new ImmutableExpression( "3^3" );
exp2 = new ImmutableExpression( "4*2" );
exp3 = exp1.add(exp2) ;
debug(exp3.toString());
assertEquals( exp3.value(), 35 );
}
function twoExpressionsShouldNotBeTheSame(){
exp1 = new ImmutableExpression( "3^3" );
exp2 = new ImmutableExpression( "4*2" );
assertNotSame( exp1, exp2 );
}
function expressionTest(){
exp = new ImmutableExpression( "3^3" );
debug( exp.toString() );
}
function expressionWillBeEvaluatedIfPassingInNotInQuotes(){
exp = new ImmutableExpression( 3^3 ); //27
debug( exp.toString() );
assertEquals(27,exp.value());
assertEquals(27,exp.getExpression());
}
}
</cfscript>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment