Let's say my application has some services implemented as ClassA
and ClassB
. Both have some similarities but also differences.
start()
method with the same method signature but a different implementation.process()
method with a different signature and a different implementation.log()
method, i.e. the code is exactly the same.public class ClassA {
public String start(String s1, String s2) {
startImplementation();
return someString;
}
public String process(String s) {
processingImplementation();
return processedString;
}
private String log(String s) {
logImplementation();
return sharedString;
}
}
public class ClassB {
public String start(String s1, String s2) {
otherStartImplementation();
return someString;
}
public String process(Long l) {
otherProcessingImplementation();
return processedString;
}
private String log(String s) {
logImplementation();
return sharedString;
}
}
I'm having trouble thinking of a "design pattern" how I could organize this in a more generic way. As of 3. I could easily move this method to a superclass which ClassA
and ClassB
extend. But how would/could I design the application so that 1. and 2. are also taken into account?
Item 1. sounds a little bit like an interface to me but I don't have any idea how this could be combined with the superclass for item 3. And what about item 2?
Yes, we can define multiple methods in a class with the same name but with different types of parameters. Which method is to get invoked will depend upon the parameters passed.
You do not have to use one pattern for every piece of functionality. In fact, many pieces of functionality don't use patterns at all, and some pieces will use multiple patterns.
Factory Design Pattern One of the most popular design patterns used by software developers is a factory method. It is a creational pattern that helps create an object without the user getting exposed to creational logic.
I would design this so that class A
and class B
extend a generic abstract class, with a type parameter for the process
method's parameter type.
public abstract class BaseClass<T> {
public abstract String start(String s1, String s2);
public abstract String process(T value);
protected final String log(String s) {
// shared log implementation
}
}
public class A extends BaseClass<String> {
@Override
public String start(String s1, String s2) {
// A.start implementation
}
@Override
public String process(String s) {
// A.process implementation
}
}
public class B extends BaseClass<Long> {
@Override
public String start(String s1, String s2) {
// B.start implementation
}
@Override
public String process(Long l) {
// B.process implementation
}
}
In Java 9+, you could instead use a generic public interface Base<T>
instead of an abstract class, by giving log
a default implementation. However, that doesn't allow you to make log
only accessible to the implementing classes, and it doesn't prevent subclasses from overriding log
.
How about using composition over inheritance? The implementations of start
and process
could be provided by functions like in the example below:
import java.util.function.BiFunction;
import java.util.function.Function;
class X<T> {
public String start(BiFunction<String, String, String> f, String s1, String s2) {
return f.apply(s1, s2);
}
public String process(Function<T, String> f, T t) {
return f.apply(t);
}
// example
public static void main(String[] args) {
X<String> xString = new X();
xString.start((s1, s2) -> s1 + s2, "a", "b");
X<Long> xLong = new X();
xLong.process((t) -> { Long tt = t * 2;return tt.toString(); }, 4L);
}
}
Same as previous example, but with implementations provided in the constructor and using functional interfaces instead of lambdas.
import java.util.function.BiFunction;
import java.util.function.Function;
class StartFunctionExample implements BiFunction<String, String, String> {
@Override
public String apply(String s1, String s2) {
return s1 + s2;
}
}
class ProcessFunctionExample implements Function<Long, String> {
@Override
public String apply(Long t) {
Long tt = (t * 2);
return tt.toString();
}
}
class Z<T> {
private final BiFunction<String, String, String> startFunction;
private final Function<T, String> processFunction;
public Z(
BiFunction<String, String, String> startFunction,
Function<T, String> processFunction
) {
this.startFunction = startFunction;
this.processFunction = processFunction;
}
public String start(String s1, String s2) {
return startFunction.apply(s1, s2);
}
public String process(T t) {
return processFunction.apply(t);
}
// example
public static void main(String[] args) {
Z<Long> xLong = new Z(new StartFunctionExample(), new ProcessFunctionExample());
xLong.start("a", "b"); // ab
xLong.process(7L); // 14
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With