Is the usage of partial classes a good practice/design?

Viewed 675

Could you say splitting a class into partial classes is a way to encapsulate your code?

Also, why is splitting a code into partial classes not a part of a design pattern? I mean you kind of design the code by splitting it up for a better overview, right?

General.cs:

/*  SINGLETON DESIGN PATTERN (slides and https://sourcemaking.com/design_patterns/singleton)

     * INTENT
       - Ensure a class has only one instance, and provide a global point of access to it.
       - Encapsulated "just-in-time initialization" or "initialization on first use".

      * ABOUT
       - Make the class of the single instance object responsible for creation, initialization,
       access, and enforcement.

 */
namespace SpaceTaxi_3.States.GameRunning.Parser {
    public partial class LevelParser {

        private static LevelParser instance;

        public static LevelParser GetInstance() {
            return LevelParser.instance ?? (LevelParser.instance = new LevelParser());
        }

        public Dictionary<char, string> Obstacles;
        public Dictionary<char, string> Platforms;
        public Dictionary<char, string> Exits;
        public Dictionary<string, Tuple<int, char, string, int, int, Entity>> Costumer;
        public List<Entity> ObstacleEntities { get; set; }
        public List<Platform> PlatformEntities { get; set; }
        public List<Entity> ExitEntities { get; set; }

        public string[] LevelFile;

        // Wrapper
        public void Load(string fileName) {

            // Catches incorrect files here
            if (ValidMapCheck(fileName)) {
                PlatformEntities = new List<Platform>();
                ExitEntities = new List<Entity>();
                ObstacleEntities = new List<Entity>();

                Platforms = new Dictionary<char, string>();
                Exits = new Dictionary<char, string>();
                Obstacles = new Dictionary<char, string>();
                Costumer = new Dictionary<string, Tuple<int, char, string, int, int, Entity>>();

                LevelFile = ReadFile("Levels", fileName);

                Platforms = GetPlatforms(LevelFile);
                Exits = GetExits(LevelFile);
                Obstacles = GetObstacles(LevelFile);
                Costumer = GetCustomerInfo(LevelFile);

                AddEntities(GetAllTiles(LevelFile));
            }
        }

        /// <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>
        /// Making sure our mapfiles are not tampered with by calculating the checksum
        /// Source: https://stackoverflow.com/questions/10520048/calculate-md5-checksum-for-a-file
        /// Source: https://en.wikipedia.org/wiki/MD5
        /// </summary>
        /// <param name="filename"></param>
        public string CheckMD5(string filename)
        {
            // "using": automatically disposes the object after use,
            //  even if exception is casted
            using (var md5 = MD5.Create())
            {
                using (var stream = File.OpenRead(filename))
                {
                    var hash = md5.ComputeHash(stream);
                    return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
                }
            }
        }

        /// <summary>
        ///  Checks for invalid files and invalid filecontent
        /// </summary>
        public bool ValidMapCheck(string filename) {

            var hash = "";
            var hash32bit = "";
            Console.WriteLine("Hash: " + CheckMD5(Path.Combine(GetPath("Levels"), filename)));

            // Checks for invalid files
            if (filename != "the-beach.txt" && filename != "short-n-sweet.txt") {
                throw new ArgumentException($"Parser cannot load: {filename}");
            }

            // Checks for invalid file content
            if (filename == "the-beach.txt") {
                hash = "81b89b1908e3b3b7fcd7526810c32f14";
                hash32bit = "34d0e9c5ea54bfc60a0365f28b7d3a19";
            }
            if (filename == "short-n-sweet.txt") {
                hash = "e97f28bfff174f9643c088814377ada6";
                hash32bit = "5c4832a9a5510bdab5976ad0e6905e85";

            }

            var checksum = CheckMD5(Path.Combine(GetPath("Levels"), filename));
            if (checksum == hash || checksum == hash32bit) {
                return true;
            }
            throw new ArgumentException($"You've tampered with {filename}!");
            // Todo: Find out how to hide these hash strings and to avoid inline hardcode
        }

        /// <summary>
        ///  Collects a string[] containing all strings in the file
        /// </summary>
        private string[] ReadFile(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> GetAllTiles(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>
        ///    Adds all objects to the appropriate map being loaded
        ///    We iterate from top-left (0,1) of the screen to bottom-right (1,0)
        /// </summary>
        private void AddEntities(List<char> map) {
            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 {
                        // Adds obstacles
                        if (Obstacles.ContainsKey(map[index])) {
                            var shape = new StationaryShape(new Vec2F(tmpX, tmpY),
                                new Vec2F(Constants.WIDTH, Constants.HEIGHT));
                            var file = Path.Combine(GetPath("Assets"),
                                "Images", Obstacles[map[index]]);
                            ValidatePath(file);
                            ObstacleEntities.Add(new Entity(shape, new Image(file)));
                        }

                        // Adds platforms
                        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);
                            PlatformEntities.Add(new Platform(shape, new Image(file), map[index]));
                        }

                        // Adds exits
                        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);
                            ExitEntities.Add(new Entity(shape, new Image(file)));
                        }

