Progettazione di un "oggetto di archiviazione": oggetto utilizzato esclusivamente per memorizzare i dati

4

Ho un editor di file. Legge da un file, crea un oggetto con i dati letti e può quindi scrivere quei dati in uno stesso (o altro) file.

Ogni volta che un utente apre un file, viene creata una nuova scheda e a essa viene assegnata un'istanza privata dell'oggetto, che la contiene nella scheda.

L'oggetto stesso è complesso ed è composto da altri oggetti. Ogni tipo personalizzato ha metodi Write() e Read() che estraggono e salvano i dati da e nel file. Qualcosa del genere:

TopObject a0
    int a
    int b
    string c
    CustomObjectMid d
        int e
        bool f
    List<CustomObjectMid2> g
    CustomObjectMid3 h
        CustomObjectBottom i
            long j
            ulong k
        Dictionary<string, int> l
    byte m

L'oggetto reale è molto più grande e ha più profondità (livelli) ad esso.

TopObject , CustomObjectMid , CustomObjectMid2 , CustomObjectMid3 e CustomObjectBottom hanno tutti i metodi Write() e Read() che popolano tutti i tipi standard con i dati del file. Non contengono altri metodi. Non hanno nemmeno costruttori (diversi da quelli predefiniti).

L'unico scopo di questo oggetto è conservare le informazioni. È essenzialmente una struttura. Questo è in realtà scritto come una DLL che lo scopo di consentire agli utenti di dare un senso a quel particolare formato di file (leggere da esso e scrivere ad esso). Quindi il codice attuale non è nel mio programma, ma fa parte di una libreria. Pertanto, vorrei mantenere la libreria relativamente "neutra" - non voglio implementare il codice specifico per l'editor nella libreria.

Il modo in cui questo oggetto è implementato è che tutto è pubblico e posso accedere a qualsiasi cosa in a0.h.i.j modo. Tendo a passare parti rilevanti degli oggetti a pezzi di programma che li usano, quindi non devo fare riferimento a tutto dall'alto (a0).

Altre parti del programma visualizzano i dati all'utente e traducono le modifiche apportate dall'utente all'oggetto. Eseguono inoltre i controlli necessari per mantenere l'integrità dei dati (impedire il passaggio di caratteri per interi, ecc.)

La mia domanda è questo buon design? Ho deciso di non usare getter e setter. Sarebbe solo gonfiare il codice senza motivo (almeno non riesco a vederne uno).

Tutto ciò che viene detto, per qualche motivo questo mi sembra un brutto codice (non so perché), ma non vedo alcun altro modo di implementarlo senza fare eccessivo, dato il suo ruolo nel programma.

EDIT: Ecco il codice completo della classe root. Tutte le altre classi hanno la stessa struttura:

namespace SaveManipulator.GameData
{
[Serializable]
public class CustomFile
{
    //version = 7
    public Value<uint> saveFileVersion;

    //ecd
    public ECD ecd;

    //food
    public Food food;

    //drink
    public Drink drink;

    //inventory
    public Stack[] inventory;

    //selectedSlot
    public Value<int> selectedSlot;

    //bag
    public Stack[] bag;

    //craftedList
    public HashSet<string> craftedList;

    //spawnPoints
    public List<Vector3Di> spawnPoints;

    //SpawnKey
    public Value<long> SpawnKey;

    //notSaved = true
    public Value<bool> randomBoolean;

    //notSaved = 0
    public Value<short> randomShort;

    //bLoaded
    public Value<bool> bLoaded;

    //lastPosition
    public Vector3Di lastPosition;

    //id
    public Value<int> id;

    //droppedPosition
    public Vector3Di droppedPosition;

    //kills
    public Value<int> kills;

    //fKills
    public Value<int> fKills;

    //deaths
    public Value<int> deaths;

    //score
    public Value<int> score;

    //equipment
    public Equipment equipment;

    //unlockedList
    public List<string> unlockedList;

    //notSaved = 1
    public Value<ushort> randomUShort;

    //marker
    public Vector3Di marker;

    //favorites
    public Equipment favorites;

    //experience
    public Value<uint> experience;

