Jak uniknąć używania instanceof

0

Cześć. Próbuje zrobić aplikację w stylu bardzo uproszczonego MatLaba tzn. Będzie jakaś konsola gdzie będzie można definiować różne zmienne o różnych typach np. macierz, ułamek, liczba zespolona, a następnie odwołać się do niej w innej zmiennej. Coś w tym stylu:

    A = matrix(4, 5) //(typ zmiennej A : macierz)
    B = complex(5, 2) * A.determinant //(typ zmiennej B : liczba zespolona)
    C = A * B //(typ zmiennej C : macierz)

Jako że dla każdego obiektu - ułamka, macierzy, liczby zespolonej, można wykonać podstawowe działania arytmetyczne (dzielenie przez macierz będzie traktowanie jako mnożenie przez jej odwrotność podniesioną do odpowiedniej potęgi), zacząłem od stworzenia interfejsu MathObject:

public interface MathObject {
    MathObject add(MathObject object);
    MathObject subtract(MathObject object);
    MathObject multiply(MathObject object);
    MathObject divide(MathObject object);
}

oraz interfejsów dla poszczególnych typów zmiennych (ułamek, macierz, liczba zespolona):

public interface IFraction extends MathObject {
    //metody dla IFraction
}
public interface IComplex extends MathObject {
    //metody dla IFraction
}
public interface IMatrix extends MathObject {
    //metody dla IFraction
}

Następnie interfejs funkcyjny Function zwracający obiekt typu MathObject (tutaj będzie zapisana funkcja np. = matrix(4, 5), = A * B)

@FunctionalInterface
public interface Function<T extends MathObject> {
    T getValue();
}

I w końcu klasa zmiennej która przechowuje określoną funkcję:

public interface IVariable {
    Function getFunction();
    IVariable add(IVariable variable);
    IVariable subtract(IVariable variable);
    IVariable multiply(IVariable variable);
    IVariable divide(IVariable variable);
}

public class Variable implements IVariable {

    private Function function;

    public Variable(Function function) {
        this.function = function;
    }

    @Override
    public IVariable add(IVariable variable) {
        return new Variable(() -> function.getValue().add(variable.getFunction().getValue()));
    }
    @Override
    public IVariable subtract(IVariable variable) {
        return new Variable(() -> function.getValue().subtract(variable.getFunction().getValue()));
    }
    @Override
    public IVariable multiply(IVariable variable) {
        return new Variable(() -> function.getValue().multiply(variable.getFunction().getValue()));
    }
    @Override
    public IVariable divide(IVariable variable) {
        return new Variable(() -> function.getValue().divide(variable.getFunction().getValue()));
    }
    @Override
    public Function getFunction() {
        return function;
    }
}

Problem pojawił się podczas implementacji typów, ponieważ każdy z nich w zależności klasy implementującej oraz od typu argumentu object musi zwrócić nowy obiekt określonego typu. Dla przykładu klasa Complex:

public class Complex implements IComplex {
    @Override
    public MathObject add(MathObject object) {
        if(object instanceof IComplex) {
            return [NOWY OBIEKT TYPU IComplex]
        } else if(object instanceof IFraction) {
            return [NOWY OBIEKT TYPU IFraction]
        } else if(object instanceof IMatrix) {
            return [NOWY OBIEKT TYPU IMatrix]
        }
    }
   //reszta dziedziczonych metod
}

Jak na razie jedynie powyższe rozwiązanie przyszło mi do głowy, ale z tego co przeczytałem używanie instanceof nie jest najlepszą opcją. Czy da się w jakiś sposób uzyskać powyższy efekt nie używając instanceof ? Będę wdzięczny za każdą wskazówkę :).

2

Po prostu brakuje ci abstrakcji pasującej do wszystkich tych przypadków.
IMO powinieneś zdefiniować interfejs, który będzie reprezentował wyrażenie algebraiczne.
A to czy coś jest zmienną, czy ma typ całkowity, rzeczywisty albo urojony, powinno być sprawą drugorzędną.

Zastanów się czy chcesz implementować Computer Algebra System, bo to nie jest takie proste. Może uprość sobie problem, że przetwarzasz wyrażenia urojone.

