About the author:Pavel Fatin is working on a Scala plugin for IntelliJ IDEA in JetBrains .Introduction
This article will present examples of how classic
Scala design patterns are implemented.
')
The content of the article is the basis of my speech at the
JavaDay conference (
presentation slides ).
Design Patterns
A design pattern is a common, reusable solution for common software development problems. A design pattern is not a complete code for use in solving a specific problem, but only a template that needs to be adapted for specific needs. Design patterns are best practices for effective design that help improve readability and speed up the development process.
Design patterns in the functional world
Classic design patterns are object-oriented. They show the relationships and interactions between classes and objects. These models are less applicable in purely functional programming (see
Haskell's Typeclassopedia and
Scalaz for “functional” design patterns), however, since
Scala is an object-functional programming language, these models remain relevant even in the functional world of
Scala code.
Sometimes, design patterns are a substitute for necessary constructs that are not in the programming language. In cases where the language has the necessary capabilities, the patterns can be simplified or removed altogether. In the world of
Scala, most of the classic patterns are implemented using expressive syntactic structures.
Scala has its own additional patterns, but in this article only classical design patterns will be described, because they are the key to mutual understanding between developers.
Although the examples given here can be implemented more expressively, using the full power of the
Scala language, I have tried to avoid complex techniques in favor of simple and visual ones in order to provide a clearer understanding of how the
Java implementation correlates with the
Scala implementation.
Overview of design patterns
Structural Design Patterns :
Below are the implementation of design patterns (all code is available on the
Github repository ).
Implementing Design Patterns
Factory method
The
factory method pattern
is used to provide an object creation interface, encapsulating the decision about which class objects to create.
The pattern allows:
- combine the logic of creating a complex object,
- choose a class to create an object
- cache objects
- coordinate access to shared resources.
Below are the implementations of the
Static Factory Method pattern, which is slightly different from the classic version of the
Factory Method pattern
. .
In
Java , the
new operator is used to create an instance of a class by calling its constructor. When implementing the template, we will not use the constructor directly, but will use a special method to create the object.
public interface Animal {}
public class Dog implements Animal {}
public class Cat implements Animal {}
public class AnimalFactory { public static Animal createAnimal(String kind) { if ("cat".equals(kind)) { return new Cat(); } if ("dog".equals(kind)) { return new Dog(); } throw new IllegalArgumentException(); } }
An example of using a pattern is creating a dog.
Animal animal = AnimalFactory.createAnimal("dog");
In addition to constructors,
Scala provides a special syntactic construct that is similar to constructor calls, but is actually a convenient factory method:
trait Animal private class Dog extends Animal private class Cat extends Animal object Animal { def apply(kind: String) = kind match { case "dog" => new Dog() case "cat" => new Cat() } }
Usage example:
Animal("dog")
The factory method is defined in the so-called “companion object” - a special singleton object with the same name defined in the same source file. This syntax is limited to the “static” implementation of the pattern, because it becomes impossible to delegate the creation of an object to subclasses.
Pros:
- Reuse base class name.
- Standard and concise.
- Resembles a constructor call.
Minuses:
Delayed initialization (Lazy initialization)
Delayed initialization is a special case of
deferred calculations . This pattern provides a mechanism for initializing a value (or an object) only on the first access, thereby allowing to postpone (or avoid) some expensive calculations.
Usually, when implementing a pattern in
Java , the special value
null is used to denote the uninitialized state. But, if the
null value is a valid initialized value, then an additional flag is needed to indicate the initialization state. In a multi-threaded code, access to this flag must be synchronized to avoid a race condition. More efficient synchronization can use double-check locks, which further complicates the code.
private volatile Component component; public Component getComponent() { Component result = component; if (result == null) { synchronized(this) { result = component; if (result == null) { component = result = new Component(); } } } return result; }
Usage example:
Component component = getComponent();
Scala provides a more concise built-in mechanism:
lazy val x = { print("(computing x) ") 42 }
Usage example:
print("x = ") println(x)
Delayed initialization in
Scala also works great with
null values. Access to deferred values ​​is thread safe.
pros
- Short syntax
- Deferred values ​​can also be used for null values.
- Pending values ​​are thread safe.
Minuses
- Less control over initialization.
Singleton
The
singleton pattern provides a mechanism for limiting the number of instances of a class, namely, only one object, and also provides a global access point to this object. Singleton, perhaps the most widely known design pattern in
Java , is a clear sign of the lack of a necessary construct in a language.
In
Java, there is a special keyword
static , to indicate a lack of communication with any object (class instance); methods marked with this keyword cannot be overridden during inheritance. This concept goes against the basic OOP principle that everything is an object.
So, this pattern is used when you need to have access to some global implementation of an interface (possibly with delayed initialization).
public class Cat implements Runnable { private static final Cat instance = new Cat(); private Cat() {} public void run() {
Usage example:
Cat.getInstance().run()
However, more experienced implementations of the pattern (with deferred initialization) can be described by a larger amount of code and lead to various kinds of errors (for example,
"Double-check lock" ).
Scala has a compact mechanism for implementing this pattern.
object Cat extends Runnable { def run() {
Usage example:
Cat.run()
In
Scala, objects can inherit methods from classes or interfaces. The object can be referenced (directly or via an inherited interface). In
Scala , objects are initialized deferred, on demand.
pros
- Transparent implementation.
- Compact syntax.
- Delayed initialization.
- Thread safe.
Minuses
- Less control over initialization.
Adapter
The
adapter pattern provides a mechanism for converting one interface to another, to enable classes that implement these different interfaces to work together.
Adapters are convenient for integrating existing components.
An implementation in
Java consists of creating a wrapper class that is explicitly used in the code.
public interface Log { void warning(String message); void error(String message); } public final class Logger { void log(Level level, String message) { } } public class LoggerToLogAdapter implements Log { private final Logger logger; public LoggerToLogAdapter(Logger logger) { this.logger = logger; } public void warning(String message) { logger.log(WARNING, message); } public void error(String message) { logger.log(ERROR, message); } }
Usage example:
Log log = new LoggerToLogAdapter(new Logger());
In Scala, there is a special built-in interface adaptation mechanism - implicit classes.
trait Log { def warning(message: String) def error(message: String) } final class Logger { def log(level: Level, message: String) { } } implicit class LoggerToLogAdapter(logger: Logger) extends Log { def warning(message: String) { logger.log(WARNING, message) } def error(message: String) { logger.log(ERROR, message) } }
Usage example:
val log: Log = new Logger()
Although the
Log type is expected to be used, the
Scala compiler will create an instance of the
Logger class and wrap it with an adapter.
pros
- Transparent implementation.
- Compact syntax.
Minuses
- You can get confused without using IDE.
Decorator
The
decorator pattern is used to extend the functionality of an object without affecting other instances of the same class. Decorator provides a flexible alternative to inheritance. This pattern is useful when there are several independent ways of extending functionality that can be arbitrarily combined together.
When implemented in
Java , a new decorator class is defined that inherits the base interface and wraps the original class, so that several decorators can be nested. The intermediate decorator class is often used to delegate several methods.
public interface OutputStream { void write(byte b); void write(byte[] b); } public class FileOutputStream implements OutputStream { } public abstract class OutputStreamDecorator implements OutputStream { protected final OutputStream delegate; protected OutputStreamDecorator(OutputStream delegate) { this.delegate = delegate; } public void write(byte b) { delegate.write(b); } public void write(byte[] b) { delegate.write(b); } } public class BufferedOutputStream extends OutputStreamDecorator { public BufferedOutputStream(OutputStream delegate) { super(delegate); } public void write(byte b) {
Usage example:
new BufferedOutputStream(new FileOutputStream("foo.txt"));
To achieve the same goal,
Scala provides a direct way to override interface methods, without being tied to their specific implementation.
trait OutputStream { def write(b: Byte) def write(b: Array[Byte]) } class FileOutputStream(path: String) extends OutputStream { } trait Buffering extends OutputStream { abstract override def write(b: Byte) {
Usage example:
new FileOutputStream("foo.txt") with Buffering
Delegation is set statically, at compile time, but this is usually sufficient as long as we can arbitrarily combine decorators at the time the object is created.
In contrast to the implementation based on the composition, the approach in Scala preserves the identity of the object, so you can safely use
equals on decorated objects.
In
Scala, this approach to decorating is called the
Stackable Trait Pattern .
pros
- Transparent implementation.
- Compact syntax.
- Identification of objects is saved.
- No explicit delegation.
- Lack of intermediate class decorator.
Minuses
- Static linking
- Constructors without parameters.
Value Object (Value object)
A value object is a small immutable value. Value objects are equal if all their fields are equal. Value objects are widely used to represent numbers, dates, colors, etc. In corporate applications, such objects are used as
DTO objects for interaction between processes; because of their immutability, value objects are convenient in multi-threaded programming.
In
Java, there is no special syntax for creating value objects, instead a class is created with a constructor, getter methods and additional methods (equals, hashCode, toString).
public class Point { private final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } public boolean equals(Object o) {
Usage example:
Point point = new Point(1, 2)
In
Scala , you can use tuples or case classes to declare value objects. When a separate case class is not needed, tuples can be used:
val point = (1, 2)
Tuples are predefined, immutable “collections” that can contain a fixed number of elements of various types. Tuples provide a constructor, getter methods, and all utility methods.
type Point = (Int, Int)
In cases where a distinguished class is still needed, or when more descriptive names are required for data elements, you can define a case class:
case class Point(x: Int, y: Int) val point = Point(1, 2)
Case classes make class constructor parameters properties. By default, case classes are immutable. Like tuples, they provide all the necessary methods automatically. In addition, case classes are valid classes, which means you can work with them as with ordinary classes (for example, inherit from them).
pros
- Compact syntax.
- Predefined language constructs are tuples.
- Built-in required methods.
Minuses
None.
Null Objects (Null Object)
A null object is the absence of an object, defining neutral, "inactive" behavior.
This approach has an advantage before using
null links , because there is no need to explicitly check the link before use.
In Java, the implementation of the pattern consists in creating a special subclass with “empty” methods:
public interface Sound { void play(); } public class Music implements Sound { public void play() { } } public class NullSound implements Sound { public void play() {} } public class SoundSource { public static Sound getSound() { return available ? music : new NullSound(); } } SoundSource.getSound().play();
Now there is no need to check the link obtained by calling the
getSound method before the subsequent call to the
play method. In addition, a Null object can be made singleton.
Scala uses a similar approach, using the predefined
Option type, which can be used as an “container” of an optional value:
trait Sound { def play() } class Music extends Sound { def play() { } } object SoundSource { def getSound: Option[Sound] = if (available) Some(music) else None } for (sound <- SoundSource.getSound) { sound.play() }
pros
- Predefined type.
- Explicit option.
- Built-in constructions for working with optional values.
Minuses
Strategy
The strategy pattern defines a family of encapsulated algorithms and allows you to independently change the algorithm without affecting the clients who use it. The pattern is convenient to use when you need to change the algorithm at runtime.
In
Java , a pattern is usually implemented by creating a hierarchy of classes that inherit the basic interface:
public interface Strategy { int compute(int a, int b); } public class Add implements Strategy { public int compute(int a, int b) { return a + b; } } public class Multiply implements Strategy { public int compute(int a, int b) { return a * b; } } public class Context { private final Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public void use(int a, int b) { strategy.compute(a, b); } } new Context(new Multiply()).use(2, 3);
In
Scala, there are first-class functions, so the concept of a pattern is implemented by means of the language itself:
type Strategy = (Int, Int) => Int class Context(computer: Strategy) { def use(a: Int, b: Int) { computer(a, b) } } val add: Strategy = _ + _ val multiply: Strategy = _ * _ new Context(multiply).use(2, 3)
In the case when the algorithm-strategy contains several methods, you can use case-classes or a tuple to group the methods.
pros
Minuses
Command (Command)
The command pattern is used to encapsulate the information needed to call a method at a remote point in time. This information includes the name of the method, the object to which the method belongs, and the values ​​for the parameters of the method.
In
Java, the implementation of a pattern consists of wrapping a call into an object.
public class PrintCommand implements Runnable { private final String s; PrintCommand(String s) { this.s = s; } public void run() { System.out.println(s); } } public class Invoker { private final List<Runnable> history = new ArrayList<>(); void invoke(Runnable command) { command.run(); history.add(command); } } Invoker invoker = new Invoker(); invoker.invoke(new PrintCommand("foo")); invoker.invoke(new PrintCommand("bar"));
Scala has a special mechanism for deferred computing:
object Invoker { private var history: Seq[() => Unit] = Seq.empty def invoke(command: => Unit) {
This is how you can convert any expression or code block into a function object. Calls to the
println method are executed inside calls to the
invoke method, and then stored in the
history sequence.
pros
Minuses
Chain of responsibility
The chain of responsibility pattern separates the sender of the request from his recipient, allowing several objects to process the request. The request is processed by the chain until some object processes it.
In a typical implementation of a pattern, each object in the chain inherits the basic interface and contains an optional reference to the next processing object in the chain. Each object is given the opportunity to either process the request (and interrupt processing), or pass the request to the next handler in the chain.
public abstract class EventHandler { private EventHandler next; void setNext(EventHandler handler) { next = handler; } public void handle(Event event) { if (canHandle(event)) doHandle(event); else if (next != null) next.handle(event); } abstract protected boolean canHandle(Event event); abstract protected void doHandle(Event event); } public class KeyboardHandler extends EventHandler {
Usage example:
KeyboardHandler handler = new KeyboardHandler(); handler.setNext(new MouseHandler());
Scala has a more elegant mechanism for solving such problems, namely,
partial functions (partial functions) . A partial function is a function defined on a subset of the possible values ​​of its arguments.
Although a combination of the
isDefinedAt and
apply methods can be used to build a chain, using the
getOrElse method is more
appropriate. case class Event(source: String) type EventHandler = PartialFunction[Event, Unit] val defaultHandler: EventHandler = PartialFunction(_ => ()) val keyboardHandler: EventHandler = { case Event("keyboard") => } def mouseHandler(delay: Int): EventHandler = { case Event("mouse") => }
keyboardHandler.orElse(mouseHandler(100)).orElse(defaultHandler)
It is important to note that
defaultHandler is used
here to avoid errors on “undefined” events.
pros
- Compact syntax.
- Built-in language design.
Minuses
Dependency injection
The dependency injection pattern allows you to avoid hard-coded dependencies and substitute dependencies either at runtime or at compile time. The pattern is a special case of
control inversion (IoC) .
Dependency injection is used to select from a variety of implementations of a particular component in an application, or to provide a
mock component for
unit testing .
In addition to using
IoC containers , the simplest way to implement this pattern in
Java is to pass specific implementations of interfaces (which the class uses in its work) through the parameters of the constructor.
public interface Repository { void save(User user); } public class DatabaseRepository implements Repository { } public class UserService { private final Repository repository; UserService(Repository repository) { this.repository = repository; } void create(User user) {
In addition to composition
(“HAS-A”) and inheritance
(“IS-A”) ,
Scala offers a special kind of object relationship - a requirement
(“REQUIRES-A”) - expressed in
self-type annotations.
Self-types allow you to specify additional types to an object without applying inheritance.
You can use
self-type annotations with
traits to implement a dependency injection pattern:
trait Repository { def save(user: User) } trait DatabaseRepository extends Repository { } trait UserService { self: Repository =>
The full implementation of this methodology is known as the
Cake pattern . However, this is not the only way to implement a pattern, there are also many other ways to implement a dependency injection pattern in
Scala .
It is important to recall that the “mixing” of
traits in
Scala is static, occurring at compile time. However, in practice, a configuration change is not so often required, and the additional benefits of static checking at the compilation stage have significant advantages compared to XML configuration.
pros
- Transparent implementation.
- Compact syntax.
- Static check at compile time.
Minuses
- Configuration at compile time.
- Configuration can be “verbose.”
Conclusion
I hope that this demonstration will help bridge the gap between these two languages ​​by helping
Java programmers understand the
Scala syntax, and
Scala programmers can help in matching specific language constructs to widely known abstractions.