dimanche 12 mai 2019

Is the structure of my class following the SOLID and singleton design principles?

I am a new data science student and I'm trying to understand design patterns and how to program a C# class the "finest" way possible.

I'm trying to build a class using the "singleton design pattern" while following the SOLID principles. I feel the class follows said principles and pattern quite well, but is there something that could be improved? Having a more experienced programmer's feedback and maybe another way of structuring the code would come along way for me!

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DIKUArcade.Entities;
using DIKUArcade.Graphics;
using DIKUArcade.Math;

// Singleton Design Pattern with the SOLID principles in mind
namespace SpaceTaxi_2 {

    public class LevelParser {

        private LevelParser() {}

        private static class LevelParserHolder {
            public static readonly LevelParser INSTANCE = new LevelParser();
        }

        public static LevelParser GetInstance() {
            return LevelParserHolder.INSTANCE;
        }

        public Dictionary<char, string> Platforms = new Dictionary<char, string>();
        public Dictionary<char, string> Information = new Dictionary<char, string>();
        public Dictionary<char, string> Exits = new Dictionary<char, string>();
        public List<Picture> InformationEntity = new List<Picture>();
        public List<Picture> PlatformEntity = new List<Picture>();
        public List<Picture> ExitEntity = new List<Picture>();

        public void LoadParser(string directory, string fileName) {

            var content = GetContent(directory, fileName);
            Platforms = GetPlatforms(content);
            Information = GetTranslator(content);
            Exits = GetExits(content);

            var map = GetGameMap(content);
            AddEntities(GetGameMap(content));
        }

        /// <summary>
        ///  Checks whether a file exists or not
        /// </summary>
        private void ValidatePath(string file) {
            if (!File.Exists(file)) {
                throw new FileNotFoundException($"Error: The path to \"{file}\" does not exist.");
            }
        }

        /// <summary>
        ///  Finds full path to directory (e.g. directoryName: "Levels" or "Assets")
        ///  Starts from /bin/Debug folder, then goes to parent /bin, and so on.
        ///  Casts an exception if we iterated down to the root of the tree.
        /// </summary>
        private string GetPath(string directoryName) {
            DirectoryInfo dir = new DirectoryInfo(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location));

            while (dir.Name != directoryName) {
                if (dir.FullName == dir.Root.Name) {
                    throw new FileNotFoundException($"Error: Directory \"{directoryName}\" does not exist.");
                } else {
                    foreach (var i in dir.GetDirectories()) {
                        if (i.Name == directoryName) {
                            return i.FullName;
                        }
                    }
                    dir = dir.Parent;
                }
            }
            return dir.FullName;
        }

        /// <summary>
        ///  Collects a string[] containing all strings in the file
        /// </summary>
        private string[] GetContent(string directoryName, string fileName) {

            var dir = GetPath(directoryName);
            string path = Path.Combine(dir, fileName);
            ValidatePath(path);
            return File.ReadAllLines(path);
        }

        /// <summary>
        ///  Extracts ASCII characters from the txt file and appends it
        ///  to an array. Later we will use this array to draw pictures.
        /// </summary>
        private List<char> GetGameMap(string[] txtFile) {
            var charList = new List<char>();
            for (int i = 0; i < 23; i++) {
                foreach (var j in txtFile[i]) {
                    charList.Add(j);
                }
            }
            return charList;
        }

        /// <summary>
        ///  Extracts the txt information about what each character represents. 
        ///  Example from the-beach.txt: "A) aspargus-edge-left.png"
        ///  Here we split ')' to first get the character, A, and it's
        ///  corrospondent filename, 'aspargus-edge-left.png'.
        /// </summary>
        private Dictionary<char,string> GetTranslator(string[] txtFile) {
            var dict = new Dictionary<char, string>();
            foreach (var i in txtFile) {
                if (i.Contains(")")) {
                    var tile = i.Split(')')[0].ToCharArray()[0];
                    var name = i.Split(')')[1].Trim();

                    dict.Add(tile, name);
                }
            }
            return dict;
        }