Przypuszczalnie potrzebujesz pattern, który się nazywa Double Dispatch.

2
MarekR22 napisał(a):

Przypuszczalnie potrzebujesz pattern, który się nazywa Double Dispatch.

Podwólne wywoływanie i wzorzec wizytator to mnóstwo kodu do napisania.

Z drugiej strony jeśli pisałbyś w Scali to instanceOf nie wygląda bardzo źle:

class Complex extends IComplex {
    override def add(object : MathObject): MathObject = object match {
      case c: IComplex => addComplex(c)
      case f: IFraction => addFraction(f)
      case m: IMatrix => addMatrix(m)
    }
   //reszta dziedziczonych metod
}

Jeśli jednak chcesz robić podwójne wywoływanie to będzie to wyglądać tak:

public class Complex implements IComplex {
    @Override
    public MathObject add(MathObject object) {
        return object.addComplex(this)
    }
   //reszta dziedziczonych metod
}

i teraz w metodzie addComplex będziesz znał typy obu argumentów operacji

5

Po co w ogóle wszystkie te rzeczy mają mieć wspólny interfejs? Może warto zrobić jeden krok wstecz i zastanowić się, czy faktycznie taka abstrakcja jest potrzebna.

Bo oprócz tego, że implementacyjnie wyjdzie trochę taki potworek, bo Java nie oferuje takich fajnych mechanizmów jak Scala czy Haskell (type classes byłyby jak znalazł), to jeszcze wydajność będzie bardzo słaba względem operacji na typach wbudowanych. No i typesafety też ucierpi, bo co jak ktoś zechce dodać skalar do macierzy 5x5? Wyjątek?

0
Krolik napisał(a):

No i typesafety też ucierpi, bo co jak ktoś zechce dodać skalar do macierzy 5x5? Wyjątek?

Dla macierzy 5x5 i wszystkich kwadratowych to nie byłby problem. Wzorując się na https://matrixcalc.org/pl , byłoby to dodawanie do macierzy, macierzy jednostkowej o tych samych wymiarach pomnożonej przez skalar. Problem pojawiłby się w przypadku macierzy nie kwadratowych.

Dziękuje wszystkim za odpowiedzi i zainteresowanie. Jestem świeży jeżeli chodzi o interfejsy i jak tylko w miarę załapałem interfejs funkcyjny to stworzenie takiego prostego kalkulatora wydawało mi się banałem, ale jak widzę nie będzie to takie proste.

1

A może zrób hash-mapę "classname.op.classname" na lambdę?
(ewentualnie podwójną hash-mapę)

1

Luźny pomysł, bo z rana nie znalazłem motywacji by budować jakiś proof of concept (który by wykazał, że to działa, albo że pomysł jest z d...olnej półki i nie działa :P). Może wzorzec wizytator ?

Każdemu z różnych typów obiektów dostarczałbyś konkretnego wizytatora (obsługującego konkretną operację):

matrix_by_matrix = matrix.compute( new MulByMatrix(matrix) );
matrix_by_scalar = matrix.compute( new MulByScalar(new Complex(5,3) ));
inverted_matrix = matrix.compute( new InvertMatrix() );
addition = scalar.compute(new Addition(scalar2) ); 

compute ~ visit

0

Po raz kolejny dziękuję za wszystkie odpowiedzi. Spróbowałem rozwinąć pomysł Yarel :

yarel napisał(a):

Luźny pomysł, bo z rana nie znalazłem motywacji by budować jakiś proof of concept (który by wykazał, że to działa, albo że pomysł jest z d...olnej półki i nie działa :P). Może wzorzec wizytator ?

Każdemu z różnych typów obiektów dostarczałbyś konkretnego wizytatora (obsługującego konkretną operację):

matrix_by_matrix = matrix.compute( new MulByMatrix(matrix) );
matrix_by_scalar = matrix.compute( new MulByScalar(new Complex(5,3) ));
inverted_matrix = matrix.compute( new InvertMatrix() );
addition = scalar.compute(new Addition(scalar2) ); 

