Design patterns makes designs and communicating about the designs much easier. By applying the proper design patterns, the we can get rid of boiling plates code, make the code more flexible maintainable, no mention the code is safer and easier to be reused.
Java 7 introduced lambda functions, which allow us to pass functions around as variable. By combining the design patterns and java lambda expressions, we now get even conciser and more powerful code.
Lambda expressions can act as a design tool. In stead of wrapping a function inside an object and pass the object around, we can pass the lambda function as a variable around. Long gone the days of writing lengthy inner classes, welcome to the world of lambda functions.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test {
public static void main(String[] args) {
// Thread t = new Thread(new Runnable() {
// public void run() {
// System.out.println("in thread");
// }
// });
Thread t = new Thread(() -> System.out.println("in thread"));
t.start();
System.out.println("in main");
ExecutorService exec = Executors.newSingleThreadExecutor();
exec.execute(() -> System.out.println("in executor thread"));
exec.shutdown();
}
}
in thread
in main
in executor thread
Strategy patten
One of the most commonly used design pattern is strategy pattern. We have a Duck class which makes different sound depending on the situations. Instead of passing in a concrete class which provides makeSound function, we pass in an interface with makeSound function. At run time, we can have many concrete implementation of that interface, the Duck class remain the same, but it can make different sounds. Now with lambda expression, the same pattern looks like the following:
public class Duck {
public static void main(String[] args) {
QuackBehavior duckCry = () -> System.out.println("quack!");
FlyBehavior duckFly = () -> System.out.println("fly with wings");
QuackBehavior dummyCry = () -> System.out.println("no sound ...");
FlyBehavior dummyFly = () -> System.out.println("wooden duck don't fly");
Duck duck = new Duck(duckCry, duckFly);
duck.cry();
duck.fly();
duck = new Duck(dummyCry, dummyFly);
duck.cry();
duck.fly();
duck = new Duck(dummyCry, duckFly);
duck.cry();
duck.fly();
}
private QuackBehavior quack;
private FlyBehavior fly;
public Duck(QuackBehavior quack, FlyBehavior fly) {
this.quack = quack;
this.fly = fly;
}
public void cry() {
this.quack.makeSound();
}
public void fly() {
this.fly.fly();
}
private interface QuackBehavior {
public void makeSound();
}
private interface FlyBehavior {
public void fly();
}
}
Decorator Pattern
Recall our famous Starbuzz Beverages Example.
|
starbuzz decorator example |
We have an abstract class Beverage with the two methods getDescription() and cost(). The getDescription() method is already implemented for us, but the cost() method is abstract.
public abstract class Beverage {
String desc = "some beverage";
public String getDescription() {
return desc;
}
public abstract double cost();
}
Then there are a few coffee type classes that extend the Beverage. For example,
public class DarkRoast extends Beverage {
public DarkRoast() {
super.desc = "DarkRoast";
}
@Override
public double cost() {
return .99;
}
}
Now the decorators.
The super class of all sorts of decorators is and abstract class that extends Beverage, it force the subclasses to provide the description.
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
A decorator extends CondimentDecorator, it takes the wrapped object as a parameter in constructor, then store it as an instance variable, so that when later the getDescription() or cost() is called, it can add new states to the wrapped object, like a decorator does.
public class Milk extends CondimentDecorator {
Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Milk";
}
public double cost() {
return beverage.cost() + 0.10;
}
}
public class Soy extends CondimentDecorator {
Beverage beverage;
public Soy(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Soy";
}
public double cost() {
return beverage.cost() + 0.11;
}
}
Finally, the decorated objects can be wrapped by another decorator class, and we can do this wrapping forever.
public class StarbuzzCoffee {
public static void main(String...args) {
Beverage coffee = new DarkRoast();
coffee = new Milk(coffee);
coffee = new Soy(coffee);
System.out.println(coffee.getDescription());
System.out.println(coffee.cost());
}
}
output:
DarkRoast, Milk, Soy
1.2000000000000002
What we have learned from the example? We can start from the outmost layer of wrapper class, apply its behavior function, then apply the inner layer's behavior function, which in turn, after applying its own behavior function, then calls another layer of inner wrapper class's behavior function, like a chain reaction.
Let's open the LambdaStarBuzzCoffee shop, hope you enjoy it. Wait, where are the coffees, shop and condiments in this futuristic shop?
import java.util.function.*;
import java.util.stream.Stream;
public class LambdaBeverage {
public String getDescription(String desc, Function<String, String>...descriptionDecorators) {
return Stream.of(descriptionDecorators).reduce(Function.identity(), Function::andThen).apply(desc);
}
public double cost(double cost, Function<Double, Double>...costDecorators) {
return Stream.of(costDecorators).reduce(Function.identity(), Function::andThen).apply(cost);
}
public static void main(String...args) {
Function<Double, Double> soyCost = i -> i + 0.11;
Function<Double, Double> milkCost = i -> i + 0.10;
Function<String, String> soyDesc = i -> i + ", soy";
Function<String, String> milkDesc = i -> i + ", milk";
LambdaBeverage LambdaCoffee = new LambdaBeverage();
System.out.println(LambdaCoffee.getDescription("darkroast", soyDesc, milkDesc));
System.out.println(LambdaCoffee.cost(0.99, soyCost, milkCost));
}
}
output:
darkroast, soy, milk
1.2000000000000002
Fluent interface pattern
In software engineering, a fluent interface is an object-oriented api whose design relies extensively on method chaining. Its goal is to increase code legibility by creating a domain specific language. For example, instead of pass in the private field values as constructor parameters. They are set in the setter functions. The setter function returns the object itself, therefore we can chain the setter methods together to get a more fluent interface.
public class Kitten {
private String name;
private String color;
public Kitten name(String name) {
this.name = name;
return this;
}
public Kitten color(String color) {
this.color = color;
return this;
}
public void play() {
System.out.println(name + " has " + color + " fur");
}
public static void main(String[] args) {
new Kitten().color("red").name("Mina").play();
}
}
The drawback in the above pattern is, we have to create the object with new keyword. It is a drawback because:
- the user of Kitten class don't necessarily concern about how the Kitten object is created, as long as it has name and color property.
- the code is not flexible. If we want to get the Kitten object from an object pool instead, the burden has to be put to the user, who might be a simple user and should not deal with tech aspects such as object pool, reflection, spring annotation etc.
The LambdaKitten class get the fluent interface design pattern further. Instead of having the user to create the Kitten object with new keyword, the Kitten object is created within the LambdaKitten class itself. The user's responsibility is to specify the name and color of a given kitten. This way, the LambdaKitten class can change the object creation code without changing the interface.
import java.util.function.Consumer;
public class LambdaKitten {
private String name;
private String color;
private LambdaKitten() {}
public static void play(Consumer<LambdaKitten> consumer) {
LambdaKitten kitten = new LambdaKitten();
consumer.accept(kitten);
System.out.println(kitten.name + " has " + kitten.color + " fur");
}
public LambdaKitten name(String name) {
this.name = name;
return this;
}
public LambdaKitten color(String color) {
this.color = color;
return this;
}
public static void main(String...args) {
LambdaKitten.play(kitten -> kitten.name("Mina").color("red"));
}
}
Virtual Proxy Pattern
We sometimes use a proxy class to control access to an object that is expensive to instantiate. In this pattern, both the class providing the functionality and the proxy representing these functionality implement the sa,e interface. The proxy class has an instance of the proxied class as a private field. When client need to access the functionality promised in the interface, they don't have access to the proxied class, they only have access to the proxy class. The proxy class can implement techniques such as lazy loading, a functionality which is not necessary a concern of the proxied class. For example.
public interface Singer {
public void sing();
}
=============================
public class XyzSinger implements Singer {
private String songName;
public XyzSinger(String songName) {
this.songName = songName;
practice(songName);
}
public void sing() {
System.out.println(this.getClass().getName() + " sings " + songName);
}
private void practice(String songName) {
System.out.println("practicing " + songName);
}
}
============================
import java.util.Random;
public class Proxy implements Singer {
private Singer singer;
private String songName;
public Proxy(String songName) {
this.songName = songName;
}
public void sing() {
if(singer == null) {
singer = new XyzSinger(songName);
}
singer.sing();
}
public static void main(String...args) {
Singer singer = new XyzSinger("always song");
Proxy agent = new Proxy("Lazy song");
if(new Random().nextBoolean()) {
singer.sing();
agent.sing();
}
}
}
output:
practicing always song
=====or===============
practicing always song
XyzSinger sings always song
practicing Lazy song
XyzSinger sings Lazy song
In the above example, Proxy doesn't really concern about which singer is singing the song, and the singer probably doesn't care about how the proxy arranges the shows for the same song she/he has practiced.
The advantage of proxy is lazy loading. If we have the XyzSinger to sing the song, he/she will always practice the song even without audience. The lazy proxy, after taking the songName, don't even bother to have his/her represented singer to practice unless the show is real.
This kind of lazy loading requirement can be alternatively achieved with lambda functions.
public interface Singer {
public void sing();
}
=============================
public class XyzSinger implements Singer {
private String songName;
public XyzSinger(String songName) {
this.songName = songName;
practice(songName);
}
public void sing() {
System.out.println(this.getClass().getName() + " sings " + songName);
}
private void practice(String songName) {
System.out.println("practicing " + songName);
}
}
import java.util.Random;
import java.util.function.Supplier;
public class LamdaProxy<T> {
private T instance;
private Supplier<T> supplier;
public LamdaProxy(Supplier<T> supplier) {
this.supplier = supplier;
}
public T get() {
if(instance == null) {
instance = supplier.get();
supplier = null;
}
return instance;
}
public static void main(String...args) {
Singer diligentSinger = new XyzSinger("always song");
LamdaProxy<Singer> agent = new LamdaProxy<>(() -> new XyzSinger("lazySong"));
if(new Random().nextBoolean()) {
diligentSinger.sing();
Singer singer = (Singer)agent.get();
singer.sing();
}
}
}
As we can see, LambdaProxy in this case is fat and clumsy, however, it demonstrated a new way of achieving lazy load. The constructor takes a Supplier that expresses how to supply a singer, however, the singer object won't be created until the get() method is called. LamdaProxy is more flexible than the Proxy, because now the creation of singer became a variable. While Proxy can only represent a XyzSinger, the LambdaProxy can represent any singer. For example, you can introduce your spouse or 5 year old kid to the LamdaProxy, the LamdaProxy will arrange a show to have them sing whatever song you like with whatever special effect you want.
import java.util.function.Function;
public interface Singer {
public void sing(Function<String, String>...songDecorators);
}
=============================
import java.util.Random;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.*;
public class LamdaProxy<T> implements Singer{
public static Function<String, String> lightEffect = i -> i + ", color enhenced";
public static Function<String, String> soundEffect = i -> i + ", disco enhenced";
private T instance;
private Supplier<T> supplier;
public LamdaProxy(Supplier<T> supplier) {
this.supplier = supplier;
}
public T get() {
if(instance == null) {
instance = supplier.get();
supplier = null;
}
return instance;
}
public static void main(String...args) {
String songName = "whatever song";
Singer a = decorators -> {
String customizedSong = Stream.of(decorators).reduce(Function.identity(), Function::andThen).apply(songName);
System.out.println("practicing " + customizedSong);
System.out.println("whatever name sings " + customizedSong);
};
LamdaProxy<Singer> agent = new LamdaProxy<>(() -> a);
if(new Random().nextBoolean()) {
agent.sing(soundEffect, lightEffect, i -> i + ", happy birthday");
}
}
public void sing(Function<String, String>...songDecorators) {
Singer singer = (Singer)supplier.get();
singer.sing(songDecorators);
}
}
no output or output the following:
practicing whatever song, disco enhenced, color enhenced, happy birthday
whatever name sings whatever song, disco enhenced, color enhenced, happy birthday
Isn't it cool or is it a little bit too much?
Like it or not, the lambda expression is a great design tool.
No comments:
Post a Comment