        /// <summary>
        ///  Works the same way as GetTranslator, but finds only platforms
        /// </summary>
        private Dictionary<char,string> GetPlatforms(string[] txtFile) { // Read entire file
            // Search through a .txt file, find the string "Platforms" and take it's following characters
            // Example: "Platforms: J, i, r". We take J, i, r and put it in a dictionary with its correspondent filename.png
            IEnumerable<string> findPlatforms = txtFile.Where(l => l.StartsWith("Platforms"));
            var platforms = findPlatforms.First().Split(':')[1].Split(',');

            // Add each char and filename to a dictionary
            var dict = new Dictionary<char, string>();
            foreach (var i in platforms) {
                var line = txtFile.Where(l => l.StartsWith(i.Trim() + ") "));
                var tile = line.First().Split(' ', ')')[0].ToCharArray()[0];
                var filename = line.First().Split(' ')[1];
                dict.Add(tile, filename);
            }
            return dict;
        }

        /// <summary>
        ///  Works the same way as GetTranslator, but finds only exits
        /// </summary>
        private Dictionary<char, string> GetExits(string[] txtFile) {
            var dict = new Dictionary<char,string>();

            foreach (var i in txtFile) {
                if (i.Contains('^')) {
                    dict.Add('^', "aspargus-passage.png");
                    return dict;
                }
            }
            return dict;
        }

        /// <summary>
        ///    We iterate from top-left (0,1) of the program to bottom-right (1,0)
        /// </summary>
        private List<Entity> AddEntities(List<char> map) {    

            List<Entity> entityContainer = new List<Entity>();

            float tmpX = Constants.X_MIN;
            float tmpY = Constants.Y_MAX;

            int index = 0;

            // Going from top (y 1.0) to bottom (y 0.0)
            while (tmpY > Constants.Y_MIN)
            {
                // Going from left (x 0.0) to right (x 1.0)
                while (tmpX < Constants.X_MAX)
                {
                    // There can be empty strings in our list of strings.
                    if (map[index].ToString() == " ")
                    {
                        index += 1;
                    }
                    else
                    {
                        if (Information.ContainsKey(map[index])) {
                            var shape = new StationaryShape(new Vec2F(tmpX, tmpY), new Vec2F(Constants.WIDTH, Constants.HEIGHT));
                            var file = Path.Combine(GetPath("Assets"), "Images", Information[map[index]]);
                            Console.WriteLine(file);
                            ValidatePath(file);
                            InformationEntity.Add(new Picture(shape, new Image(file)));
                        } 
                        if (Platforms.ContainsKey(map[index])) {
                            var shape = new StationaryShape(new Vec2F(tmpX, tmpY), new Vec2F(Constants.WIDTH, Constants.HEIGHT));
                            var file = Path.Combine(GetPath("Assets"), "Images", Platforms[map[index]]);
                            ValidatePath(file);
                            PlatformEntity.Add(new Picture(shape, new Image(file)));
                        }
                        if (Exits.ContainsKey(map[index])) {
                            var shape = new StationaryShape(new Vec2F(tmpX, tmpY), new Vec2F(Constants.WIDTH, Constants.HEIGHT));
                            var file = Path.Combine(GetPath("Assets"), "Images", Exits[map[index]]);
                            ValidatePath(file);
                            ExitEntity.Add(new Picture(shape, new Image(file)));
                        }
                        index += 1;
                    }

                    tmpX += Constants.WIDTH;
                }
                tmpX = 0;
                tmpY -= Constants.HEIGHT;
            }
            return entityContainer;
        }
    }
}

The class is a parser and should basically read a .txt file and translate it into lists with entities containing images.

For example, with "LoadParser" I was thinking of giving an opportunity for someone calling the class to easily "activate" the other methods, but not quite sure if this approach is okay.

Aucun commentaire:

Enregistrer un commentaire