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.
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