                        // Update index
                        index += 1;
                    }

                    tmpX += Constants.WIDTH;
                }

                tmpX = 0;
                tmpY -= Constants.HEIGHT;
            }
        }
    }
}

ParseCustomer.cs:

public partial class LevelParser {
    public Dictionary<string, Tuple<int, char, string, int, int, Entity>>
        GetCustomerInfo(string[] txtFile) {

        // Creates a dictionary containing all info about the customer
        var retDict = new Dictionary<string, Tuple<int, char, string, int, int, Entity>>();

        // Iterates over the entire txtFile and finds certain string
        IEnumerable<string> findName = txtFile.Where(l => l.StartsWith("Customer: "));
        foreach (var line in findName)
        {
            // Parses file
            var name = line.Split(' ')[1];
            var timeBeforeSpawn = Convert.ToInt32(line.Split(' ')[2]); // seconds
            var spawnOnPlatform = Convert.ToChar(line.Split(' ')[3]);
            var destinationPlat = line.Split(' ')[4]; // string
            var patienceTime = Convert.ToInt32(line.Split(' ')[5]); // seconds
            var rewardPoints = Convert.ToInt32(line.Split(' ')[6]);

            // Adds an entity
            var shape = new DynamicShape(new Vec2F(), new Vec2F());
            var image = new Image(Path.Combine("Assets", "Images", "CustomerStandLeft.png"));
            var entity = new Entity(shape, image);

            retDict.Add(name, new Tuple<int, char, string, int, int, Entity>(timeBeforeSpawn,
                spawnOnPlatform, destinationPlat, patienceTime, rewardPoints, entity));

        }

        // Now we have everything we need in one dictionary
        return retDict;
    }
}

There are 3 more files each containing the same partial class but has a method which does something different than the others.

4 Answers

A Partial classes role in this world is for code generators and Designers. Also as a developer you can take advantage of extending classes without messing with generated code.

However, their uses beyond this is limited and fairly suspect.

As for design patterns, design pattern in general is a reusable solution to a commonly occurring problem within a given context in software design. Are partial classes design patterns? hrmm, well not really, just like extension methods in C# are not.

All they are is a bit of syntactic sugar that are compiled to the one class anyway.

For example

public partial class Test
{
    public int testing1 {get;set;}
}

public partial class Test
{    
    public int testing2 {get;set;}
}

Gets generated as this

public class Test
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <testing1>k__BackingField;

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <testing2>k__BackingField;

    public int testing1
    {
        [CompilerGenerated]
        get
        {
            return <testing1>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <testing1>k__BackingField = value;
        }
    }

    public int testing2
    {
        [CompilerGenerated]
        get
        {
            return <testing2>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <testing2>k__BackingField = value;
        }
    }
}

partial class'es are simply a way to split the definition of a class across multiple files.

It is not part of a design pattern since it has no actual effect on the functionality of the class compared to a "regular" class defined in a single file.

Could you say splitting a class into partial classes is a way to encapsulate your code?

Encapsulation refers to a mechanism of bundling the data with the methods (or other functions) operating on that data and thereby restricting direct access to some of the object's components.

whereas,

Partial classes provide a special ability to implement the functionality of a single class into multiple files and all these files are combined into a single class file when the application is compiled.

Partial classes therefore, take care of the physical distribution of code across files while encapsulation takes care of logical grouping and classification of data and behaviours meant to deal with that data.

why is splitting a code into partial classes not a part of a design pattern?

Design patterns aim to provide reusable solution to a common recurring software design problem. These patterns work at a logical level to provide solutions for the creation of an object, definition of its structure or establishment of its behavior with other entities which conform to SOLID principles.

When we are splitting a class into multiple files, we are not doing any logical change to the class that affects its relationship with any other class. Hence partial classes are not considered as design patterns.

A partial class is still a single class but spread over multiple files. The user of a partial class doesn't realize the fact that this class is partial. Encapsulation on the other hand would introduce more classes instead. Encapsulation is a structural design principle on design level and partial classes a compiler or linker concept on compiler level.

A pattern must have an impact on the design of the code's structure, behavior or architecture to be considered a design pattern. Commonly design patterns are categorized in Creational, Structural, Architectural and Behavioral Design Patterns. The concept of partial classes doesn't fall into any of those categories. The partial class concept depends on a compiler or linker to know this concept.

What makes a pattern a pattern? A pattern is a template of how to do things or how to accomplish the same goal (solve the same problem) in different scenarios. A pattern is independent from the compiler platform since it is a general solution that is language independent.

Partial classes where introduced to the C# language to provide a way to separate UI markup code (XAML) from the corresponding code-behind (C#). The compiler then would merge the two files. In the first step the XAML code is converted to C# by the XAML interpreter. Then the two partial classes are merged before translated to IL.

Related