4

I have two interfaces Query and Filter(Query is a class in example for simplification, I have 1 query for now) , I now want write function Query.applyFilter() depending on what Filter is real is i.e different functions for NameFilter and DateFilter and every other Filter.

My solution is as follows:

interface Filter {
    public abstract void modifyQuery(Query query);
};

class NameFilter implements Filter{
    public void modifyQuery(Query query){
        query.applyFilter(this);
    }
};

class DateFilter implements Filter{
    public void modifyQuery(Query query){
        query.applyFilter(this);
    }
};

class Query {
    public void applyFilter(Filter filter){
        filter.modifyQuery(this);
    }

    void applyFilter(NameFilter* filter) {
        //"applying NameFilter";
    }

    void applyFilter(DateFilter* filter) {
       //apply dateFilter
   }
}

OK then, here I need to rewrite modifyQuery() implementation for every Filter class.

Then, I have solution how to avoid it in C++: We use templates and cast in modifyQuery():

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <vector>

using namespace std;

class Query;

class IFilter
{
public:
  virtual void modifyQuery(Query* query) = 0;
};

template <typename T>
class Filter : public IFilter
{
public:
  virtual void modifyQuery(Query* query);
};

class DateFilter;
class NameFilter;

class Query
{
public:

  void applyFilter(IFilter* filter)
  {
    cout << "applying Filter" << endl;
    filter->modifyQuery(this);
  }

  void applyFilter(NameFilter* filter)
  {
    cout << "applying NameFilter" << endl;
  }

  void applyFilter(DateFilter* filter)
  {
    cout << "applying DateFilter" << endl;
  }
};

template <typename T>
void Filter<T>::modifyQuery(Query* query)
{
  query->applyFilter(dynamic_cast<T*> (this));
}

class DateFilter : public Filter<DateFilter>
{
};

class NameFilter : public Filter<NameFilter>
{
};

int main()
{
  Query* query = new Query();
  IFilter* nameFilter = new NameFilter();
  IFilter* dateFilter = new DateFilter();

  std::vector<IFilter*> filterList;
  filterList.push_back(nameFilter);
  filterList.push_back(dateFilter);

  for (int i = 0; i < 2; ++i)
  {
    query->applyFilter(filterList[i]);
  }
  return 0;
}

DEMO

But I can't use this solution in Java because generics do not operate the same as templates. What can use in Java to avoid copy-pasting?

RiaD
  • 46,822
  • 11
  • 79
  • 123
  • You can use generics as part of an interface though: you can define `Filter` and, for instance, `NameFilter` as extending `Filter`. No idea if this will help, but also, have you considered the builder pattern? This way you could create a builder for your queries, add filters to the builder and ultimately `.build()` your `Query`. – fge Dec 25 '12 at 18:50
  • It looks like you need `interface Filter`. – Luiggi Mendoza Dec 25 '12 at 18:52
  • "I need to rewrite modifyQuery() implementation for every Filter class" - Well, isn't that the whole purpose of your class hierarchy? - And why do you need those overloaded methods in `Query` when all your filters are instances of `Filter`? – JimmyB Dec 25 '12 at 20:05
  • Besides: Did you notice you were producing endless recursions with `filter.modifyQuery(this);` and `query.applyFilter(this);`? – JimmyB Dec 25 '12 at 20:06
  • @Hanno: I will produce endless recursions only if there no implementation of `void applyFilter(ThisConcreteFilter)`. I need different functions because I want manage them __differently__ – RiaD Dec 25 '12 at 20:25

2 Answers2

2

When finding the right method, Java does not take into account the run-time type of the method's arguments. Java simply reasons about types of variables instead of values.

Solution:

Java 6 annotations can be used to annotate methods and implement multimethods and value dispatch. All this can be done at runtime without the need for any special compilation or preprocessing and the usage can still be reasonably user-friendly.

We need to introduce two "simple" annotations to annotate:

Methods: What multimethod this method implements?

Parameters: What value should we dispatch on?

We can then process the annotations and build a list of methods that implement a particular multimethod. This list needs to be sorted so that the most specific methods come first. "Most specific" means that for each method parameter (from left to right), the parameter type/value is more specialized (e.g. it is a subclass or it is matched agains the specified value). Calling a multimethod means invoking the most specific applicable method. "Applicable" means that the method prototype matches the actual runtime arguments and "the most specific" means that we can simply search through the sorted list and find the first one which is applicable.