    //level
    public Value<int> level;

    //bCrouched
    public Value<bool> bCrouched;

    //cdata
    public Cdata cdata;

    //favoriteList
    public List<string> favoriteList;

    //J
    public Skills skills;

    //totalItems
    public Value<uint> totalItems;

    //distance
    public Value<float> distance;

    //life
    public Value<float> life;

    //waypoints
    public Waypoints waypoints;

    //skillPoints
    public Value<int> skillPoints;

    //quests
    public Quests quests;

    //deathTime
    public Value<int> deathTime;

    //currentL
    public Value<float> currentL;

    //bDead
    public Value<bool> bDead;

    public CustomFile Clone()
    {
        Stream stream = new FileStream("CustomFile ", FileMode.Create, FileAccess.Write, FileShare.None);

        IFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, this);
        stream.Close();

        stream = new FileStream("CustomFile", FileMode.Open, FileAccess.Read, FileShare.None);
        PlayerDataFile clone = (PlayerDataFile)formatter.Deserialize(stream);
        stream.Close();

        File.Delete("CustomFile");

        return clone;
    }

    public void Read(string path)
    {
        BinaryReader reader = new BinaryReader(new FileStream(path, FileMode.Open));

        if (reader.ReadChar() == 'd' && reader.ReadChar() == 't' && reader.ReadChar() == 's' &&
            reader.ReadChar() == '
TopObject a0
    int a
    int b
    string c
    CustomObjectMid d
        int e
        bool f
    List<CustomObjectMid2> g
    CustomObjectMid3 h
        CustomObjectBottom i
            long j
            ulong k
        Dictionary<string, int> l
    byte m
') { saveFileVersion = new Value<uint>((uint)reader.ReadByte()); ecd = new ECD(); ecd.Read(reader); food = new Food(); food.Read(reader); drink = new Drink(); drink.Read(reader); inventory = Stack.Read(reader); selectedSlot = new Value<int>((int)reader.ReadByte()); bag = Stack.Read(reader); //num int craftedListLength = (int)reader.ReadUInt16(); craftedList = new HashSet<string>(); for (int i = 0; i < craftedListLength; i++) { craftedList.Add(reader.ReadString()); } //b byte spawnPointsCount = reader.ReadByte(); spawnPoints = new List<Vector3Di>(); for (int j = 0; j < (int)spawnPointsCount; j++) { Vector3Di spawnPoint = new Vector3Di(); spawnPoint.x = new Value<int>(reader.ReadInt32()); spawnPoint.y = new Value<int>(reader.ReadInt32()); spawnPoint.z = new Value<int>(reader.ReadInt32()); spawnPoints.Add(spawnPoint); } spawnKey = new Value<long>(reader.ReadInt64()); randomBoolean = new Value<bool>(reader.ReadBoolean()); randomShort = new Value<short>(reader.ReadInt16()); bLoaded = new Value<bool>(reader.ReadBoolean()); lastPosition = new Vector3Di(); lastPosition.x = new Value<int>(reader.ReadInt32()); lastPosition.y = new Value<int>(reader.ReadInt32()); lastPosition.z = new Value<int>(reader.ReadInt32()); lastPosition.heading = new Value<float>(reader.ReadSingle()); id = new Value<int>(reader.ReadInt32()); droppedPosition = new Vector3Di(); droppedPosition.x = new Value<int>(reader.ReadInt32()); droppedPosition.y = new Value<int>(reader.ReadInt32()); droppedPosition.z = new Value<int>(reader.ReadInt32()); kills = new Value<int>(reader.ReadInt32()); fKills = new Value<int>(reader.ReadInt32()); deaths = new Value<int>(reader.ReadInt32()); score = new Value<int>(reader.ReadInt32()); equipment = Equipment.Read(reader); //num int count = (int)reader.ReadUInt16(); unlockedList = new List<string>(); for (int k = 0; k < count ; k++) { unlockedList .Add(reader.ReadString()); } randomUShort = new Value<ushort>(reader.ReadUInt16()); marker = new Vector3Di(); marker.x = new Value<int>(reader.ReadInt32()); marker.y = new Value<int>(reader.ReadInt32()); marker.z = new Value<int>(reader.ReadInt32()); favorites = Equipment.Read(reader); experience = new Value<uint>(reader.ReadUInt32()); level = new Value<int>(reader.ReadInt32()); bCrouched = new Value<bool>(reader.ReadBoolean()); cdata = new Cdata(); cdata.Read(reader); //num int favoriteListSize = (int)reader.ReadUInt16(); favoriteList = new List<string>(); for (int l = 0; l < favoriteListSize; l++) { favoriteList.Add(reader.ReadString()); } //num2 int size = (int)reader.ReadUInt32(); skills = new Skills(); skills.Read(reader); totalItems = new Value<uint>(reader.ReadUInt32()); distance = new Value<float>(reader.ReadSingle()); life = new Value<float>(reader.ReadSingle()); waypoints = new Waypoints(); waypoints.Read(reader); skillPoints = new Value<int>(reader.ReadInt32()); quests = new Quests(); quests.Read(reader); deathTime = new Value<int>(reader.ReadInt32()); currentL = new Value<float>(reader.ReadSingle()); bDead = new Value<bool>(reader.ReadBoolean()); //irelevant bytes reader.ReadByte(); reader.ReadBoolean(); reader.Close(); } else { throw new IOException("Save file corrupted!"); } } public void Write(string path) { BinaryWriter writer = new BinaryWriter(new FileStream(path, FileMode.Create)); writer.Write('d'); writer.Write('t'); writer.Write('s'); writer.Write((byte)0); writer.Write((byte)saveFileVersion.Get()); ecd.Write(writer); food.Write(writer); drink.Write(writer); Stack.WriteItemStack(writer, inventory); writer.Write((byte)selectedSlot.Get()); Stack.WriteItemStack(writer, bag); writer.Write((ushort)craftedList.Count); HashSet<string>.Enumerator enumerator = craftedList.GetEnumerator(); while (enumerator.MoveNext()) { writer.Write(enumerator.Current); } writer.Write((byte)spawnPoints.Count); for (int i = 0; i < spawnPoints.Count; i++) { writer.Write(spawnPoints[i].x.Get()); writer.Write(spawnPoints[i].y.Get()); writer.Write(spawnPoints[i].z.Get()); } writer.Write(selectedSpawnKey.Get()); writer.Write(randomBoolean.Get()); writer.Write(randomShort.Get()); writer.Write(bLoaded.Get()); writer.Write((int)lastPosition.x.Get()); writer.Write((int)lastPosition.y.Get()); writer.Write((int)lastPosition.z.Get()); writer.Write(lastPosition.heading.Get()); writer.Write(id.Get()); writer.Write(droppedPosition.x.Get()); writer.Write(droppedPosition.y.Get()); writer.Write(droppedPosition.z.Get()); writer.Write(kills.Get()); writer.Write(zKills.Get()); writer.Write(deaths.Get()); writer.Write(score.Get()); equipment.Write(writer); writer.Write((ushort)unlockedList.Count); List<string>.Enumerator enumerator2 = unlockedList.GetEnumerator(); while (enumerator2.MoveNext()) { writer.Write(enumerator2.Current); } writer.Write(randomUShort.Get()); writer.Write(marker.x.Get()); writer.Write(marker.y.Get()); writer.Write(marker.z.Get()); favorites.Write(writer); writer.Write(experience.Get()); writer.Write(level.Get()); writer.Write(bCrouched.Get()); cdata.Write(writer); writer.Write((ushort)favoriteList.Count); List<string>.Enumerator enumerator3 = favoriteList.GetEnumerator(); while (enumerator3.MoveNext()) { writer.Write(enumerator3.Current); } skills.Write(writer); writer.Write(totalItems.Get()); writer.Write(distance.Get()); writer.Write(life.Get()); waypoints.Write(writer); writer.Write(skillPoints.Get()); quests.Write(writer); writer.Write(deathTime.Get()); writer.Write(currentL.Get()); writer.Write(bDead.Get()); writer.Write((byte)88); writer.Write(true); writer.Close(); } public CustomFile(string path) { Read(path); } } }

Dato che il codice è per lo più copiato dalla sorgente del gioco, l'ho offuscato un po ', ecco perché alcuni nomi possono sembrare vaghi. Anche Value<Type> è una classe wrapper usata per passare attorno ai tipi di valore pur mantenendo il loro riferimento senza doverlo pensare esplicitamente. In questo modo l'utente può modificare i bit del file più facilmente. Non vedo davvero come potrei salvare parzialmente il file. I file di salvataggio sono in genere da 4kB a (la mia stima) 10kB. Non sono così grandi.

    
posta Karlovsky120 26.09.2016 - 05:15
fonte

2 risposte

4

Oh caro, che fine ha fatto il mondo, quando le persone sono preoccupate di usare una struttura semplice?

Sì, è perfettamente a posto. Il tuo codice non ha odore. Le strutture sono solo un altro nome per i tipi di record (sebbene usare la parola structure solitamente connota la mutevolezza), che sono una forma di tipo di prodotto . I tipi di prodotto sono uno dei due tipi di dati compositi fondamentali nei linguaggi di programmazione / sistemi di tipi (l'altro è costituito da tipi di somma). Più puoi contare su questi tipi semplici, meglio è. Getter e setter sono assolutamente esagerati.

Per quello che vale, objects sono solo tipi di record in cui alcuni campi sono funzioni (con una spruzzata di auto-riferimento per mantenere le cose adeguatamente confuse). Classi sono solo un alias di tipo per un determinato tipo di record insieme a una funzione (chiamata il costruttore) che crea record di quel tipo.

    
risposta data 26.09.2016 - 05:32
fonte
1

Non direi male, ma sarebbe molto difficile programmare bene

Il problema chiave riguarda la necessità di salvare / leggere solo parti dell'oggetto. Conducendo ai metodi di lettura / scrittura in ogni oggetto piuttosto che solo sull'oggetto root.

Puoi immaginare i problemi, per esempio vedo che hai una lista g sotto l'oggetto radice. Se creo una nuova g e la aggiungo alla lista, viene salvata automaticamente? Devo chiamare g.save () o root.save () o entrambi?

Posso caricare un oggetto secondario senza prima caricare il suo genitore?

Sembra che questa libreria stia seguendo uno schema di registrazione attivo. Un approccio più moderno sarebbe quello di salvare / caricare solo l'oggetto 'Aggregate root' di livello superiore.

Probabilmente scriverebbe un oggetto repository separato per gestire il formato del file e scrivere su disco ecc.

Tuttavia, alcuni anni fa il record attivo era di gran moda, e se il requiremenr per caricare / salvare parti dell'oggetto root è un must, allora dovresti scrivere qualcosa di simile. So che le persone dome adottano un approccio molto difficile per "codificare l'odore". Sono un po 'più clemente. Direi sì è un odore di codice, ma se indaghi potresti scoprire che c'è una buona ragione per questo!

In ogni caso, dal momento che stai consumando la libreria, non hai molta scelta in merito.

La mia reazione istintiva è di provare a racchiuderlo in uno stile di deposito di salvataggio / caricamento, iterando attraverso l'albero e salvando tutti gli elementi. Passare solo la radice nelle proprie funzioni ecc. Ma senza una piena comprensione è una dura chiamata a supporre che questo sarebbe un uso ragionevole del tempo.

Stai scrivendo un sacco di duplicati 'salva tutto g e childern' 'carica tutto d dove x' la logica di stile? Hai trovato un bug in cui devi controllare il file per un genitore prima di sapere che child.save () verrà eseguito correttamente? o funziona "solo"?

Modifica ****

Scusa se ho appena realizzato una libreria che tu stesso hai scritto. Riscrivere definitivamente come una semplice struct in una dll Models più un FileRepository in una seconda dll. Quindi le persone possono consumare l'oggetto senza la logica di salvataggio ed è possibile aggiungere ulteriori DatabaseRepository, InMemoryRespository ecc ecc. Più tardi

    
risposta data 26.09.2016 - 09:47
fonte

Leggi altre domande sui tag