📜 ⬆️ ⬇️

Classic Scala Design Patterns

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



Creational patterns ( Creational patterns ):



Structural Design Patterns :



Behavioral patterns of design ( Behavioral 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:


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:



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) // x = (computing x) 42 


Delayed initialization in Scala also works great with null values. Access to deferred values ​​is thread safe.

pros



Minuses



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() { // do nothing } public static Cat getInstance() { return instance; } } 


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() { // do nothing } } 


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



Minuses



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



Minuses



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) { // ... delegate.write(buffer) } } 


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) { // ... super.write(buffer) } } 


Usage example:
 new FileOutputStream("foo.txt") with Buffering // with Filtering, ... 


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



Minuses



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) { // ... return x == that.x && y == that.y; } public int hashCode() { return 31 * x + y; } public String toString() { return String.format("Point(%d, %d)", x, y); } } 


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) // new Tuple2(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) // Tuple2[Int, Int] val point: Point = (1, 2) 


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



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



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) { // by-name parameter command history :+= command _ } } Invoker.invoke(println("foo")) Invoker.invoke { println("bar 1") println("bar 2") } 


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 { // MouseHandler... protected boolean canHandle(Event event) { return "keyboard".equals(event.getSource()); } protected void doHandle(Event event) { /* ... */ } } 


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



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) { // ... repository.save(user); } } new UserService(new DatabaseRepository()); 


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 => // requires Repository def create(user: User) { // ... save(user) } } new UserService with DatabaseRepository 


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



Minuses



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.

Source: https://habr.com/ru/post/212121/


All Articles