Annotation processing can be wrapped up in a class which can then be used in a user defined method that will simply invoke the multimethod dispatch code with the actual runtime arguments.

Implementation

The interface Multi implements a runtime method annotation used to mark multimethods:

package jmultimethod;

import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Multi {

    public String value();
}

The interface V implements a runtime parameter annotation used to specify dispatch values:

package jmultimethod;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface V {

    public String value();
}

The Multimethod code follows:

package jmultimethod;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

public class Multimethod {

    protected String name;
    protected final ArrayList<Method> methods = new ArrayList<Method>();
    protected final MethodComparator methodComparator = new MethodComparator();

    public Multimethod(String name, Class... classes) {
        this.name = name;
        for(Class c: classes) {
            add(c);
        }
    }

    public void add(Class c) {
        for(Method m: c.getMethods()) {
            for(Annotation ma: m.getAnnotations()) {
                if(ma instanceof Multi) {
                    Multi g = (Multi) ma;
                    if(this.name.equals(g.value())) {
                        methods.add(m);
                    }
                }
            }
        }
        sort();
    }

    protected void sort() {
        Method[] a = new Method[methods.size()];
        methods.toArray(a);
        Arrays.sort(a, methodComparator);
        methods.clear();
        for(Method m: a) {
            methods.add(m);
        }
    }

    protected class MethodComparator implements Comparator<Method> {
        @Override
        public int compare(Method l, Method r) {
            // most specific methods first 
            Class[] lc = l.getParameterTypes();
            Class[] rc = r.getParameterTypes();
            for(int i = 0; i < lc.length; i++) {
                String lv = value(l, i);
                String rv = value(r, i);
                if(lv == null) {
                    if(rv != null) {
                        return 1;
                    }
                }
                if(lc[i].isAssignableFrom(rc[i])) {
                    return 1;
                }
            }
            return -1;
        }
    }

    protected String value(Method method, int arg) {
        Annotation[] a = method.getParameterAnnotations()[arg];
        for(Annotation p: a) {
            if(p instanceof V) {
                V v = (V) p;
                return v.value();
            }
        }
        return null;
    }

    protected boolean isApplicable(Method method, Object... args) {
        Class[] c = method.getParameterTypes();
        for(int i = 0; i < c.length; i++) {
            // must be instanceof and equal to annotated value if present 
            if(c[i].isInstance(args[i])) {
                String v = value(method, i);
                if(v != null && !v.equals(args[i])) {
                    return false;
                }
            } else {
                if(args[i] != null || !Object.class.equals(c[i])) {
                    return false;
                }
            }
        }
        return true;
    }

    public Object invoke(Object self, Object... args) {
        Method m = null; // first applicable method (most specific)
        for(Method method: methods) {
            if(isApplicable(method, args)) {
                m = method;
                break;
            }
        }
        if(m == null) {
            throw new RuntimeException("No applicable method '" + name + "'.");
        }
        try {
            return m.invoke(self, args);
        } catch (Exception e) {
            throw new RuntimeException("Method invocation failed '" + name + "'.");
        }
    }
}

To use multimethods, user code must:

  1. Annotate methods with the multimethod name, e.g.

    @Multi("myMultimethod")
    
  2. The name of the annotated methods can be anything Java is happy with. It is most likely going to be different from the multimethod name because some methods can have prototype similar enough to cause name clashes for the Java compiler (and maybe because the compiler could have problems with the null value). Also, the method should be visible (e.g. public) to the Multimethod class.

  3. Process the annotations by creating the Multimethod object, e.g.

     protected Multimethod mm = new Multimethod("myMultimethod", getClass());
    
  4. Define multimethod "entry point" method with parameters as general as necessary. This method dispatches using the Multimethod object created above, e.g.

     public void myMultimethod(Object X, Object Y) {
       mm.invoke(this, X, Y);
    }
    
  5. And then, the multimethod can be called as any normal Java method, e.g.

myMultimethod(1, null);

Limitations:

Value dispatch works only with values supported by Java annotations, e.g. values of type String.

The following code is based on the Multiple Dispatch example

package jmultimethod;

public class AsteroidTest {

    class Asteroid {}

    class Spaceship {}

    @Multi("collide")
    public void collideOO(Object X, Object Y) {
       log("?? Bang, what happened? ", X, Y);
    }

    @Multi("collide")
    public void collideAA(Asteroid X, Asteroid Y) {
        log("AA Look at the beautiful fireworks! ", X, Y);
    }

