Design Pattern per metodi astratti interdipendenti

3

Voglio modellare alcune strutture matematiche. Per questo scopo voglio definire un'interfaccia, una classe astratta per algoritmi di uso generale e implementazioni concrete di quella classe (ne ho in mente tre).

Ora sorge la situazione che gli algoritmi di scopo generale che non dipendono dai dettagli della struttura dati, ma dipendono solo dai tre metodi fondamentali int Length() , Set<int> DescentSet() e int[] Normalform() . Normalmente questo significa che userò il modello di modello di metodo, cioè, vorrei rendere astratti questi tre metodi e lasciare che le implementazioni concrete si occupino di esso. Ma: quei metodi sono interdipendenti. Ho solo bisogno di qualcuno per definire gli altri due. Quindi dovrei davvero farne uno solo astratto. Ma le tre implementazioni previste sono esattamente diverse su quale di esse preferirebbero essere il metodo astratto: il primo ha un calcolo del tempo facile Length , il secondo può facilmente calcolare DescentSet e il terzo ottiene Normalform gratuitamente.

Qual è la soluzione più pulita per questa situazione?

    
posta Johannes Hahn 14.10.2016 - 13:58
fonte

5 risposte

4

Utilizzerei il modello di strategia qui.

Un'interfaccia che espone i tre metodi. Tre classi concrete che definiscono ciascuna un metodo diverso e le altre in termini di esso.

Qualsiasi algoritmo di uso generale può essere spostato sul client o sul contesto. Non consiglio l'ereditarietà o il modello di modello.

    
risposta data 14.10.2016 - 14:36
fonte
2

Non hai taggato con un linguaggio di programmazione specifico, quindi fornirò la soluzione in ML, che è almeno la pena di ponderare, se non direttamente applicabile a OOP.

Questa è una situazione abbastanza comune. Spesso siamo in grado di definire una suite di funzioni da una singola funzione di base. Ad esempio, se abbiamo la funzione "minore di" leq: t * t -> bool , possiamo definire la suite completa di relazioni booleane:

geq a b = leq b a
eq a b = (leq a b) and (geq a b)
le a b = (leq a b ) and not (eq a b)
ge a b = (geq a b) and not (eq a b)

Una soluzione pulita (-ish) consiste nell'utilizzare un modulo parametrico (functor) che accetta una struttura con la firma richiesta e genera una struttura estesa. Utilizzando l'esempio precedente, possiamo definire il seguente modulo parametrico:

functor MakeRelationalAlgebra(Ord: ORD) = 
struct
    geq a b = Ord.leq b a
    eq a b = (Ord.leq a b) and (geq a b)
    le a b = (Ord.leq a b ) and not (eq a b)
    ge a b = (geq a b) and not (eq a b)
end

Ancora una volta, non hai menzionato quale lingua stai usando, solo che sembra essere un linguaggio OOP, quindi non sono sicuro di come questo possa tradursi. Ma penso che sia concettualmente interessante.

    
risposta data 14.10.2016 - 17:36
fonte
2

Those methods are interdependent. I really only need any one of them to define the other two.

Questo non sembra accurato. In particolare:

  • La lunghezza può essere derivata dall'insieme o dalla forma normale
  • L'insieme può essere derivato dalla forma normale
  • La forma normale non può essere derivata da nient'altro (se è simile a qualcosa che sembra)

L'interdipendenza implica un ciclo, ma qui non vi è alcun ciclo, le relazioni formano un albero. E questo è fondamentale, perché la forma normale è alla radice di questo albero, quindi non puoi farne a meno. Ciò lo rende una scelta chiara per il tuo metodo astratto.

Se la forma normale non è necessaria per tutti i casi d'uso, probabilmente hai un disegno maleodorante e devi cambiare le tue astrazioni. Una cattiva alternativa potrebbe essere quella di esporre sull'interfaccia tutti e 3 i metodi che ti servono, e in implementazioni che non vogliono mantenere il set più costoso (richiede più memoria della lunghezza) o la forma normale ancora più costosa (probabilmente richiede più memoria rispetto al set), potresti lanciare un UnsupportedOperationException . Ma io non lo consiglio. I metodi di interfaccia inutilizzati suggeriscono un design puzzolente.

    
risposta data 15.10.2016 - 08:46
fonte
1

Utilizzerei una combinazione della delega con interfacce funzionali (per la componibilità) e metodi factory (per evitare una composizione errata).

@FunctionalInterface
public interface Length {
    int length();
}

@FunctionalInterface
public interface DescentSet {
    Set<Integer> descentSet();
}

@FunctionalInterface
public interface NormalForm {
    int[] normalForm();
}

public final class Algorithm {
    private final Length l;
    private final DescentSet d;
    private final NormalForm n;

    private Algorithm(Length l, DescentSet d, NormalForm n) {
        this.l = l;
        this.d = d;
        this.n = n;
    }

    public void compute(int inputData) {
        //TODO use l, d and n
    }

    public static Algorithm algoName1(Length l) {
        DescentSet easySet = () -> new HashSet<>(l.length());
        NormalForm easyNormal = () -> new int[l.length()];

        return new Algorithm(l, easySet, easyNormal);
    }

    public static Algorithm algoName2(DescentSet d) {
        Length easyLength = () -> d.descentSet().size();
        NormalForm easyNormal = () -> new int[d.descentSet().size()];

        return new Algorithm(easyLength, d, easyNormal);
    }

    public static Algorithm algoName3(NormalForm n) {
        Length easyLength = () -> n.normalForm().length;
        DescentSet easySet = () -> new HashSet<>(n.normalForm().length);

        return new Algorithm(easyLength, easySet, n);
    }
}
    
risposta data 18.10.2016 - 15:39
fonte
0

Utilizza delegati e chiusure, separatamente o in combinazione.

Nella mia mente questa è l'idea del metodo template ma il template è all'interno del metodo stesso. Inoltre, un delegato è un modello di metodo runtime.

Sembra che tu voglia usare una fabbrica e / o un costruttore.

    
risposta data 14.10.2016 - 17:10
fonte