The Factory Design Patterns in?Scala
In this article I will briefly explain what factory patterns are and why they are useful, and then explain the different factory patterns: factory method and abstract factory, using some code examples. This will be done from the perspective of Scala, a pure object and functional oriented language. Code snippets can be found on my github.
Design Patterns
In software there are a lot of industries such as mobile phone, website, big data, etc. These industries tackle a huge load of programming problems. Nevertheless in programming we usually come up with problems that are the same that other programmers have tackled or somehow similar, and there is a reusable solution that is considered the most correct way and is like a blueprint of how the problem needs to be solved, that is a design pattern. These design patterns are needed sometimes due to the lack of expressiveness of the language, that it doesn’t make available a simple solution. The expressiveness and the functionalities available in a programming language also can make available some additional design patterns that other programming languages don′t, due to the lack of certain functionality. The purpose of these design patterns is to obtain code that is more efficient, extensible, testable and readable.
Design Patterns in Scala
Scala it’s a rich expressive language and it makes some of the design patterns not necessary or simpler. Also, due to its functional oriented capabilities, it’s gonna make additional design patterns available that other traditional object oriented programming languages don’t have.
Factory Pattern
The factory pattern it’s classified as a creational design pattern. Creational design patterns deal with the complexity of object creation providing a better alternative from direct object creation. The complexity of object creation comes in the form of a large number of parameters, difficulties obtaining these parameters and difficulties in the validation of the objects created with these parameters. The factory pattern provides a software component named factory that deals with the complexity of object creation for the client and decouples the client code from the code that creates the objects. This makes the object creation code more extensible as it can be extended without needing to change the code that uses it.
Factory Method
The factory method will be the first of the factory patterns to be explained. In this pattern the client uses a factory object that instantiates different classes that inherit from a given trait or superclass. The following diagram describes this.
The ProductFactory is used as an object instead of a class due to the fact that there is only gonna be one instance for it, as this instance doesn’t need parameters and doesn’t have any attributes. Code it’s more extensible as it can be extended the Product class or trait with a new implementation of it without changing other code. Furthermore, more concise and easy to use code it’s achieved because in the client code there is no need to execute concrete methods of each Product implementation as all Product implementations share the same methods signatures and the concrete product implementations can be used together. This pattern deals also with the complexity of object creation as no parameters are needed for instantiating each concrete Product implementations and the complexity of obtaining the parameters is also managed transparently for the client. Also validation of the objects instantiated it’s managed automatically as no parameters from the user are given, it can only be instantiated concrete implementations provided for the Product ensuring that the Product class or trait cannot be instantiated with an incorrect set of parameters.This was a generic example but a concrete example is shown in the next diagram.
In this diagram a factory it’s implemented called RederFactory used to instantiate different implementations of Reader: csvReader, jsonReader and parquetReader. Each of them with its concrete implementation of the open procedure. This is called procedure instead of method because it does not have a return type. The logic of it does not intend to produce an output, but instead is intended to produce side effects.
The Factory and the ReaderFactory are implemented the following way.
trait Factory { def apply(s: String): Reader } object ReaderFactory extends Factory { def apply(s: String): Reader = { var pos = s.lastIndexOf(".") if (pos < 0) { pos = 0 } val endsWith = s.substring(pos) endsWith match { case ".csv" => csvReader(s) case ".json" => jsonReader(s) case ".parquet" => parquetReader(s) case _ => throw new RuntimeException("Unknown file type") } } }
This is more extensible than letting the client create the concrete Reader implementations, cause in case the name of the Reader implementations change, instead of needing to change then the client code, with this approach only the ReaderFactory is needed to be changed. In the apply method the end of the string it’s used to check the type of the document which it’s intended to be readed and based on the type of document a csvReader, jsonReader or parquetReader it’s used. If the file doesn’t correspond to any of the expected files a RuntimeException is thrown. It is used an apply method due to the fact that this method it’s called implicitly when ReaderFactory is given arguments. The following calls are the same: ReaderFactory(s) = ReaderFactory.apply(s). This is a syntactic sugar that allows the client to call ReaderFactory like the following.
object Test extends App { val csvReader = ReaderFactory("file.csv") csvReader.open() val jsonReader = ReaderFactory("file.json") jsonReader.open() val parquetReader = ReaderFactory("file.parquet") parquetReader.open() }
Using this approach it’s optional and it is thought to be dangerous. Nevertheless if the factory is well documented there shouldn’t be problems and using this approach makes the ReaderFactory feel like native language support, and this is one of the main goals of Scala, to develop libraries that feel they have native language support for the client.
The Reader and its subtypes were implemented the following way.
abstract class Reader { var path: String def open(): Unit } private case class csvReader(var path: String) extends Reader { override def open(): Unit = System.out.println(s"csvReader opened csv on path ${path}") } private case class jsonReader(var path: String) extends Reader { override def open(): Unit = System.out.println(s"jsonReader opened json on path ${path}") } private case class parquetReader(var path: String) extends Reader { override def open(): Unit = System.out.println(s"parquetReader opened parquet on path ${path}") }
An abstract class was used for Reader although a trait could also be used to implement it. In cases when the piece of code must be reused in multiple or unrelated classes or complex class hierarchies, traits are more useful. Since this is a simple class hierarchy where the implementations naturally models a class and its subclasses, an abstract class has been used. Each of the subclasses provides its specific implementation of the procedure open.
The factory method nonetheless has its drawbacks and they can arise when there are more than one factory methods, as incompatibilities can arise. It could be added a second abstract class named Options with its specific implementations for csv, json and parquet. The Reader class implementations csvReader, jsonReader and parquetReader will use the specific Options implementations for them through the options method. This is shown in the following diagram.
The Options abstract class and its implementation are the following.
abstract class Options { def getOptions(): String } class csvOptions extends Options { override def getOptions(): String = "csv options specific for csv files" } class jsonOptions extends Options { override def getOptions(): String = "json options specific for json files" } class parquetOptions extends Options { override def getOptions(): String = "parquet options specific for parquet files" }
And the Reader and its implementations have changed like the following to obtain specific Options for them and use them in the method open.
abstract class Reader { var path: String def open(): Unit def options: Options } private case class csvReader(var path: String) extends Reader { override def open(): Unit = System.out.println(s"csvReader opened csv on path ${path} with ${options.getOptions}") override def options = new csvOptions } private case class jsonReader(var path: String) extends Reader { override def open(): Unit = System.out.println(s"jsonReader opened json on path ${path} with ${options.getOptions}") override def options = new jsonOptions } private case class parquetReader(var path: String) extends Reader { override def open(): Unit = System.out.println(s"parquetReader opened parquet on path ${path} with ${options.getOptions}") override def options = new parquetOptions }
Nonetheless It could also have been done the following for the csvReader for example.
private case class csvReader(var path: String) extends Reader { override def open(): Unit = System.out.println(s"csvReader opened csv on path ${path} with ${options.getOptions}") override def options = new jsonOptions }
In this last case the options for csvReader are incorrect as I wrote the options for jsonReader, but the compiler won’t alert any error. These logic errors are a headache to debug and can arise easily as more factory methods are implemented and incompatibilities could be caused.
Furthermore in case we want to provide a new specific method to one of the Reader implementations, it’s needed also to provide this method to the abstract Reader class and through all other implementations of Reader.
Abstract Factory
The abstract factory is the second pattern from the factory patterns. The purpose of this pattern as with the rest of factory patterns it’s encapsulating the logic of the object creation, and hide it from the user. An Abstract Factory is a logical group of factory methods where each factory method provides the creation of a different kind of object. A Factory Provider using the client input or some configuration will access the Concrete Factory that implements the Abstract Factory, and it will provide the specific objects of each kind needed for the client. This approach provides the same extensible benefits of factory method, as the different kinds of products can be extended and changed their name without modifying client code. The following diagram describes this pattern.
There is the Abstract Factory which provides the interface supplied by the factories to the clients. The concrete factories implement the Abstract Factory methods through which it instantiates the different concrete objects of each kind. And the factory of factories or Factory Provider provides the appropriate concrete factory. The concrete factories are implemented as singleton objects as a client only needs one instance per family of products.
The factory method pattern is preferred over abstract factory pattern when there are few factory methods, but as the number of methods arise it makes sense to group them on concrete factories that return the specific products of each kind compatible between them.
The following it’s a diagram of an example implemented for the abstract factory method pattern similar to the one of the factory method pattern.
There are two families of objects, the ones related with csv, csvReader and csvOptions, each of a different kind, Options and Reader. Also there are objects related to the json family. The concrete implementations of the AbstractFactory, that it’s the csvFactory class and jsonFactory class instantiate each of the previous families of objects. The Options and Reader implementations are the following.
abstract class Options { def getOptions(): String } class CsvOptions extends Options { override def getOptions(): String = "csv options specific for csv files" } class JsonOptions extends Options { override def getOptions(): String = "json options specific for json files" } abstract class Reader { def open(options: Options): Unit } private case class CsvReader(var path: String) extends Reader { override def open(options: Options): Unit = System.out.println(s"csvReader opened csv on path ${path} with ${options.getOptions}") } private case class JsonReader(var path: String) extends Reader { override def open(options: Options): Unit = System.out.println(s"jsonReader opened json on path ${path} with ${options.getOptions}") }
The Options implementation returns a string specific to their kind of options through the getOptions method. The different Reader implementations through open method print a string based on the kind of reader, their path attribute and the options parameter.
As it can be observed the different kinds of classes are instantiated separately and not inside each other like in the factory method, where the Option implementations were instantiated inside the Reader implementations. This makes the abstract factory method pattern more testable than factory method pattern because the classes can be tested independently, using mock objects.
These families of objects are instantiated through AbstractFactory implementations and are the following.
abstract class AbstractFactory { var options: Options def reader(path: String): Reader } class CsvFactory extends AbstractFactory { override var options: Options = new CsvOptions override def reader(path: String): Reader = CsvReader(path) } class JsonFactory extends AbstractFactory { override var options: Options = new JsonOptions override def reader(path: String): Reader = JsonReader(path) }
They provide access to the Options object through options attribute and to the Reader through the reader method that uses an input path string as input for instantiating the concrete Reader implementation for the concrete AbstractFactory implementation. The client will access the specified AbstractFactory implementation through the FactoryProvider based on the system variable, as it is shown below.
object FactoryProvider { private val default: AbstractFactory = new CsvFactory private val factories: mutable.HashMap[String, AbstractFactory] = mutable.HashMap("json"->new JsonFactory, "csv"->default) def factory = factories.getOrElse(System.getProperty("fileType"), default) }
It is preferable to grant access for clients to the specific factory they need to through environment variables or system variables, if it is possible, rather than through their input as this tends to be error prone. The client will interact with the FactoryProvider like how it is shown below.
object Test extends App { val csvFactory = FactoryProvider.factory val options = csvFactory.options val reader = csvFactory.reader("path.csv") reader.open(options) }
Nevertheless incompatibility problems can arise the same as in the factory method pattern in case in the implementation of the Factory incompatible objects are created and the compiler won’t raise any error.
Want to connect?