Java 8 Optional and objects with dynamic structure. Part 3


Image source: PIRO4D pixabay.com

In this post I will continue the topic Optional in Java 8, started in

Java 8 Optional and objects with dynamic structure. Part 1

and continued in the

Java 8 Optional and objects with dynamic structure. Part 2

Examples of code for these posts you will find on my page in GitHub:

https://github.com/vsirotin/Smartenesse-Java

Let me remind you that we are considering objects with a dynamic structure and their implementation using Java 8 Optional. Under the dynamic nature of the structure, we mean the property of objects depending on various factors in some sense to have or not have any of their parts. Very often the absence of a part of the object means that some expected functions of the object are not provided at the time of the request. So, in the example with an office boiler, such functions are two – to give out cold water or to give boiled water.

This definition can cause legitimate objections from some readers. After all, if water is not supplied to the input of the device and therefore it does not function, it does not become “less” and none of its components cease to exist. This remark is true, but that’s why I focused your attention on the fact that this is one of the possible definitions.

In the first post, we looked at ways to solve the problem of the dynamic structure of an object in the early versions of Java before Optional there appeared in Java 8. In the second post, we considered a simple example of solving one of these tasks using Optional.

In these posts, we looked at the problem in a sense with Java. In this category of my blog, called “Materialization of Ideas”, I try to practically substantiate the thesis that the development of software is nothing more than the materialization of ideas. At least, I believe that this view of our profession speeds up and simplifies our work and improves its results.

What gives this approach in the case of the dynamic structure of objects? Let’s try to look at the problem from the other side. Namely: consider the typical tasks that we have to deal with when working with these kinds of objects and how Optional can help us by their solutions.

If you like, this can be called a solution to a certain range of tasks. Personally, I believe that this is somewhat different than the programming patterns in the classical sense. But I’ll write about it somehow in one of my following blogs

Ability to abstract tasks is one of the most important skills of architects and developers of software systems. What abstractions exist and how they relate to each other – the debate on this topic lasts for decades. Personally, I am of the opinion that the hierarchy of object abstractions in programming is useful to conduct on the basis of their semantics. If this is the case, then at the top of our hierarchy of abstractions there will be only a few processes. Among them, one of the most important and most commonly used is the process with the name ETL … (Extract – Transform – Load) was historically unsuccessfully fixed. In other words, a lot of what our systems are doing can be reduced to a sequence of three operations:

  • Extracting an object from something
  • Transformation of the object (changing its parts or convertion it into a new object)
  • Writing (uploading) an object somewhere

Java 8 uses more modern terminology for this purpose, replacing extract with supply and load with consume. Accordingly, we will further on this blog use the term Supplier to designate the creator or transmitter of data for transformation and Consumer for the consumer or data receiver after the transformation. Thus, instead of ETL patern, we will talk about the STC (Supplier – Transformer – Consumer) patern.

Optional in Supplier

The office boiler considered in the previous post is an example of an object of the Supplier type. The essence of his work is to give out boiled or cold water. The results of his work depend on external conditions, which were set with the help of Boolean variables. But in most practical problems, not simple variables, but objects, are used as the input. Sometimes they can be null.

Consider how you can apply Optional if the Supplier behavior is not defined by Boolean variables, but by conventional objects that allow null values.

Case for the object

Let the input of our boiler be a slightly different model than in the first example is defined as follows:

public interface IBoiler2 extends IBoilerInput2, IBoilerOutput {}

The null-value of the water object means that water does not flow into the appliance from the water supply.
Then the behavior of the device as a whole is specified by the following interface:

public interface IBoiler2 extends IBoilerInput2, IBoilerOutput {}

As in the previous example, we define tests that verify the correctness of our implementation:

public class Boiler2Test {
   
   private IBoiler2 boiler;
   
   @Before
   public void setUp() throws Exception {
      boiler = new Boiler2();
   }

