Edit Page

Lab 2: The singleton design pattern

The goal of this lab is to apply a creational design pattern, the singleton, to solve a commonly occurring problem where we only need a single instance of a class to be created once and shared with all system components.

Singleton is a commonly used design pattern in many programming languages. It is used when one instance of a class is needed. In this lab activity, you will writer a logger class that should write all log messages in a single file.

Video

Objectives

In this lab you will

  1. understand a real-world scenario and choose when to apply the appropriate design pattern.
  2. design and implement the Singleton design pattern.
  3. write unit tests and apply Test-Driven Development (TDD).

Requirements and Tools

Getting Started

If your instructor is using GitHub classroom, you will need to accept the assignment using the link below, clone the template repository, and import it as a project into your IDE.

If your instructor is not using GitHub classroom, clone and import the template project at https://github.com/cpit252/lab-02.

Problem Statement

Logging is the process of keeping a record of events happening in a system in the form of messages.

A developer is working on an order tracking and shipment system. She needs to create a unique logger object that is used and shared by all instances in the system to log messages from any application component. She thought of implementing it as a singleton class and started with the following code:

Logger.java


import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.io.PrintWriter;
import java.io.FileWriter;

public class Logger {
  DateTimeFormatter myFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy-HH-mm-ss");
  String currentDateTimeString = LocalDateTime.now().format(myFormatter);
  private final String logFile = currentDateTimeString + ".txt";
  private PrintWriter writer; 

  public Logger() {
    try {
      FileWriter fw = new FileWriter(logFile);
      writer = new PrintWriter(fw, true);
    }
    catch (IOException ex) {
      ex.printStackTrace();
    }
  }
  public void info (String message) {
    writer.println("INFO: " + message);
  }
  public void error (String message) {
    writer.println("Error: " + message);
  }
}

And the rest of the classes in her system are as follow:

Shipment.java


import java.util.Random;

public class Shipment {
  private int trackingNumber;
  private String name;
  private String address;
  private String phoneNumber;
  private Logger log = new Logger();

  private int getRandomNumber(){
    Random ran = new Random();
    return ran.nextInt(Integer.MAX_VALUE);
  }
  public Shipment(String name, String address, String phoneNumber){
    // Emulate slow initialization.
    try {
      Thread.sleep(2000);
    } catch (InterruptedException ex) {
      ex.printStackTrace();
    }

    this.trackingNumber = getRandomNumber();
    this.name = name;
    this.address = address;
    this.phoneNumber = phoneNumber;
    log.info("A new shipment was created");
    log.info(this.toString());
  }
  public String toString(){
    return "Shipment info:\nTracking number: " + this.trackingNumber +
      "\nName" + this.name + "\nAddress: "+ this.address +
      "\nPhone: " + this.phoneNumber;
  }
}

Order.java

import java.util.Random;
import java.time.LocalDate;

public class Order {
  private int orderNumber;
  private LocalDate orderDate;
  private Logger log = new Logger();
  private int getRandomNumber(){
    Random ran = new Random();
    return ran.nextInt(Integer.MAX_VALUE);
  }
  public Order(){
    // Emulate slow initialization.
    try {
      Thread.sleep(2000);
    } catch (InterruptedException ex) {
      ex.printStackTrace();
    }
    
    this.orderNumber = getRandomNumber();
    this.orderDate = LocalDate.now();
    log.info("A new order was created");
    log.info(this.toString());
  }
  public String toString(){
    return "Order info:\nOrder number: " + this.orderNumber +
      "\nDate" + this.orderDate;
  }
}

And the client/main class is as follows:


public class App{
  public static void main(String[]args){
    Order o1 = new Order();
    Shipment sh1 = new Shipment("Ahmed", 
        "248 NE. Pleasant St. Niceville, FL 32578",
        "123-477-0001");
  }
}

Upon Running the main program, she discovered that her code generates more than one log file per run, which is not the expected behavior.

running the program results in two log files instead of one

Note: To see the output files after running your program in your IDE, go to the file directory where you saved your project in. For example, on Windows, go to File Explorer and browse to Documents\NetBeansProjects\ProjectName, and you should see two text files instead of one. You may need to delete previously generated text files if you have alreadt run the program multiple times.

The application should generate only one log file per run instead of two or more log files. She believes that her Singleton implementation is wrong, so she wrote the following unit test to check her implementation:

The following unit test ensures that the code always returns a single instance of the Logger class.

LoggerTest.java

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class LoggerTest 
{
  @Test
  public void shouldBeIdenticals()
  {
    Logger log1 = new Logger();
    Logger log2 = new Logger();
    assertEquals(log1, log2);
  }

}
Hint: If this is your first time writing tests with JUnit, see writing JUnit Tests in Java under miscellaneous.

Question: Fix the current implementation of the Logger class, so it will only return a single instance of the Logger class and all test cases in the unit test are passing.

Deliverables and Submission

Extra Task [Optional]

If you are done with this activity, you may enable a continuos integration tool such as CircleCI ↗ to automatically run your JUnit test upon code changes. You may also embed the status image/badge that shows the status of your build and test (passing/failing) into your README file (e.g., passing status image and failing status image). Please be sure to push to another repository under your own account, so you can enable the integration of CI tools in your account.

You may refer to the lecture notes from the prerequisite course CPIT-251 on Continuous Integration (CI) and adding a code coverage badge.