Let’s remove the boilerplate code with execute-around pattern

Posted on by

Categories:     

100DaysOfProgramming_Day019

In this article, I will first create a thread-safe stack from scratch with a traditional approach, and then I will refactor it with an execute-around design pattern.

Stack is a last-in-first-out (LIFO) data structure. It has two main methods, push and pop. Let’s create it –

package com.bazlur;

import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrentStack<T> {
  private final Lock lock = new ReentrantLock();
  private T[] elements;
  private int size = -1;

  public ConcurrentStack() {
    this(10);
  }

  public ConcurrentStack(int initialCapacity) {
    this.elements = (T[]) new Object[initialCapacity];
  }

  public void push(T value) {
    lock.lock();
    try {
      growIfNeeded();
      elements[size] = value;
    } finally {
      lock.unlock();
    }
  }
  
  public T pop() {
    lock.lock();
    try {
      if (size == -1) {
        throw new NoSuchElementException();
      }
      trimToSizeIfNeeded();

      var element = elements[size];
      elements[size] = null;
      size--;
      return element;
    } finally {
      lock.unlock();
    }
  }

  private void growIfNeeded() {
    if (++size == elements.length) {
      grow();
    }
  }

  private void grow() {
    int newCapacity = elements.length * 2;
    elements = Arrays.copyOf(elements, newCapacity);
  }

  private void trimToSizeIfNeeded() {
    if (size < elements.length) {
      elements = Arrays.copyOf(elements, size + 1);
    }
  }
}

This is the most straightforward implementation, and it has many shortcomings; anyway, that’s beside the point.

Look at the push and pop method; one thing is very common: locking the lock and then unlocking it. This seems boilerplate, and it has nothing to do with the actual business logic of the stack.

Let’s move this boilerplate code to another place-

package com.bazlur;

import java.util.concurrent.locks.Lock;
import java.util.function.Supplier;

public class LockHelper {
  public static void withLock(Lock lock, Runnable runnable) {
    lock.lock();
    try {
      runnable.run();
    } finally {
      lock.unlock();
    }
  }

  public static <T> T withLock(Lock lock, Supplier<T> supplier) {
    lock.lock();
    try {
      return supplier.get();
    } finally {
      lock.unlock();
    }
  }
}

The first method takes a lock and a runnable. It does the locking and looking, and in between, it executes the run method of the runnable. The following method also takes a lock and a Supplier. In case we need to return something, that’s why we used Supplier here.

Let’s use these methods in the ConcurrentStack.

package com.bazlur;

import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrentStack2<T> {
  private final Lock lock = new ReentrantLock();
  private T[] elements;
  private int size = -1;

  public ConcurrentStack2() {
    this(10);
  }

  public ConcurrentStack2(int initialCapacity) {
    this.elements = (T[]) new Object[initialCapacity];
  }

  public void push(T value) {
    LockHelper.withLock(lock, () -> {
      growIfNeeded();
      elements[size] = value;
    });
  }
  
  public T pop() {
    return LockHelper.withLock(lock, () -> {
      if (size == -1) {
        throw new NoSuchElementException();
      }
      trimToSizeIfNeeded();

      var element = elements[size];
      elements[size] = null;
      size--;
      return element;
    });
  }

  private void growIfNeeded() {
    if (++size == elements.length) {
      grow();
    }
  }

  private void grow() {
    int newCapacity = elements.length * 2;
    elements = Arrays.copyOf(elements, newCapacity);
  }

  private void trimToSizeIfNeeded() {
    if (size < elements.length) {
      elements = Arrays.copyOf(elements, size + 1);
    }
  }
}

Now our code is much cleaner and doesn’t’t have any of those boilerplates locking related code.

If we want to add another method in this class, we will use a similar pattern. –

public T peek() {
    return LockHelper.withLock(lock, () -> elements[size]);
}

We are executing our code around something that seems not present before our eyes, removing noise and bringing clarity. This is why it is called execute around design pattern, and it helps to remove a lot of similar boilerplate code.

That’s for today!

Cheers!!

for copy/paste pleasure: https://github.com/rokon12/100DaysOfJava/blob/main/src/main/java/com/bazlur/ConcurrentStack2.java

           

Share on:

Author: A N M Bazlur Rahman

Java Champion | Software Engineer | JUG Leader | Book Author | InfoQ & Foojay.IO Editor | Jakarta EE Ambassadors| Helping Java Developers to improve their coding & collaboration skills so that they can meet great people & collaborate

100daysofcode 100daysofjava access advance-java agile algorithm arraylist article bangla-book becoming-expert biginteger book calculator checked checked-exceptions cloning code-readability code-review coding coding-convention collection-framework compact-strings completablefuture concatenation concurrency concurrentmodificationexception concurrentskiplistmap counting countingcollections critical-section daemon-thread data-race data-structure datetime day002 deliberate-practice deserialization design-pattern developers duration execute-around executors export fibonacci file file-copy fork/join-common-pool functional future-java-developers groupby hash-function hashmap history history-of-java how-java-performs-better how-java-works http-client image import inspiration io itext-pdf java java-10 java-11 java-17 java-8 java-9 java-developers java-performance java-programming java-thread java-thread-programming java11 java16 java8 lambda-expression learning learning-and-development linkedlist list local-type-inference localdatetime map methodology microservices nio non-blockingio null-pointer-exception object-cloning optional packaging parallel pass-by-reference pass-by-value pdf performance prime-number programming project-loom race-condition readable-code record refactoring review scheduler scrum serialization serversocket simple-calculator socket software-development softwarearchitecture softwareengineering sorting source-code stack string string-pool stringbuilder swing thread threads tutorial unchecked vector virtual-thread volatile why-java zoneid