   @Test
   public void testBothNotAvailable() {            
      boiler.setAvailability(null, false);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   
   @Test
   public void testPowerAvailable() {                
      boiler.setAvailability(null, true);
      assertFalse(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testWaterAvailable() {    
      boiler.setAvailability(new CupOfWater(), false);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertFalse(boiler.getCupOfBoiledWater().isPresent());
   }
   
   @Test
   public void testBothAvailable() {     
      boiler.setAvailability(new CupOfWater(), true);
      assertTrue(boiler.getCupOfWater().isPresent());
      assertTrue(boiler.getCupOfBoiledWater().isPresent());
   }
}

If we compare these tests with the tests for the boiler of the first model, we will see them very similar. Checking the results of the same tests from different sets is the same. Only instead of true for the source of water we supply the object, and instead of false – null.

And here is the implementation itself:

public class Boiler2 implements IBoiler2 {
   
   @Nullable
   private CupOfWater water;
   private boolean powerAvailable;
   

   @Override
   public void setAvailability(@Nullable CupOfWater water, boolean powerAvailable) {
      this.water = water;
      this.powerAvailable = powerAvailable;
   }

   @Override
   public Optional getCupOfWater() {
      return Optional.ofNullable(water);
   }

   @Override
   public Optional getCupOfBoiledWater() {
      if(!powerAvailable)return Optional.empty();
      return getCupOfWater().map(cupOfWater->cupOfWater.boil());
   }
}

As we see, the method Optional.ofNullable() allows you  elegantly to “put” a dangerous object with a potentially null value in the “case” (Optional). If the object is null, the case will be empty. Otherwise, it contains the object.

Let us now consider another, not so rare, situation where a certain resource is represented as the main and reserve element.

It’s good to have a reserve..

In technical systems, resources of the same species can often be accessed in more than one way.

In the following example, we will consider a simple water supply device. Rainwater is collected in a special container, which is then consumed first. If water in container is over, water is spent from the water pipe.

We will not complicate the task with details about the incomplete fullness of the rainwater tank and its size and simply use the familiar CupOfWater class.

The input of such a device is described thus:

public interface IWaterDispenserInput {
   
   void setAvailability(@Nullable CupOfWater firstPortion);
}

If rainwater is not collected, then at the entrance we have a null, otherwise – a normal object.

The output of the device is described by the following interface:

public interface IWaterDispenserOutput {
   
   CupOfWater getCupOfWater();
}

Note that at the output we have a CupOfWater object and not an Optional object. We do so in order to show the mechanism that interests us more clearly. After you, dear readers, understand it, you can easily reprogram the example and get it on the output of Optional.
The behavior of the device as a whole is determined by the sum of these interfaces:

public interface IWaterDispenser extends IWaterDispenserInput, IWaterDispenserOutput {}

As in the previous examples, we prepare first tests to verify the behavior of our implementation:

public class WaterDispenser1Test {
    private IWaterDispenser waterDispenser;

    @Before
    public void setUp() throws Exception {
        waterDispenser = new WaterDispenser1();
    }

    @Test
    public void testMainAvailable() {
        waterDispenser.setAvailability(new CupOfWater());
        assertNotNull(waterDispenser.getCupOfWater());
    }


    @Test
    public void testMainNotAvailable() {
        waterDispenser.setAvailability(null);
        assertNotNull(waterDispenser.getCupOfWater());
    }
}

Our expectations are as follows: the device issues water regardless of whether the tank with rainwater is full or not, because in the latter case, water will take from the “reserve” (water supply).
Let us now consider the implementation:

public class WaterDispenser1  implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElse(new CupOfWater());
    }
}

As we can see, in the call chain after the method ofNullable() was added the method orElse(). If the first element returns the empty Optional (no rainwater is accumulated), the second method adds an object from itself. If the first method gives a non-empty Optional, the second method will simply pass it through itself and the reserve will remain untouched.
This implementation assumed the existence of a backup object. If the object before this is necessary to create (in our case – to pump water), one can use the method orElseGet() with a parameter of type Supplier:

public class WaterDispenser2 implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElseGet(()->new CupOfWater());
    }
}

Do not let the gin out of the bottle

In some cases, restrictions on your API do not allow you to use Optional as the return value.

Suppose that our interface is defined in such a way that the client always expects an object at the output of our function. If the requested resource is not present at the time of the request, and we do not want to return null, we have to throw Exception. Thus, we do not release the gin from the bottle – we do not let the released null – object turn into NullPoiner Exception into client code.

Can we help in this case Java 8 Optional? Yes. It can.

But before considering the solution, we will prepare a test that verifies the correctness of its work:

@Test  (expected = IllegalStateException.class)
public void testMainNotAvailable() {
    waterDispenser.setAvailability(null);
    waterDispenser.getCupOfWater();
    fail("This code line must be not reached");
}

And here is the solution:

public class WaterDispenser3 implements IWaterDispenser{
    @Nullable private CupOfWater mainCup;

    @Override
    public void setAvailability(@Nullable CupOfWater mainCup) {
        this.mainCup = mainCup;
    }

    @Override
    public CupOfWater getCupOfWater() {
        return Optional.ofNullable(mainCup).orElseThrow(()->new IllegalStateException("Resource not available"));
    }
}

I think many readers will not be convinced of this decision. In fact, what is in this code better than checking for null with if?

The main argument in favor of this solution is the ability to build chains of functional calls. But to think about how to do this, I invite you, dear readers, as an exercise.

I, on my part, will try in one of the subsequent posts to propose one interesting (in my opinion) solution to the problem of processing Exception in chains of functional calls.

In the next post in this series, I’ll try to talk about using Java 8 Optional in the remaining phases of the STC process-with Transform and Consume.

In addition, we will try to find the answer to the question whether Option is a part of functional programming.

Leave a Reply

Your email address will not be published. Required fields are marked *