Due classi che si comportano in modo identico sono semanticamente differenti

2

Sto scrivendo un programma che è simile a Migration Record Migration di Ruby , in cui ogni migrazione ha sia un "Su" e "Giù" in termini di creazione di una modifica al database, "Up" significa portare avanti il database in tempo e "Giù" invertire tale modifica.

Il mio programma ha due funzioni principali per raggiungere i suoi casi d'uso

  1. Ottieni un elenco di script dal filesystem
  2. Esegui le migrazioni appropriate sul database

Le migrazioni "Su" e "Giù" sono archiviate in file separati, ma corrispondono a uno schema di denominazione simile

  • 2014000000000001_CreateTable_up.sql
  • 2014000000000001_CreateTable_down.sql

Esiste una correlazione 1: 1 tra gli script su e giù. Avere solo un up senza lo script down corrispondente non è valido (e viceversa).

Ho due classi nel mio progetto per rappresentare questi file del filesystem.

public sealed class UpScript // (and DownScript, which looks identical)
{
    public UpScript(long version, string name, string content)
    {
        Content = content;
        Version = version;
        Name = name;
    }

    public long Version { get; private set; }
    public string Name { get; private set; }
    public string Content { get; private set; }
}

Il motivo per cui non ho creato una singola classe chiamata Script è che non volevo creare ambiguità sullo scopo di un'istanza Script specifica. Le classi UpScript e DownScript non devono essere utilizzate come intercambiabili in quanto non conformi al Principio di sostituzione di Liskov.

Il problema che ho qui è che alcune parti del codice che hanno a che fare con entrambe le istanze UpScript o DownScript sembrano quasi identiche. Come posso ridurre la duplicazione senza perdere l'espressività di avere classi diverse?

Le mie attuali idee con preoccupazioni sono:

Classe base astratta

public abstract class Script
{
    protected Script(long version, string name, content)
    {
        // code
    }

    // properties
}

public sealed UpScript : Script
{
    public UpScript(long version, string name, string content)
        : this(version, name, content)
    {}
}

public sealed DownScript : Script
{
    public DownScript(long version, string name, string content)
        : this(version, name, content)
    {}
}

Questo riduce il codice ridondante nelle due classi, tuttavia, il codice che crea e consuma è ancora molto duplicato.

Singola classe con Direction proprietà

public enum Direction { Up, Down }

public sealed class Script
{
    protected Script(Direction direction, long version, string name, string content)
    {
        // ...
    }

    public Direction Direction { get; private set; }

    // ...
}

Questo codice riduce la duplicazione, tuttavia, aggiunge on on consumes per garantire che venga passato lo script corretto e crea ambiguità se ignori la proprietà Direction .

Singola classe con due% proprietà% di%

public sealed class Script
{
    protected Script(long version, string name, string upContent, string downContent)
    {
        // ...
    }

    public string UpContent { get; private set; }
    public string DownContent { get; private set; }
}

Questo riduce la duplicazione e non è ambiguo, tuttavia, l'uso del programma è quello di eseguire lo script su, o eseguire gli script in basso. Con questo schema, il codice che raccoglie e crea queste istanze di Content sta facendo il doppio del lavoro, non ritengo che non si tratti di ottimizzazione prematura, perché se l'utente vuole eseguire una migrazione up, non ha senso guardare giù gli script.

Con tutto ciò che ho detto, potrei considerare il problema sbagliato da risolvere completamente.

Se ulteriori informazioni potrebbero essere utili per formulare un suggerimento, puoi visualizzare ScriptMigrations su GitHub

    
posta Matthew 01.01.2015 - 03:27
fonte

4 risposte

3

Il mio C # è arrugginito, spero di non aver ignorato i limiti linguistici.

implementerei UpScript e DownScript come interfacce con gli stessi membri:

public interface IUpScript {
    public long Version { get; private set; }
    public string Name { get; private set; }
    public string Content { get; private set; }
}
// ...
public interface IDownScript {
    public long Version { get; private set; }
    public string Name { get; private set; }
    public string Content { get; private set; }
}