    @Multi("collide")
    public void collideAS(Asteroid X, Spaceship Y) {
        log("AS Is it fatal? ", X, Y);
    }

    @Multi("collide")
    public void collideSA(Spaceship X, Asteroid Y) {
        log("SA Is it fatal? ", X, Y);
    }

    @Multi("collide")
    public void collideSS(Spaceship X, Spaceship Y) {
        log("SS Who's fault was it? ", X, Y);
    }

    @Multi("collide")
    public void collide1S(String X, Spaceship Y) {
        log("1S any string? ", X, Y);
    }

    @Multi("collide")
    public void collide2S(@V("hi") String X, Spaceship Y) {
        log("2S 'hi' value? ", X, Y);
    }

    protected Multimethod mm = new Multimethod("collide", getClass());

    public void collide(Object X, Object Y) {
        mm.invoke(this, X, Y);
    }

    public void run() {
        Object A = new Asteroid();
        Object S = new Spaceship();
        collide(A, A);
        collide(A, S);
        collide(S, A);
        collide(S, S);
        collide(A, 1);
        collide(2, A);
        collide(S, 3);
        collide(4, S);
        collide(5, null);
        collide(null, null);
        collide("hi", S);
        collide("hello", S);
    }

    public void log(Object... args) {
        for(Object o: args) {
            if(o instanceof String) {
                System.out.print(" " + (String) o);
            } else {
                System.out.print(" " + o);
            }
        }
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        AsteroidTest t = new AsteroidTest();
        t.run();
    }
}

The program output (partially edited to fit on the screen) is:

AA Look at the beautiful fireworks!  Asteroid@1f24bbbf Asteroid@1f24bbbf
AS Is it fatal?  Asteroid@1f24bbbf Spaceship@24a20892
SA Is it fatal?  Spaceship@24a20892 Asteroid@1f24bbbf
SS Who's fault was it?  Spaceship@24a20892 Spaceship@24a20892
?? Bang, what happened?  Asteroid@1f24bbbf 1
?? Bang, what happened?  2 Asteroid@1f24bbbf
?? Bang, what happened?  Spaceship@24a20892 3
?? Bang, what happened?  4 Spaceship@24a20892
?? Bang, what happened?  5 null
?? Bang, what happened?  null null
2S 'hi' value?  hi Spaceship@24a20892
1S any string?  hello Spaceship@24a20892
Harish Raj
  • 1,565
  • 3
  • 17
  • 27
1

Proposal:

interface Filter {
    public abstract void modifyQuery(Query query);
};

class NameFilter implements Filter{
    public void modifyQuery(Query query){
        // Modify query based on "Name"...
    }
};

class DateFilter implements Filter{
    public void modifyQuery(Query query){
        // Modify query based on "Date"...
    }
};

class Query {
    public void applyFilter(Filter filter){
        filter.modifyQuery(this);
    }

    // No need for other applyFilter() methods - all filters are instances of Filter.
}
JimmyB
  • 12,101
  • 2
  • 28
  • 44
  • It's posiible that other implementation of Query will be added. i.e I will change my SQL DB to NoSQL one, I should be able to change it without modifying filter. I.e Filter should not not what the query is (or better not know that Query exitst, but I can't achive this) – RiaD Dec 25 '12 at 20:22
  • Then you need to define an interface first: What data will be given to the filter and what will the filter return - in a way that's independent of the `Query` type, so you need to identify common properties of different `Query` implementations to use for this interface. - And where do you want to put the filter's functionality? Naturally, it will go into each filter class and the `Query` will never have to treat different filter types differently. That's why you define the common interface `Filter` in the first place. – JimmyB Dec 25 '12 at 21:09
  • oh, ok suppose query is interface and all implementations are in QueryImpl, it isn't natural when filters knows what kind of database I use – RiaD Dec 25 '12 at 23:26
  • Your `Query` interface should, however, represent an abstraction which will be valid for all implementations; for instance, if all `Query` implementations will provide key-value kinds of data the interface can expose access to those key-value elements, irrespective of where/how they were retrieved, and a `Filter` could operate on key-value elements too. - Identifying and grouping common properties and/or behaviors, and abstracting where necessary, is what OO modelling is all about after all. – JimmyB Dec 25 '12 at 23:38
  • Whether `Filters` operate on `Queries`, or if `Queries` actively 'use' `Filters` is up to you to decide; each way may make sense and neither is 'bad' from the start. – JimmyB Dec 25 '12 at 23:42