Software Engineer | Technical Evangelist

Blog PostsResume

Combining Singleton Pattern with Parameterised Factory Method Pattern

Factory method defines an interface for creating an object and lets the sub-classes decide which class to instantiate. It lets a class defer instantiation to subclasses. Singleton design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance.

In this blog, we look at an approach of combining the singleton design pattern with parameterised factory method. Consider an example of a File Reader library that lets a client read CSV and Excel files.

Factory UML

The participants in Factory Method Pattern are:

  • FileReader (Product): defines the interface of objects the factory method creates to read files.
  • CSVFileReader/ExcelFileReader (ConcreteProduct): implements the Product interface.
  • FileReaderFactory (Creator): declares the factory method, which returns an object of type FileReader.

Let’s start with an interface FileReader. We’ll only have one method in our contract readFile, which returns the list of records read from the input file.

public interface FileReader {
    List<String[]> readFile(String file);
}

Let’s create classes for the supported file format that implements the FileReader interface. These classes can be singleton as we are not storing any class attributes that may change.

@Singleton
public class CSVFileReader implements FileReader {

    private final CSVParser csvParser;

    @Inject
    public CSVFileReader(@NonNull final CSVParser csvParser) {
        this.csvParser = csvParser;
    }

    @Override
    public List<String[]> readFile(final String file) throws Exception {
        try (final BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
             final CSVReader csvReader = new CSVReaderBuilder(bufferedReader).withCSVParser(csvParser).build()) {
            return csvReader.readAll();
        } catch (final IOException e) {
            throw new Exception("Error occurred");
        }
    }
}
public class ExcelFileReader implements FileReader {

    @Override
    public List<String[]> readFile(final String file) throws Exception {
        try (
                final FileInputStream fileInputStream = new FileInputStream(file);
                final Workbook workbook = new XSSFWorkbook(fileInputStream)) {
            final Sheet datatypeSheet = workbook.getSheetAt(0);
            final List<SampleData> sampleDataList = new ArrayList<>();
            ....
            ....
            return sampleDataList;
        } catch (final IOException e) {
            throw new Exception("Error occurred");
        }
    }
}

Now let’s create our factory method. The Creator class contains a Map<String, FileReader> that is an injectable map declared using MapBinder. For each file-format, the map returns a respective singleton instance of FileReader.

final MapBinder<String, FileReader> mapBinder =
        MapBinder.newMapBinder(binder(), String.class, FileReader.class);

mapBinder.addBinding(CSV_FORMAT).to(CSVFileReader.class);
mapBinder.addBinding(EXCEL_FORMAT).to(ExcelFileReader.class);

The factory method createReader() returns an instance of FileReader based on the input file format.

/**
 * Creator Class
 */
@RequiredArgsConstructor
public class FileReaderFactory {
    private final Map<String, FileReader> fileReaderMap;

    public FileReader createReader(final String fileType) {
        if (!fileReaderMap.containsKey(fileType)) {
            return null;
        }
        return fileReaderMap.get(fileType);
    }
}

The code to use your factory would look like this.

@Override
public String clientMethod() throws Exception {
    final FileReader csvFileReader =
            fileReaderFactory.createReader("csv");
    final List<String[]> csvFileContent = csvFileReader.readFile("/tmp/a.csv");
    ....
    ....
    final FileReader excelFileReader = fileReaderFactory.createReader("excel");
    final List<String[]> excelFileContent = excelFileReader.readFile("/tmp/a.xlsx");
    ....
    ....
}

Finally, we create a @Provides method for FileReaderFactory. We should ensure that its singleton.

public class DefaultModule extends AbstractModule {
    @Provides
    @Singleton
    public FileReaderFactory fileReaderFactory(
            final Map<String, FileReader> fileReaderMap) {
        return new FileReaderFactory(fileReaderMap);
    }
}

You can find the sample code here


© 2021 Ujjwal Bhardwaj. All Rights Reserved. (Source code)