compute ~ visit

I wyszło mi coś takiego (przepraszam za masę kodu):
MathObject ~ Visitable, MathOperation ~ Visitor

public interface MathObject {
    MathObject compute(MathOperation mathOperation);
}
public interface MathOperation {
    MathObject visit(Complex complex);
    MathObject visit(Matrix matrix);
}

klasa macierzy i liczby zespolonej:

public class Matrix implements MathObject {
    //...//
    public Matrix multiply(Complex complex) {
        Matrix result = [macierz * l. zespolona];
        return result;
    }
    public Matrix multiply(Matrix matrix) {
        Matrix result = [macierz * macierz] ;
        return result;
    }
    public Complex determinant() {
        Complex determinant = [obliczanie wyznacznika] ;
        return determinant;
    }
    //...//
    @Override
    public MathObject compute(MathOperation mathOperation) {
        return mathOperation.visit(this);
    }
}
public class Complex implements MathObject {
    //...//
    public Complex multiply(Complex complex) {
        Complex result = [l. zespolona * l.zespolona];
        return result;
    }
    public Matrix multiply(Matrix matrix) {
        return matrix.multiply(this);
    }
    //...//
    @Override
    public MathObject compute(MathOperation mathOperation) {
        return mathOperation.visit(this);
    }
}

oraz przykładowe klasy (operacje) mnożenia i wyznacznika :

public class Multiplication implements MathOperation {
    private MathObject object;
    public Multiplication(MathObject object) {
        this.object = object;
    }

    @Override
    public MathObject visit(Complex complex) {
        return object.compute(new MathOperation() {
            public MathObject visit(Fraction fraction) {
                return complex.multiply(fraction);
            }
            public MathObject visit(Complex complex_) {
                return complex.multiply(complex_);
            }
            public MathObject visit(Matrix matrix) {
                return complex.multiply(matrix);
            }
        });
    }
    @Override
    public MathObject visit(Matrix matrix) {
        return object.compute(new MathOperation() {
            public MathObject visit(Fraction fraction) {
                return matrix.multiply(fraction);
            }
            public MathObject visit(Complex complex) {
                return matrix.multiply(complex);
            }
            public MathObject visit(Matrix _matrix) {
                return matrix.multiply(_matrix);
            }
        });
    }
}
public class Determinant implements MathOperation {
    @Override
    public MathObject visit(Fraction fraction) {
        return fraction;
    }
    @Override
    public MathObject visit(Complex complex) {
        return complex;
    }
    @Override
    public MathObject visit(Matrix matrix) {
        return matrix.determinant();
    }
}

klasa Main:

public class Main {
    public static void main(String[] args) {
        MathObject object1 = new Complex();
        MathObject object2 = new Matrix();
        MathObject object3 = object1.compute(new Multiplication(object1));
        MathObject object4 = object1.compute(new Multiplication(object2));
        MathObject object5 = object2.compute(new Determinant());
        MathObject object6 = object5.compute(new Multiplication(object5));

        System.out.println("obj1: " + object1.getClass());
        System.out.println("obj2: " + object2.getClass());
        System.out.println("obj3: " + object3.getClass());
        System.out.println("obj4: " + object4.getClass());
        System.out.println("obj5: " + object5.getClass());
        System.out.println("obj6: " + object6.getClass());
    }
}
output:
obj1: class Complex
obj2: class Matrix
obj3: class Complex
obj4: class Matrix
obj5: class Complex
obj6: class Complex

Wydaje mi się że to co napisałem nie wygląda najpiękniej, ale działa, bez użycia instanceof. Czy takie rozwiązanie jest akceptowalne ?

0

Najprościej by było w prototypach funkcji dawać konkretne typy (Complex, Matrix, itp.). Z drugiej strony problem, jeśli nie tu, wystąpi w momencie podawania argumentów. Niestety Java jest na to za głupia. Myślę że dorosłeś do tego żeby użyć mądrzejszego języka. Scala pozwoli ci to zrobić, nie tracąc kompatybilności z Javą.

1 użytkowników online, w tym zalogowanych: 0, gości: 1