scala decorator design pattern with real world example

The Decorator Pattern is part of the structural design pattern and this is a pattern which attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to sub classing for extending functionality. Its build on the design principle “Classes should be open for extension, but closed for modification.”

The goal is to allow classes to be easily extended to incorporate new behavior without modifying existing code. So our design will be tough to change and flexible enough to take on new functionality to meet changing requirements. The purpose of the decorator design pattern is to add functionality to objects without extending them and without affecting the behavior of other objects from the same class.

The decorator design pattern works by wrapping the decorated object and it can be applied during runtime. Decorators are extremely useful in the cases where there could be multiple extensions of a class and they could be combined in various ways. Instead of writing all the possible combinations which can lead into class explosion, decorators can be created and additional functionality can be applied at runtime.

Below is the generic class diagram for decorator pattern

Lets say we wanted to apply some calibration on the data we get from ems and what calibration to apply is decided on the basis of what vendor a specific device type belongs to so when we started this desing pattern below is the class diagram we implemented

But after sometime there was additional requirement where we needed to apply additional calibration based on the techtype,policytype and device type also. Initially we thought of creating a one class for each combination of vendor and property type but then we realized that it would create too many classes and also there was a high possibility that there will be additional properties would be added to the list which would cause in creating many more classes.So here we considered using decorator pattern so that we can dynamically add functionality into objects.

Below is the class diagram

So we create an abstract CalibrationDecorator which extends VendorCalibration and then we add the calibrate method to each of the property classes and we delegate the functionality from each property classes to calculate the calibration for the vendor type . Below is a snippet so in the below snippet we first apply the calibration based on the vendor type and then based on techtype and then finally on the devicetype.


val calibratedData:Double = new DeviceTypeCalibration(new TechTypeCalibration(new HuwCalibration()))
.getCalibration(rawData);

Lets code the example

trait VendorCalibration {

def getCalibration(rawdata: Double): Double;

}


class HuwCalibration extends VendorCalibration {

override def getCalibration(rawdata: Double): Double =
{
return rawdata * 0.99876;
}

}


class BrocadeCalibration extends VendorCalibration {

override def getCalibration(rawdata: Double): Double =
{
return rawdata * 0.967;
}

}


class CiscoCalibration extends VendorCalibration {

override def getCalibration(rawdata: Double): Double =
{
return rawdata * 0.967;
}
}

Lets code the decorator abstract class and the concrete classes


trait CalibrationDecorator extends VendorCalibration {

}


class PolicyCalibration(vendorCalibration: VendorCalibration) extends VendorCalibration {

def getCalibration(rawdata: Double): Double =
{
return vendorCalibration.getCalibration(rawdata) * 0.9898
}
}

Here we delegate the calibration to be calculated based on the vendor type using the composed vendorCalibration


class TechTypeCalibration(vendorCalibration: VendorCalibration) extends VendorCalibration {

def getCalibration(rawdata: Double): Double =
{
return vendorCalibration.getCalibration(rawdata) * 0.9876
}
}


class DeviceTypeCalibration(vendorCalibration: VendorCalibration) extends VendorCalibration {

def getCalibration(rawdata: Double): Double =
{
return vendorCalibration.getCalibration(rawdata) * 1.1122
}
}


object Test extends App {

val rawData:Double = 2;

val calibratedData:Double = new DeviceTypeCalibration(new TechTypeCalibration(new HuwCalibration()))
.getCalibration(rawData);

System.out.println(calibratedData);

}