Some basic steps to creating an immutable class:
- Make constructors private and use factory methods to create objects.
- Make fields
final
, forcing them to be instantiated in a constructor. - Make class
final
.
Some of the basic principles of immutability are easy to adopt and Static Analysis tooling can help enforce and remind us to think about adopting those principles.
In this section we will explore some simple transformations we can use to convert a mutable class to an immutable class.
While it would be tempting to try and create a single recipe 'this class is mutable - fix it to be immutable'. Such a recipe would be hard to build and maintain. It is often easier to create smaller recipes to handle intermediate step transformations, and allow the programmer to make decisions about which steps to apply, rather than trying to cram a lot in an single recipe.
Try these recipes on the mutable
classes or when using the AbuseOpenCoordinates
in the test folder's immutability.exercise
package.
A simple immutable class can be impacted if we can extend it.
Making it final
can help prevent that.
Before:
public class MutableCoordinates {
After:
public final class MutableCoordinates {
To warn me about this, and create a QuickFix action to help fix it I can use Sensei as follows:
Name - Immutable: use final classes to prevent extension
Description - Make the classes final to prevent extension with a mutable subclass
Level - Warning
Recipe Settings:
search:
class:
allOf:
- modifier: public
without:
modifier: final
QuickFix:
availableFixes:
- name: "Remove public modifier and make final"
actions:
- changeModifiers:
final: true
Before:
private int x;
private int y;
public Coordinates(){
x=0;
y=0;
}
After:
private int x;
private int y;
private Coordinates(final int x, final int y ){
this.x=x;
this.y=y;
}
public final static Coordinates create(final int x, final int y ){
return new Coordinates(x, y );
}
Name - Immutable: default constructor should set field values from parameters
Description - avoid default constructor and create a private constructor that sets the field values
Level - Warning
In the recipe below I match on a default constructor:
search:
constructor:
without:
child:
argument: {}
And then perform two actions:
- rewrite the constructor to be private, and force instantiation of the fields
- add a static factory method which returns a new object
availableFixes:
- name: "amend to private constructor that sets fields with a static factory create\
\ method"
actions:
- rewrite:
to: "private {{{ name }}}({{#sed}}{{#encodeString}}s/(.*),/$1/{{/encodeString}},{{#encodeString}}\n\
{{#containingClass.fields}}final {{typeElement}} {{name}}, {{/containingClass.fields}}{{/encodeString}}{{/sed}}){\n\
{{#containingClass.fields}}this.{{name}}={{name}};\n {{/containingClass.fields}}\n\
}"
target: "self"
- addMethod:
method: "public static final {{{ name }}} create({{#sed}}{{#encodeString}}s/(.*),/$1/{{/encodeString}},{{#encodeString}}\n\
{{#containingClass.fields}}final {{typeElement}} {{name}}, {{/containingClass.fields}}{{/encodeString}}{{/sed}}){\n\
return new {{{ name }}}({{#sed}}{{#encodeString}}s/(.*),/$1/{{/encodeString}},{{#encodeString}}{{#containingClass.fields}}{{name}},\
\ {{/containingClass.fields}}{{/encodeString}}{{/sed}});\n}"
position:
after:
constructor: {}
target: "parentClass"
The above recipe looks pretty complicated because of all the embedded sed
functionality.
Also I used the Mustache foreach
construct to iterate over all the fields. When writing a foreach
in the template it is important to use {{
rather than {{{
and to have no spaces when matching on the #
i.e. {{#containingClass.fields}}
not {{{ #containingClass.fields }}}
- why might the 2nd representation crop up? Because when we add variables from the Show Variables
list, in order to have the text unescaped (i.e. normal) we add it to the template as {{{
.
Before:
private int x;
After:
private final int x;
This recipe is a good candidate to use with the "Fix All" Context Menu Action.
Name: Immutable: Fields should be final and set in the constructor
Description: Making fields final can highlight mutability issues
Level: Warning
search:
field:
without:
modifier: final
availableFixes:
- name: "make fields `final`"
actions:
- rewrite:
to: "{{{ modifierList }}} final {{{ typeElement }}} {{{ name }}};"
target: "self"
If I applied all of the above recipes on my class I would still have some steps to implement because my code would no longer be syntactically correct.
Because my fields are final, my setter methods would no longer pass a syntax check:
public void setX(int x){
this.x = x;
}
public void setY(int y){
this.y = y;
}
And my transform
method would fail for the same reason:
public void transform(int xAdjust, int yAdjust){
this.x+=xAdjust;
this.y+=yAdjust;
}
Ideally I would want to remove the void methods and return a newly constructed Immutable object.
I can automatically remove any public
void
setter methods, and rely on the static factory method or constructor.
Before:
public void setX(int x){
this.x = x;
}
public void setY(int y){
this.y = y;
}
After:
NOTE: the methods were completely removed.
This is another QuickFix that is a good candidate for tryin the "Fix All" functionality.
In my recipe I match any method with a name beginning with set
which has been declared as public
with a return type of void
. And then have a rewrite action to delete the code.
Name - delete public void setters
Description - void setters can be replaced with use of constructor or static factory methods
Level - Warning
search:
method:
with:
modifier: "public"
returnType: "void"
name:
matches: "set.*"
availableFixes:
- name: "Delete The public void method"
actions:
- rewrite:
to: ""
target: "self"
If I had a non-void setter method then I probably don't want to automatically transform this code, as I would need to manually amend my code to avoid the need to return a value from a setter in the first place.
I could create a recipe to alert me to this condition.
Name - Immutable: avoid setters that return values
Description - avoid setters methods that return values
Level - Error
search:
method:
with:
modifier: "public"
returnType:
not: "void"
name:
matches: "set.*"
availableFixes:
- name: "Fix the code by manual refactoring"
actions: []
Void methods mean that a side-effect has probably taken place and really I want to return a new Immutable object.
That can be a complicated refactor because we may need to perform complex setup, so this refactoring is partial in that it just removes the void
, triggering a syntax error to remind me that I need to return an immutable object.
Before:
public void transform(int xAdjust, int yAdjust){
this.x+=xAdjust;
this.y+=yAdjust;
}
After:
public Object transform(int xAdjust, int yAdjust){
this.x+=xAdjust;
this.y+=yAdjust;
}
Then I would manually amend it to fix the syntax error e.g.
public Coordinates transform(int xAdjust, int yAdjust){
return new Coordinates(this.x + xAdjust, this.y + yAdjust);
}
Name - Immutable: avoid void methods
Description - void methods have side-effects, return a new oject or primitve instead
Level - Warning
search:
method:
not:
modifier: private
in:
class:
name:
not:
matches: .*Test
returnType: void
I added the extra check to not match in Test classes because I do often have void
methods in Test classes for support and setup code.
availableFixes:
- name: "Make this return an Object"
actions:
- changeType:
type: "Object"
Tempting as it may be to create fields which are public final
and avoid writing getters.
Having an abstraction in place to create a longer term interface and hide the internal implementation seems like a sensible option.
So I wrote a recipe to convert my public
fields without getters to private fields with getX
methods.
Before:
public int x;
After:
private int x;
public final int getX(){
return x;
}
Name - Immutability: use a getter rather than public field
Description - To encourage use of interfaces rather than direct field access, add a getter.
Level - Warning
This search looks for a field which is public and not in a typeDeclaration
which contains a child method named getX (where X is the name of the field).
search:
field:
not:
in:
typeDeclaration:
with:
child:
method:
name:
caseSensitive: false
is: get{{{markedElement.name}}}
modifier: public
Note: the markedElement
gives me access to the publi field as a template.
availableFixes:
- name: "add a getter and make private"
actions:
- addMethod:
method: "public final {{{markedElement.typeElement}}} get{{#upperCaseFirst}}{{{markedElement.name}}}{{/upperCaseFirst}}(){\n\
\ return {{{markedElement.name}}};\n}"
target: "parentClass"
- changeModifiers:
visibility: "private"
The QuickFix adds a 'get' method which is public final
and returns the 'type', then changes the field visibility to private
.
Some of the basic concepts of immutability are fairly simple:
- prevent extension of the class by making it
final
- avoid setter methods and rely on fully constructed objects either from a constructor, or a static factory method
- avoid public
void
methods to reduce external side-effects - make fields
final
to prevent internal side-effects
But the correct combination of transformations to choose varies depending on the context of the class and how far we want to take immutability.
To support the adoption of immutability, creating a contextual set of Sensei recipes can help flag the problem, and offer choice to the programmer about which set of transformations to apply.
Use the AbuseOpenCoordinates
class in the test
folder in immutability.exercise
. The tests in AbuseOpenCoordinatesTest
should start failing as you migrate the class to become more immutable.