2
\$\begingroup\$

I responded to this question about designing around interdependant methods with the strategy pattern.

Afterwards I started thinking about the functional additions to java 8 and wondered if I was being an old foggy stuck in the past and if maybe there was a better way with all these fancy new tricks.

So I started hammering on the idea with single method interfaces. Not as elegant as lambdas I know but it's what I found myself doing.

Here's the gist of the original question:

... only depend on the three fundamental methods int Length(), Set DescentSet() and int[] Normalform(). Normally this means that I would use the method template pattern, i.e. I would make these three methods abstract and let the concrete implementations deal with it. But: Those methods are interdependent. I really only need any one of them to define the other two. ...

Problem is performance concerns demand we be able to pick which one is NOT dependant on the others.

The strategy pattern works well here but on the off chance even more flexibility is desired I tried this:

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.junit.Test;

import static org.junit.Assert.*;

//design-pattern-for-interdependent-abstract-methods
//https://softwareengineering.stackexchange.com/q/333608/131624
public class EntryPoint {
    public static void main(String[] args) {
        long startTime = System.nanoTime();

        System.out.println(".-=== Interdependent test start ===-.");

        int i = 1;

        List<Interdependent> ids = Arrays.asList(
                //         int i, IntInLen r,         IntInSet s,         IntInArray a
                new IntPackage(i, new Leng(), new DSetFromLeng(), new NormFromLeng() ),
                new IntPackage(i, new LengFromDSet(), new DSet(), new NormFromDSet() ),
                new IntPackage(i, new LengFromNorm(), new DSetFromNorm(), new Norm() )
        );

        for (Interdependent id : ids) {
            System.out.println(id.length() + " : " +
                               id.descentSet().iterator().next() + " : " +
                               id.normalform()[0]);

            assertEquals(i, id.length());
            assertEquals(i, (int)id.descentSet().iterator().next());
            assertEquals(i, id.normalform()[0]);
        }
        System.out.println("'-=== Interdependent test end   ===-'");

        long estimatedTime = System.nanoTime() - startTime;
        double time = (double)estimatedTime / 1000000000.0;
        System.out.println("time = " + time);
    }
}

interface Interdependent {
    int length();
    Set<Integer> descentSet();
    int[] normalform();
}

interface IntInLen {
    int length(Interdependent id, int i);
}

interface IntInSet {
    Set<Integer> descentSet(Interdependent id, int i);
}

interface IntInArray {
    int[] normalform(Interdependent id, int i);
}

class IntPackage implements Interdependent {
    private int i;

    IntInLen r;
    IntInSet s;
    IntInArray a;

    public IntPackage(int i, IntInLen r, IntInSet s, IntInArray a) {
        this.i = i;
        this.r = r;
        this.s = s;
        this.a = a;
    }

    @Override
    public int length() {
        return r.length(this, i);
    }

    @Override
    public Set<Integer> descentSet() {
        return s.descentSet(this, i);
    }

    @Override
    public int[] normalform() {
        return a.normalform(this, i);
    }
}

// .-=== IntInLen ===-. //
class Leng implements IntInLen {

    @Override
    public int length(Interdependent ip, int i) {
        return i;
    }
}

class LengFromDSet implements IntInLen {

    @Override
    public int length(Interdependent id, int i) {
        return id.descentSet().iterator().next();
    }
}

class LengFromNorm implements IntInLen {

    @Override
    public int length(Interdependent id, int i) {
        return id.normalform()[0];
    }
}
// '-=== IntInLen ===-' //

// .-=== IntInSet ===-. //
class DSetFromLeng implements IntInSet {

    @Override
    public Set<Integer> descentSet(Interdependent id, int i) {
        Set<Integer> s = new HashSet<>();
        s.add(id.length());
        return s;
    }
}

class DSet implements IntInSet {

    @Override
    public Set<Integer> descentSet(Interdependent id, int i) {
        Set<Integer> s = new HashSet<>();
        s.add(i);
        return s;
    }
}

class DSetFromNorm implements IntInSet {

    @Override
    public Set<Integer> descentSet(Interdependent id, int i) {
        Set<Integer> s = new HashSet<>();
        s.add(id.normalform()[0]);
        return s;
    }
}
// '-=== IntInSet ===-' //

// .-=== IntInArray ===-. //
class NormFromLeng implements IntInArray {

    @Override
    public int[] normalform(Interdependent id, int i) {
        int[] result = new int[1];
        result[0] = id.length();
        return result;
    }
}

class NormFromDSet implements IntInArray {

    @Override
    public int[] normalform(Interdependent id, int i) {
        int[] result = new int[1];
        result[0] = id.descentSet().iterator().next();
        return result;
    }
}

class Norm implements IntInArray {

    @Override
    public int[] normalform(Interdependent id, int i) {
        int[] result = new int[1];
        result[0] = i;
        return result;
    }
}
// '-=== IntInArray ===-' //

Outputs:

.-=== Interdependent test start ===-.
1 : 1 : 1
1 : 1 : 1
1 : 1 : 1
'-=== Interdependent test end   ===-'
time = 0.00656082

This kinda thing have a name?

This puts a lot of flexibility in our hands. We can extend any number of implementations of the methods completely independently.

However, this also gives us the ability to shoot ourselves in the foot:

new IntPackage(i, new LengFromNorm(), new DSetFromLeng(), new NormFromLeng() )

That will reliably produce a stack overflow.

I know this is more flexibility that was actually asked for but is there some twist on this (functional or otherwise) that I'm simply not seeing that could do the strategy pattern one better?

This was fun but still feels overdesigned for these requirements.

