Problema di politica di responsabilità singola

2

Sono un po 'bloccato usando la singola politica di responsabilità.

Ho una classe chiamata Parser , che può essere configurata per analizzare l'input in un determinato modo. Per comodità, limiterò le proprietà di configurazione a una singola proprietà config nel seguente esempio:

class Parser {

    boolean config;
    State state;

    AST parse(String input) { ... }
}

Voglio riutilizzare l'istanza di analisi per più analisi. Il problema è che lo stato è ridondante per l'istanza stessa, tuttavia è necessario durante l'analisi. Oltre al sovraccarico dello stato in memoria, non funziona nell'ambiente multi-thread.

Lo stesso vale per una classe chiamata Compiler :

class Compiler {

    boolean config;
    State state;

    Code compile(String input) { ... }
}

Mi stavo chiedendo come questo sia generalmente risolto.

Nota: Questo è forse un esempio minimo, ma spesso mi viene in mente di pensare a un design migliore.

    
posta Tim 03.10.2015 - 12:56
fonte

3 risposte

1

Va bene se il parser e il compilatore hanno lo stato, ma non è necessario riutilizzare gli oggetti. Quello che tendo a fare è offrire un metodo pubblico statico Compiler.compile() o Parser.parse() che si occupa di creare un'istanza di un oggetto stateful, alimenta l'input ad esso, recupera il risultato e restituisce il risultato mentre scartando l'oggetto. Poiché i metodi statici sono problematici, possiamo dividere la cosa dello parser di stato dall'API pubblica che fornisce il metodo parse() :

public interface Parser {
  public Ast parse(String input);
}

/*internal*/ interface ParserImpl {
  Ast result();
  void doFoo();
  void doBar();
}

class ParserAPI implements Parser {
  @Override
  public Ast parse(String input) {
    ParserImpl p = makeParserImpl(input):
    p.doFoo();
    p.doBar();
    return p.result();
  }

  protected /*virtual*/ ParserImpl makeParserImpl(String input) {
    return new DefaultParserImpl(input, otherDependency);
  }
}

/*internal*/ class DefaultParserImpl implements ParserImpl {
  State state;

  DefaultParserImpl(String input, Dependency otherDependency) {
    ... // initialize state
  }

  @Override
  Ast result() { ... }

  @Override
  void doFoo() { ... }

  @Override
  void doBar() { ... }
}

Qual è il punto di questa indiretta? Testabilità. La classe ParserAPI utilizza il modello del modello di modello in modo che le sottoclassi possano scambiare la dipendenza su DefaultParserImpl , ad es. usare un oggetto finto. Mentre un utente di Parser può continuare a chiamare parse ancora e ancora sullo stesso oggetto, in effetti stiamo ricreando un nuovo stato per ogni analisi e non manteniamo lo stato di analisi dopo che l'analisi è stata completata. Questo non solo rende facile testare il parser e il codice che dipende dal parser; creare un oggetto nuovo per ogni analisi rende anche più facile ragionare sulla correttezza.

Lo svantaggio è che ora abbiamo un'altra classe nel nostro sistema, quindi perdiamo un po 'la semplicità. Tuttavia, questo è compensato dall'API più semplice esposta a un utente parser.

    
risposta data 03.10.2015 - 14:00
fonte
1

Una volta ho affrontato un problema simile. Mentre la mia soluzione non è certamente ottimale, ha fatto il trucco.

Ho usato un livello aggiuntivo tra la compilazione effettiva e la% di corrispondenzaCompiler utilizzata in modo concorrente. Questa classe si chiamerebbe Compilation nel tuo esempio.

/*
 This class exists once for multiple (simultaneous) inputs
*/
public class Compiler{
    CompilationParameters compilationParameters;

    public Compiler(CompilationParameters params){
        ...
    }

    public CompilationResult compile(CompilationInput input){           // Of course you can simplify this to a string

        Compilation compilation = new Compilation(compilationParameters, input);
        compilation.compile();
        return compilation.getResult();
    }
}

/*
 This class exists exactly once per input
*/
public class Compilation{
    CompilationState state;

    public Compiler(CompilationParameters params, CompilationInput input){
        ...
    }
    public void compile(){
        ...
    }
    public CompilationResult getResult(){
        ...
    }
}

Ora potresti obiettare che questo viola il Principio Aperto-Chiuso. E avresti ragione. Questa soluzione può essere estesa per diversi tipi di compilation utilizzando un modello di fabbrica per la creazione di Compilation -object. Immagina quanto segue:

public abstract class Compilation{...};
public class CompiletimeOptimizedCompilation extends Compilation{...};
public class RuntimeOptimizedCompilation extends Compilation{...};
public class CompilationFactory{
    public static Compilation createCompilation(CompilationType type, CompilationParameters params, CompilationInput input)
    {
        switch(CompilationType)
        {
        case eCompileTimeOptimized:
            return new CompileTimeOptimizedCompilation(params, input);
        case eRuntimeOptimized:
            return new RuntimeOptimizedCompilation(params, input);
        case ...
        }
    }
}

In questo caso Compilation potrebbe anche essere un'interfaccia invece di una classe astratta. Per decidere che probabilmente avrei bisogno di maggiori dettagli sulle implementazioni. E naturalmente il Compiler -class si ridurrebbe in:

public class Compiler{
    CompilationParameters compilationParameters;

    public Compiler(CompilationType type, CompilationParameters params){ // This type CAN be part of the parameters
        ...
    }

    public CompilationResult compile(CompilationInput input){           // Of course you can simplify this to a string

        Compilation compilation = CompilationFactory.createCompilation(type, compilationParameters, input);
        compilation.compile();
        return compilation.getResult();
    }
}

Ovviamente lo stesso funziona per il tuo Parser -class, ma ho pensato che sarebbe stato un cattivo esempio perché il nome di "analizzare" è "l'analisi".

Se hai ulteriori domande o commenti, non esitare a chiedere.

PS: scusate eventuali errori di ortografia. Non sono nativo.

    
risposta data 09.10.2015 - 11:35
fonte
0

Se il tuo State è necessario solo dal metodo parse() , perché lo tieni nell'istanza? Perché non creare oggetti State solo quando sono veramente necessari? Potresti quindi passarli ai metodi, chiamati da parse() se necessario.

Se la maggior parte dei metodi all'interno di Parser utilizza State , non dovresti semplicemente riutilizzare l'istanza. La costruzione di oggetti è in genere molto economica se si mantengono leggeri i costruttori. Se la configurazione di Parser è complessa, utilizzare le fabbriche per costruirle. Utilizza pesi volanti se il processo di configurazione stesso richiede molto tempo.

Se puoi evitare di usare un singolo oggetto da thread diversi, fallo per favore a tutti i costi. Meno problemi di sincronizzazione hai, meglio è, credimi.

    
risposta data 09.10.2015 - 10:46
fonte