Questo approccio tenta di fornire il codice minimo necessario per creare una distinzione tra su e giù, niente di più. Pensieri:

  • Le classi che implementano un'interfaccia avranno le proprietà comuni che hai definito come negli altri tuoi approcci, ma non ereditano dettagli di implementazione come un costruttore.
  • Le tue classi di implementazione sarebbero libere di estendere altre classi se necessario.
  • Sembra che non sia necessario supportare più implementazioni di script su e giù, ma non avresti problemi a farlo.
risposta data 01.01.2015 - 10:31
fonte
1

Penso che la tua soluzione con la classe base astratta stia bene. È una programmazione OO comune: si dispone di un tipo astratto con due diverse implementazioni. Non dovresti preoccuparti della duplicazione della creazione di due classi. In un linguaggio come Scala che fornisce alcune funzioni sintattiche, è possibile definire tali casi in un modo più conciso, ma per C # e Java non penso che si possa fare molto meglio. In Scala puoi usare case classes per esempio:

trait Script(long version, string name, content)
case class UpScript(long version, string name, content) extends Script
case class DownScript(long version, string name, content) extends Script

Per quanto riguarda l'istanziazione, nel tuo caso attuale va bene dato che al momento hai solo due sottoclassi. Puoi pensare di avere fabbriche e poi quando leggi i file up, usa UpFactory, ad esempio, e viceversa. In questo modo il codice cliente non sarà vincolato all'attuazione effettiva.

    
risposta data 01.01.2015 - 04:28
fonte
1

La soluzione con la classe base astratta è la strada da percorrere.

Per ridurre la duplicazione nel codice che determina quali script elaborare, puoi introdurre una classe factory / metodo che crea un'istanza UpScript o DownScript in base a un parametro (lo stesso parametro che determina se gli script con il suffisso "_up" o "_down" dovrebbe essere usato).

Se l'ordine di esecuzione degli script dipende dalla direzione, è possibile aggiungere un metodo di confronto a Script (con implementazioni in UpScript e DownScript ) per ordinare correttamente le istanze Script .
L'esecuzione degli script diventa quindi facile come scorrere su una raccolta ordinata senza dover sapere se si sta eseguendo l'aggiornamento o il downgrade.

    
risposta data 01.01.2015 - 12:08
fonte
1

How do I reduce the duplication without losing the expressiveness of having different classes?

Considerando gli scenari in cui il tuo programma interagisce con gli script, come indicato nella tua descrizione di primo livello:

  1. Get a list of scripts off the filesystem
  2. Execute the appropriate migrations on the database

La prima decisione riguarda gli script o da applicare durante la migrazione:

public interface IScriptSource
{
    IEnumerable<IScript> ReadUpScripts();

    IEnumerable<IScript> ReadDownScripts();
}

La seconda decisione è quella di eseguire ogni script, che sposterà il database in avanti o all'indietro nel tempo:

public interface IScript
{
    void Execute();
}

Questo ci offre una buona base per affrontare la tua preoccupazione principale:

The problem that I have here is a few parts of code that are dealing with both either UpScript or DownScript instances look almost identical.

Con queste astrazioni di alto livello, possiamo modellare le implementazioni attorno ai dettagli condivisi:

public abstract class Script : IScript
{
    protected Script(long version, string name, string content)
    {
        Version = version;
        Name = name;
        Content = content;
    }

    protected long Version { get; private set; }
    protected string Name { get; private set; }
    protected string Content { get; private set; }

    public virtual void Execute()
    {
        SomethingShared();

        MoveInDirection();

        SomethingElseShared();
    }

    protected abstract void MoveInDirection();
}

Quindi differenzia le specifiche di ogni tipo:

public sealed class UpScript : Script
{
    public UpScript(long version, string name, string content)
        : base(version, name, content)
    {}

    protected override void MoveInDirection()
    {
        // Up-specific
    }
}

public sealed class DownScript : Script
{
    public DownScript(long version, string name, string content)
        : base(version, name, content)
    {}

    protected override void MoveInDirection()
    {
        // Down-specific
    }

    protected override SomethingElseShared()
    {
        base.SomethingElseShared();

        // Down-specific
    }
}

Ciò consentirà di consolidare il codice di diffusione accoppiandolo direttamente ai dati.

Riceve anche il timbro di approvazione Liskov a causa del IScriptSource che nasconde i tipi di calcestruzzo.

    
risposta data 02.01.2015 - 00:23
fonte