top bar

Difference between revisions of "Using Builders"

(Why immutability)
(Why immutability)
Line 109: Line 109:
  
 
=== Why immutability ===
 
=== Why immutability ===
 +
 +
Making objects immutable is a common technique used to simplify data handling in complex programs, especially multithreaded ones.
  
 
At first glance it would seem like making objects that you aren't allowed to modify just makes your life a lot more complicated. And it is true that there are a few more steps to follow to create a modified Metadata than there would be if you could just modify the Metadata in-place. However, the fact that these objects are immutable is vital for Micro-Manager's image workflows, because it means that no piece of code has to worry about an object being changed "out from under it".
 
At first glance it would seem like making objects that you aren't allowed to modify just makes your life a lot more complicated. And it is true that there are a few more steps to follow to create a modified Metadata than there would be if you could just modify the Metadata in-place. However, the fact that these objects are immutable is vital for Micro-Manager's image workflows, because it means that no piece of code has to worry about an object being changed "out from under it".

Revision as of 09:14, 16 April 2015

This is a guide to using the Builder pattern, which is used extensively in the Micro-Manager 2.0 API for creating objects like image coordinates and metadata.

Overview

Let's use the Metadata class as an example. This class describes the metadata for a single image in Micro-Manager. Below is a simplified extract from the interface definition, with only five properties:

public interface Metadata { 
   interface MetadataBuilder {
      Metadata build();
      MetadataBuilder bitDepth(Integer bitDepth);
      MetadataBuilder channelName(String channelName);
      MetadataBuilder exposureMs(Double exposureMs);
      MetadataBuilder pixelSizeUm(Double pixelSizeUm);
      MetadataBuilder userData(PropertyMap userData);
   }
   MetadataBuilder copy();
   Double getExposureMs();
   Double getPixelSizeUm();
   Integer getBitDepth();
   PropertyMap getUserData();
   String getChannelName();
}

There are two classes: the Metadata itself, and the Builder for the Metadata. Note that the Metadata does not have any methods that change it: it is impossible to modify a Metadata instance after it is created (the Metadata is "immutable"). Instead, a Builder is created, and it can be modified (for justification of why these objects are immutable, see the bottom of the page). However, properties of the Builder cannot be read, so it isn't very useful on its own. To generate a Metadata from the Builder, you use the build method. For example:

Metadata.MetadataBuilder builder = mm.data().createMetadataBuilder();
// Create metadata for first image.
builder = builder.bitDepth(14).channelName("DAPI").exposureMs(100);
Metadata metadata1 = builder.build();
int exposure1 = metadata1.getExposureMs(); // will be 100
// Create metadata for second image: only channel name and exposure time change
builder = builder.channelName("Cy5").exposureMs(80);
Metadata metadata2 = builder.build();
Integer exposure2 = metadata2.getExposureMs(); // will be 80
Double pixelSize = metadata2.getPixelSizeUm(); // will be null!
// Don't do this: it would throw a NullPointerException:
// double pixelSize = metadata2.getPixelSizeUm();

There's a few things to call your attention to here:

  • Since every method on a Builder except the build() method returns the Builder, you can chain those methods together to set several properties in a single statement.
  • The Builder object can be re-used to generate multiple Metadata instances with only minor variations.
  • Any properties not set in the Builder will also not be set in the Metadata. In this case, that means that the values are null. It is important that you use the "boxed" objects like Integer, Double, and Boolean when accessing properties of a Metadata, as opposed to using int, double, or boolean. The latter will throw a NullPointerException if you try to assign a null to them.

In addition to using a "raw" Builder object to construct new Metadata objects, you can also ask a Metadata object for a Builder that's based on it, using the copy() method. The above example code could equivalently be written as:

Metadata.MetadataBuilder builder = mm.data().createMetadataBuilder();
// Create metadata for first image.
builder = builder.bitDepth(14).channelName("DAPI").exposureMs(100);
Metadata metadata1 = builder.build();
// Create metadata for second image: only channel name and exposure time change
Metadata metadata2 = metadata1.copy().channelName("Cy5")
      .exposureMs(80).build();