\$\endgroup\$
2
  • \$\begingroup\$ Fun that I had the same thought when I read the question on Programmers. I have the feeling that OOP is not the right tool to do that job... (I've also come up with a same "overdesigned" solution as yours) \$\endgroup\$
    – Spotted
    Commented Oct 18, 2016 at 13:09
  • \$\begingroup\$ I've added an answer to the original question on Programmers: programmers.stackexchange.com/a/333954/189221. What are your thoughts about that ? \$\endgroup\$
    – Spotted
    Commented Oct 18, 2016 at 13:41

2 Answers 2

1
\$\begingroup\$

First off, great example, very good code.

This was fun but still feels overdesigned for these requirements

I don't think so. I've designed a framework that modelled mathematical objects (Algebras etc.) and their properties, and there's so many possibilities that such flexibility wouldn't be wasted.

This kinda thing have a name?

Yes, this Pattern has a name: Delegate. It is close to Composite, or even a Mixin?

With the Delegate pattern, you construct an object with fields that provide functionality, and each method call on the object is forwarded to a corresponding field's method.

This puts a lot of flexibility in our hands [...] However, this also gives us the ability to shoot ourselves in the foot:

With great power comes great responsibility. However responsibilities are well split between your objects:

  • The IntInLen, IntInSet, and IntInArray interfaces provide flexibility
  • The Interdependent interface provides usability
  • The Leng, LengFromDSet, LengFromNorm provide implementation library

What is missing is coherence. The lack of coherence is what allows you to shoot yourself in the foot by bringing together implementations fom the library, that do not play nice with each other. This is where you've gone too far, and here's how I would 'fix' it:

The IntPackage provides too much (uncontrolled) power through its constructor that allows anything. Instead, I would make IntPackage abstract as long is it does not have safe-guards, and only expose constructor of safe implementations of it (like a IntPackageFromNorm), which come with coherence built-in and cannot be misused:

public abstract class IntPackage implements Interdependent{
    private int i;

    IntInLen r;
    IntInSet s;
    IntInArray  a;

    // This is not always coherent, so is protected.
    protected IntPackage(int i, IntInLen r, IntInSet s, IntInArray a) {
        this.i = i;
        this.r = r;
        this.s = s;
        this.a = a;
    }

    @Override
    public int length() {
        return r.length(this, i);
    }

    @Override
    public Set<Integer> descentSet() {
        return s.descentSet(this, i);
    }

    @Override
    public int[] normalform() {
        return a.normalform(this, i);
    }
}

 public class IntPackageFromNorm extends IntPackage {
    // This is always coherent, so is exposed as public
    public IntPackage(int i) {
        super(i, new LengFromNorm(), new DSetFromNorm(), new Norm()); 
    }
}

Finally, You're referencing the Strategy Pattern, but as far as mathematical operations go, you don't need to change implementations on the fly because math constructs should not gain functionality out of the blue (but I may be wrong).
In which case, the r, s, a attributes of the IntPackage could be made final, providing the desirable property that the safe implementations (IntPackageFromNorm and the like) would keep their coherence (because their operation implementations could not be changed after instantiation).

\$\endgroup\$
0
\$\begingroup\$

For comparison, this is what it looks like to do this with the Strategy Pattern:

public class EntryPoint {
    public static void main(String[] args) {
        long startTime = System.nanoTime();

        System.out.println(".-=== Interdependent test start ===-.");

        int i = 1;

        List<Interdependent> ids = Arrays.asList(
                new IntPackageFromLeng(i),
                new IntPackageFromDSet(i),
                new IntPackageFromNorm(i)
        );

        for (Interdependent id : ids) {
            System.out.println(id.length() + " : " +
                               id.descentSet().iterator().next() + " : " +
                               id.normalform()[0]);

            assertEquals(i, id.length());
            assertEquals(i, (int)id.descentSet().iterator().next());
            assertEquals(i, id.normalform()[0]);
        }
        System.out.println("'-=== Interdependent test end   ===-'");

        long estimatedTime = System.nanoTime() - startTime;
        double time = (double)estimatedTime / 1000000000.0;
        System.out.println("time = " + time);
    }
}

interface Interdependent {
    int length();
    Set<Integer> descentSet();
    int[] normalform();
}

class IntPackageFromLeng implements Interdependent{
    private int i;

    public IntPackageFromLeng(int i) {
        this.i = i;
    }

    @Override
    public int length() {
        return i;
    }

    @Override
    public Set<Integer> descentSet() {
        Set<Integer> s = new HashSet<>();
        s.add(length());
        return s;
    }

    @Override
    public int[] normalform() {
        int[] result = new int[1];
        result[0] = length();
        return result;
    }
}

class IntPackageFromDSet implements Interdependent{
    private int i;

    public IntPackageFromDSet(int i) {
        this.i = i;
    }

    @Override
    public int length() {
        return descentSet().iterator().next();
    }

    @Override
    public Set<Integer> descentSet() {
        Set<Integer> s = new HashSet<>();
        s.add(i);
        return s;
    }

    @Override
    public int[] normalform() {
        int[] result = new int[1];
        result[0] = descentSet().iterator().next();
        return result;
    }
}

class IntPackageFromNorm implements Interdependent{
    private int i;

    public IntPackageFromNorm (int i) {
        this.i = i;
    }

    @Override
    public int length() {
        return normalform()[0];
    }

    @Override
    public Set<Integer> descentSet() {
        Set<Integer> s = new HashSet<>();
        s.add(normalform()[0]);
        return s;
    }

    @Override
    public int[] normalform() {
        int[] result = new int[1];
        result[0] = i;
        return result;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.