The Delegation Design Pattern in Scala
In this article I will briefly explain the delegation design pattern 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.
Structural Design Patterns
The delegation design pattern is a structural design pattern. Structural design patterns deal with the establishment of the relationship of simple entities through composition in order to form larger and complex structures. They also define how each entity should be structured in order for these to have a high level of flexibility.
The Delegation Design Pattern
The delegation design pattern is composed of a delegator type, that uses a delegate to provide some or all of its behavior, and this delegate type that provides the implementation required for the delegator. The following it’s an example diagram and an implementation using an EC2ConDelegator class that delegates the implementation of its connect method to the delegate class EC2ConDelegate. EC2ConDelegate class it’s private to the package in order for only the delegator to use this class and not provide access to it for the client that will be outside the package, as the client it’s supposed to be using only the delegator class.
class EC2ConDelegator(private val id: Int) {
private val delegate = new EC2ConDelegate
def connect: String = delegate.connect(id)
}
private[delegate] class EC2ConDelegate {
def connect(id: Int): String = s"singleton EC2Con connection connection with session id ${id} established"
}
object Test extends App {
val ec2Con = new EC2ConDelegator(1)
System.out.println(ec2Con.connect)
}
An Alternative to Inheritance for Code Reuse
Object oriented languages are defined by four fundamental characteristics: encapsulation, abstraction, polymorphism and inheritance. Inheritance is a singular characteristic of object oriented languages that distinguish them from a procedural language. The problem with inheritance is that if badly used it can reduce comprehensibility of code, make extensibility more complex, make maintenance harder and difficult troubleshooting.
Some of the problems related to inheritance are exemplified through the Yo-Yo problem. The term was introduced in?Problems in Object-Oriented Software Reuse?[1]. In object oriented languages when trying to understand the execution flow of a program, due to inheritance, the inheritance graph can end up being long and complicated, and a developer must be jumping up and down the class hierarchy. This makes troubleshooting more difficult and complicates extensibility as a complex understanding of the class hierarchy could be needed to extend the application. To control this problem the inheritance graph should be as shallow as possible and also there should be used other techniques for code reuse like composition through the delegation design pattern.
Problems when using inheritance arise due to the dependencies between the superclass and the subclasses. If the subclass assumes a particular behavior of the superclass from a method or combination of methods, then for implementing and extending the superclass the subclasses and this superclass needs to be understanded. If the superclass assumes a particular behavior from a method or combination of methods from the subclasses errors can arise. An apparently simple change in the implementation of a subclass can damage the operation of the application as the superclass could end up not handling correctly the behavior of the subclass. Because of this, the behavior of the superclass and the rest of the subclasses needs to be understanded for implementing and modifying the subclasses.
领英推荐
The last situation explained before typically occurs when a developer implements a couple of classes and realizes that they have some common features that can be implemented on a common superclass. This ends up producing an overly complex structure for the superclass, and ends up being a god object. A god object is an object that references a large number of distinct types, and has too many unrelated methods. Due to the comprehensibility and extensibility problems that it has, this god object is an example of an anti-pattern and a code smell. This situation is exemplified with the following code example.
case class Location(x: Int, y: Int
abstract class AnimalSound {
val obSound: String
}
class BurkSound extends AnimalSound {
val obSound: String = "BurkSound"
}
class Dog(val name: String, var location: Location) {
def movDirection(x: Int, y: Int) = {
this.location = new Location(this.location.x + x, this.location.y + y)
}
def makeSound(burkSound: BurkSound) = System.out.println(s"Dog ${name} made ${burkSound.obSound}")
override def toString: String = s"Dog ${name} on location ${location.x}, ${location.y}"
}
class Fox(val name: String, var location: Location) {
def movDirection(x: Int, y: Int) = {
this.location = Location(this.location.x + x, this.location.y + y)
}
def makeSound(burkSound: BurkSound) = System.out.println(s"Fox ${name} made ${burkSound.obSound}")
override def toString: String = s"Fox ${name} on location ${location.x}, ${location.y}"
}
object TestNonPolymorphism extends App {
val dog: Dog = new Dog("Scooby Doo", Location(4, 2))
val fox: Fox = new Fox("Robin Hood", Location(6, 4))
System.out.println(dog.toString)
dog.makeSound(new BurkSound)
System.out.println(fox.toString)
fox.makeSound(new BurkSound)
})
Supposing as shown in the previous code example that you have the implementations of the Dog and Fox classes with the common method movDirection and common method signature makeSound, and also the common attributes name and location, many programmers could be tempted to create an animal abstract superclass. This class would be composed of the common implementations and the method signatures in order to provide code reuse and a common interface which will come up with polymorphism capabilities for the client. This implementation is shown in the following diagram and code example.
abstract class Animal
val name: String
var location: Location
def movDirection(x: Int, y: Int) = {
this.location = Location(this.location.x + x, this.location.y +y)
}
def makeSound(burkSound: BurkSound): Unit
def toString: String
}
case class Location(x: Int, y: Int)
abstract class AnimalSound {
val obSound: String
}
class BurkSound extends AnimalSound {
val obSound: String = "BurkSound"
}
class Dog(override val name: String, override var location: Location) extends Animal {
override def makeSound(burkSound: BurkSound) = System.out.println(s"Dog ${name} made ${burkSound.obSound}")
override def toString: String = s"Dog ${name} on location ${location.x}, ${location.y}"
}
class Fox(override val name: String, override var location: Location) extends Animal {
override def makeSound(burkSound: BurkSound) = System.out.println(s"Fox ${name} made ${burkSound.obSound}")
override def toString: String = s"Fox ${name} on location ${location.x}, ${location.y}"
}
object TestPolymorphism extends App {
val dog: Dog = new Dog("Scooby Doo", Location(4, 2))
val fox: Fox = new Fox("Robin Hood", Location(6, 4))
val listAnimals: List[Animal] = List[Animal](dog, fox)
listAnimals.foreach[Unit](animal => {
System.out.println(animal.toString)
animal.makeSound(new BurkSound)
})
}{
Due to the implementation of the superclass, code reuse and also polymorphism are provided. Subtypes of the superclass Animal can be instantiated and managed together through a collection, like the listAnimals of this example. Nevertheless if a new class was intended to be implemented, like a Sponge class, problems then arise. The implementation of the superclass would need to be understanded to implement this subtype of the superclass. As the superclass was made ad-hoc for the Dog and Fox classes it lacks extendibility. The moveDirection method won’t be needed for the Sponge class, as sponges don’t move by their own, and it would need to hide this inherited behavior as is not appropriate for its class. Furthermore the makeSound method is incompatible with the Sponge class as BurkSound is not the type of animalSound that sponges make. Programmers can be tempted to modify the superclass method makeSound to use a class Sound parameter, but this could lead to runtime errors as the subtypes of the Animal class could be using an incorrect subtype of Sound class for the makeSound method and the compiler won’t rise any error as the method can have as parameter any subtype of Sound class.
Although this it’s a crude example, it exemplifies the problems with a bad use of inheritance that could arise in real world applications. This conjunction of code reuse and polymorphism at the same time through inheritance in object oriented languages it’s what leads to inheritance abuse. Modern languages like Haskell and Go have abandoned inheritance to separate both concepts. In order to avoid these problems that appear in Scala in these cases, it should be used composition to delegate implementation and inheritance for providing common interfaces. The following diagram and code implements the before example through this advice.
trait Animal
val name: String
var location: Location
def makeSound(): Unit
def toString: String
}
case class Location(x: Int, y: Int)
class MovAnimal {
def movDirection(location: Location, x: Int, y: Int): Location = {
Location(location.x + x, location.y + y)
}
}
trait AnimalSound {
val obSound: String
}
class BurkSound extends AnimalSound {
val obSound: String = "BurkSound"
}
class SpongeSound extends AnimalSound {
val obSound: String = "SpongeSound"
}
class Dog(override val name: String, override var location: Location) extends Animal {
private val burkSound = new BurkSound
private val movAnimal = new MovAnimal
override def makeSound() = System.out.println(s"Dog ${name} made ${burkSound.obSound}")
override def toString: String = s"Dog ${name} on location ${location.x}, ${location.y}"
def movDirection(x: Int, y: Int): Unit = {this.location = movAnimal.movDirection(location, x, y)}
}
class Fox(override val name: String, override var location: Location) extends Animal {
private val burkSound = new BurkSound
private val movAnimal = new MovAnimal
override def makeSound() = System.out.println(s"Fox ${name} made ${burkSound.obSound}")
override def toString: String = s"Fox ${name} on location ${location.x}, ${location.y}"
def movDirection(x: Int, y: Int) = {this.location = movAnimal.movDirection(location, x, y)}
}
class Sponge(override val name: String, override var location: Location) extends Animal {
private val spongeSound = new SpongeSound
override def makeSound() = System.out.println(s"Sponge ${name} made ${spongeSound.obSound}")
override def toString: String = s"Sponge ${name} on location ${location.x}, ${location.y}"
}
object TestPolymorphism extends App {
val dog: Dog = new Dog("Scooby Doo", Location(4, 2))
val fox: Fox = new Fox("Robin Hood", Location(6, 4))
val sponge: Sponge = new Sponge("Bob", Location(8, 6))
val listAnimals: List[Animal] = List[Animal](dog, fox, sponge)
listAnimals.foreach[Unit](animal => {
System.out.println(animal.toString)
animal.makeSound()
})
fox.movDirection(1, 1)
System.out.println(fox.toString)
}{
The traits Animal and AnimalSound are given for providing a common interface, with just the method signature needed for all the concrete subtypes of them. Reuse of code is managed through the delegate pattern by using BurkSound and SpongeSound for the implementation of the concrete sounds of the animals and MovAnimal for the implementation of the movDirection method that the Fox and Dog classes provides. A more comprehensibility code is obtained, and extensibility, maintenance and troubleshooting are easier, with a less prone to runtime errors.
References
[1] D. Taenzer, M. Ganti, and S. Podar. Problems in OO software reuse. In?Proc. of ECOOP, pages 33–34, 1989.
Want to connect?