This can be useful in situations where you don't have access to the original Builder.

Variations

The above example was specific to the Metadata class. There are some variations for other Micro-Manager 2.0 objects, which are covered here.

Coords

Coords objects are very similar to Metadata objects. They have a set of convenience functions for commonly-used coordinate axes: channel, time, z, and stagePosition. They also use the int datatype to represent coordinates, and thus cannot use null to indicate a missing value like Metadata does. Instead, missing values are represented by -1.

Here is an example of creating a new Coords object:

Coords.CoordsBuilder builder = mm.data().createCoordsBuilder();
// Note that coordinates start counting from 0!
// Thus this image will be the second channel, 5th Z slice.
builder.channel(1).z(4);
// You can also set custom axes.
builder.index("polarization", 3);
Coords coords = builder.build();
coords.getChannel(); // 1
coords.getZ(); // 4
coords.getIndex("polarization"); // 3
Coords coords2 = coords.copy().z(5).build();
coords2.getZ(); // 5
coords2.getIndex("polarization"); // 3

Images

Images do not have Builder objects because they are comparatively simple: they consist of a pixel data array, some parameters that are required to interpret that array, the Coords describing the image's location, and a Metadata instance. You can use the DataManager to create a new Image directly:

Image image = mm.data().createImage(pixels, width, height, bytesPerPixel,
      numComponents, coords, metadata);

(See the documentation on the DataManager for more information on these parameters)

You can also create a new Image from an existing Image by varying its coordinates, metadata, or both:

Image copy1 = image.copyAtCoords(newCoords);
Image copy2 = image.copyWithMetadata(newMetadata);
Image copy3 = image.copyWith(newCoords, newMetadata);

Thus for example, if you wanted to change the location of an image prior to adding it to a Datastore, you could do the following:

Datastore store = mm.data().createRAMDatastore();
// Snap an image; this image will have incorrect coordinates
Image origImage = mm.live().snap(false).get(0);
// Fix the coordinates.
Coords.CoordsBuilder builder = mm.data().createCoordsBuilder();
Coords coords = builder.channel(1).z(4).build();
Image newImage = origImage.copyAtCoords(coords);
store.putImage(newImage);

Why immutability

Making objects immutable is a common technique used to simplify data handling in complex programs, especially multithreaded ones.

At first glance it would seem like making objects that you aren't allowed to modify just makes your life a lot more complicated. And it is true that there are a few more steps to follow to create a modified Metadata than there would be if you could just modify the Metadata in-place. However, the fact that these objects are immutable is vital for Micro-Manager's image workflows, because it means that no piece of code has to worry about an object being changed "out from under it".

Without that guarantee, it is impossible for code to guarantee consistency. For example, if an image's metadata says it's for the Cy5 channel, then nobody can change that image to say it's actually for the DAPI channel instead. Code can create a new image that is identical except for its channel, but that new image does not replace the old one anywhere else (it's a new reference).

Imagine you have a plugin that saves images to different files depending on their channels, and some third-party code changes the channel string while you're running. Depending on exactly when the string changes, an image might end up in a different file! Making images immutable guarantees that you will never have this problem; if the third-party code wants that image to be in a different channel, then it will create a new image, while you will still have the old one.

Isn't this horribly inefficient?

All this copying whenever you want to make a change seems like it would be very inefficient. Fortunately this isn't the case. When you make a "copy" of, say, an Image, all you are copying is the 'references to the image's data, not that data itself. For example, if the original Image and the new Image only differ in their coordinates, then they will "share" image pixel data and Metadata, and only have different Coords objects. This holds true within Metadata and SummaryMetadata as well: if you create a new Metadata by copying an existing one and changing just the channel name, then the new and old objects will share everything else.

Consequently, copying an object is an extremely cheap action. The immutability guarantee actually allows us to use less memory than we would have to if objects were mutable, because it allows two objects that are very similar to share their resources.

© Micro-Manager : Vale Lab, UCSF 2006-2011 | All Rights Reserved | Contact