Robert Strauch
Robert Strauch

Reputation: 12876

Java design pattern for two classes sharing identical and similar but different methods

Let's say my application has some services implemented as ClassA and ClassB. Both have some similarities but also differences.

  1. Both classes have a start() method with the same method signature but a different implementation.
  2. Both classes have a process() method with a different signature and a different implementation.
  3. Both classes have an identical log() method, i.e. the code is exactly the same.

Class A

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;
    }
}

Class B

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?

Upvotes: 6

Views: 5263

Answers (3)

David Soroko
David Soroko

Reputation: 9086

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
    }
}

Upvotes: 2

Florian Salihovic
Florian Salihovic

Reputation: 3951

Let a different class implement the logging, the processing can be done by classes implementing the service interface:

interface Processable {
  String start(String s1, String s2);
  process(String s);
}

class LogDecorator {

  private Processable p;

  public LogDecorator(Processable p) {
    this.p = p;
  }

  public String start(String s1, String s2) {
    p.start(s1, s2);
  }

  public String process(String s) {
    p.process();
  }

  protected final String log(String s) {
    // logging
  }

}

Upvotes: 0

kaya3
kaya3

Reputation: 51037

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.

Upvotes: 6

Related Questions