Object Oriented Gin Installer Development

    Link to the first part
    Link to the second part

    Content and container teams


    Some commands involve working with files originally stored on the package developer’s computer. It is clear that these files must be delivered together with the package (and preferably, right inside the package) to the consumer of the package. First, let’s try to imagine how it will work.
    We have an instance of the PackageBuilder class, to which we designate the PackageBody argument, which contains, among other things, the Command command, which is the root node of the package command tree. The SaveResult () method of an instance of the PackageBuilder class must recursively traverse the entire tree, and for those commands that use content files located on the developer's computer, include the contents of all these files in the package body. It should also include an xml file in the package body, into which PackageBody itself will be serialized with a full description of the package and the commands it executes.

    How can the SaveResult () method, when recursively traversing the command tree, find out if the current command contains files that need to be included in the package? If we make this a simple check of the type of such pseudo-code: “If the current command is CreateFile, then we extract the SourceFile from it”, then as a result we get that the developer of extensions for the installer, having no access to the source code of the PackageBuilder class, can no longer add another check here for your new content team. Here, we could be helped out by the creation of another abstract class like ContentCommand, from which we would inherit, but it is not clear where we would build this class in the command hierarchy? Currently, the CreateFile command is inherited from TransactionalCommand, and CreateFile implies that it is also a content command. But WriteRegistry also inherits from TransactionalCommand, however, it cannot be content because it simply writes the value to the registry using a specific key. And here, by the way, the WriteRegistryFileCommand command is also a TransactionalCommand, and at the same time it is also content because it requires * .reg to be executed - a file that describes all the registry keys to be changed.

    Thus, it is not clear exactly where the hierarchy of the ContentCommand class can be embedded, and since C # cannot use impurity classes, as in C ++ (source), we will solve this issue by introducing a new IContentCommand interface, which we will implement in each content command . Fortunately, each particular class can implement any number of interfaces, which is at hand for us.

    The IContentCommand interface is as a first approximation:
    public interface IContentCommand
    {
        string ContentPath { get; set; }
    }
    

    Please note that we provide both getter and setter for a single interface element. We need a setter to replace the original value of the full path to the file on the package developer machine with the short file name inside the finished package. Thus, on the consumer machine of the package, the team will extract the content files from the package by their short name inside the package. And this is logical, the consumer does not have access to the developer's machine, which means that he does not need to know the full paths to the source files, and indeed the structure of the file system on the developer's machine.

    The implementation of this interface for each content team is simple and obvious. For example, for the CreateFile command, it will be like this:

    public string ContentPath
    {
        get
        {
            return SourcePath;
        }
        set
        {
            SourcePath = value;
        }
    }
    


    It will look the same for any other content team.

    The only drawback of the IContentCommand interface is that it operates with one single file path, that is, with one file or one folder. But what if the command contains pointers to two or more files or folders? Then the interface will look like this:

    interface IMultipleContentCommand
    {
        IEnumerable ContentPaths { get; }
    }
    

    Now, instead of a simple string, I had to return a collection of ContentPath objects that support writing and reading to the specified property of the owner object. I could not come up with any other options besides using type reflection, and therefore I implemented the ContentPath type as follows:
    public class ContentPath
    {
        private Command _command;
        private PropertyInfo _property;
        public ContentPath(Command command, PropertyInfo property)
        {
            if (!(command is IMultipleContentCommand))
            {
                throw new ArgumentException("Must be the IMultipleContentCommand");
            }
            _command = command;
            _property = property;
        }
        public string Name
        {
            get
            {
                return _property.Name;
            }
        }
        public string Value
        {
            get
            {
                return (string)_property.GetValue(_command, null);
            }
            set
            {
                _property.SetValue(_command, value, null);
            }
        }
    }
    


    Here I just showed the possibility of creating multi-content teams, but so far I can’t imagine a single team that requires such functionality (teams that operate with folders I’m going to refer to ordinary content teams that operate with one single path). And therefore, the IMultipleContentCommand interface will remain only in draft copies, I’m going to make a decision about its use later, without complicating the interfaces of the application classes without the need.

    As we recall, we use an instance of the PackageBuilder class to generate the installation package file. Until now, he was only responsible for generating an XML file containing the package body with the command tree included in it. After we realized the need to store in the package not only the XML file, but also the rest of the content files, we need to revise the algorithm of the PackageBuilder class. Now he is responsible for the following things:
    1. Generating an XML file containing package commands
    2. Inclusion in the package of all content files originally stored on the machine of the package developer
    3. Combining an array of files into a single archive file.

    In order not to lay directly on a particular type of archive (zip, rar, tar or something else) in my implementation, I encapsulate this behavior in a separate PackageContent class.
    image
    As you can see, now PackageBuilder does not know anything about the methods of generating the resulting package, that is, about how exactly one of the many disparate files is created. You will also need to make sure that the Package class does not rely on this knowledge during package execution, but uses the public methods of the PackageBuilder class. Thus, we provide full encapsulation of package-unpacking packages, and do not leave user classes any chance to learn about the internal implementation of this algorithm. Let the PackageContent class have two constructors: the first creates an empty package for the needs of the PackageBuilder, the second constructor receives the path to the package file at the input, and creates an instance already filled with content for the needs of the Package instance and its Execute () method.
    image
    In my case, I suggest using tar archiving as the easiest method of packing files. I am not going to delve into the details of this method, since the meaning of the article is not in these details.

    I note the following. When forming a package, it is possible that the same content file is used in several different commands. In order not to keep its identical copies inside the package, it is necessary for PackageContent to keep a list of all the files loaded into it, and could always answer the question - is there this file already inside me? This check will be made by the similarity of the absolute path to the file. And one more remark. It is also possible that two different files from different folders are added to the package, but with the same name. Since the file hierarchy is unlikely to exist (or rather, may exist, or may not), files should be saved inside the package under newly generated names, it is advisable to generate them using the Guid class, so that for sure the file names inside the package do not match. Accordingly, after renaming the file and saving it inside the package under a new name, you need to change the original path in the corresponding command to a new relative path.

    So that PackageBuilder can figure out if the current command has commands embedded in it, we will make it so that all commands (let's call them container) containing other commands can inform the calling code about this. To do this, they will implement the IContainerCommand interface, here is its description:

    public interface IContainerCommand
    {
        IEnumerable InnerCommands { get; }
    }
    


    Implementing this interface for container commands will be trivial. For example, CommandSequence will simply return its Commands property:

    public IEnumerable InnerCommands
    {
        get
        {
            return Commands;
        }
    }
    


    And ExecuteIf will return a list consisting of one Command command:

    public IEnumerable InnerCommands
    {
        get
        {
            return new List() 
            {
                Command
            };
        }
    }
    


    I will now give the code of the SaveResult and ProcessIncludedFiles methods for an accurate understanding of how all this works:

    public const string MAIN_PACKAGE_FILENAME = "package.xml";
    public const string PACKAGE_FILE_EXTENSION = ".gin";
    public const string PACKAGE_CONTENT_FILE_EXTENSION = ".cnt";
    public void SaveResult(string filePath)
    {
        ProcessIncludedFiles(_body.Command);
        string xmlFilePath = GinSerializers.PackageBodySerializer.Serialize(_body);
        _content.AddContent(xmlFilePath, MAIN_PACKAGE_FILENAME);
        _content.SaveAs(filePath);
    }
    private void ProcessIncludedFiles(Command command)
    {
        if (command is IContainerCommand)
        {
            IContainerCommand iContainer = (IContainerCommand)command;
            foreach (Command cmd in iContainer.InnerCommands)
            {
                ProcessIncludedFiles(cmd);
            }
        }
        if (command is IContentCommand)
        {
            IContentCommand cntCommand = (IContentCommand)command;
            string sourceFilePath = cntCommand.ContentPath;
            if (!_content.ContainFilePath(sourceFilePath))
            {
                string destFileName = GetGuidContentFileName();
                _content.AddContent(sourceFilePath, destFileName);
                cntCommand.ContentPath = destFileName;
            }
            else
            {
                string destFileName = _content.GetFileName(sourceFilePath);
                cntCommand.ContentPath = destFileName;
            }
        }
    }
    private string GetGuidContentFileName()
    {
        return Guid.NewGuid().ToString("N") + PACKAGE_CONTENT_FILE_EXTENSION;
    }
    


    Just above, I already described in detail the operation of this code.

    Comparison operators


    The string comparison operators I examined in the first part. Here I will try to implement comparison operators for numeric data. As we recall, the ExecuteIf command requires a boolean variable in the execution context (ExecutionContext), which is the result of comparing two operands. These operands can have various types (int, double, etc.). The comparison operator itself, in turn, can also be different (LessThan, GreaterThan, Equal, etc), and, as you can see, not every set of operators can be applicable to each specific type of operands, for example, the StartsWith operator is hardly worth applying to integers. As you can see, the creation of a team that implements the comparison of operands is a very non-trivial task, especially if we take into account our desire to design the team in such a way

    The first, most obvious, approach to implementing comparison operators looks like a command with arguments:

    string FirstOperandName,
    string SecondOperandName,
    CompareOperation Operation.

    For integers, it looks like this:

    public enum CompareOperation
    {
        Equals,
        GreaterThan
    }
    public class StringCompareCommand : Command
    {
        public string FirstOperandName { get; set; }
        public string SecondOperandName { get; set; }
        public CompareOperation Operation { get; set; }
        private ExecutionContext _context;
        public StringCompareCommand(ExecutionContext context)
        {
            _context = context;
        }
        public override void Do()
        {
            bool compareResult = false;
            int firstOperand = (int)_context.GetResult(FirstOperandName);
            int secondOperand = (int)_context.GetResult(SecondOperandName);
            switch (Operation)
            {
                case CompareOperation.Equals:
                    compareResult = firstOperand == secondOperand;
                    break;
                case CompareOperation.GreaterThan:
                    compareResult = firstOperand < secondOperand;
                    break;
            }
            _context.SaveResult(ResultName, compareResult);
        }
    }
    


    Here I have described only two comparison operators, and only for integers. Accordingly, if we want to compare decimal numbers, we will add the DecimalCompareCommand class inheriting from Command, which will almost completely repeat the IntCompareCommand class, except for casting to the (decimal) type. And if we want to add the LessThan operator to them, then we will have to make changes to the two classes IntCompareCommand, DecimalCompareCommand in the switch statement of the Do () method.

    What do I dislike about this approach? A huge number of repeating patterns - the code of each class is generally almost identical. Inability by a third-party developer to add new comparison operators to the CompareOperation enumeration.

    I think it’s worth replacing the switch statement with the use of polymorphism. Let's try to bring it to life.
    Since in our problem two aspects can change over time - the type of operands and the type of operation, each of these aspects should be encapsulated in a separate abstract class. Let them be called CompareCommand and CompareOperand.

    public abstract class CompareCommand: Command
    {
        public string FirstOperandName { get; set; }
        public string SecondOperandName { get; set; }
        public override void Do(ExecutionContext context)
        {
            bool result = Compare(Subtract(context));
            context.SaveResult(ResultName, result);
        }
        protected abstract bool Compare(int compareResult);
        private int Subtract(ExecutionContext context)
        {
            CompareOperand firstOperand = CompareOperand.Create(context.GetResult(FirstOperandName));
            CompareOperand secondOperand = CompareOperand.Create(context.GetResult(SecondOperandName));
            return (firstOperand - secondOperand);
        }
    }
    


    In this class there are:
    1. two arguments given by their names in the context of execution,
    2. Do (ExecutionContext) method, which compares by finding the sign of the difference between the two quantities (Subtract method), and passing this sign to the abstract Compare (int) method. We implement the Compare method in specific descendants of the CompareCommand class.
    3. Abstract Compare (int) method
    4. A specific Subtract method that extracts two operands from the context and stores them in instances of the descendants of the abstract class CompareOperand, and then subtracts one from the other. It is clear that polymorphism will also be used there.


    The heirs will look like this:

    public class CompareLessThan : CompareCommand
    {
        protected override bool Compare(int compareResult)
        {
            return compareResult < 0;
        }
    }
    


    We now describe the CompareOperand class:

    public abstract class CompareOperand
    {
        public static CompareOperand Create(object operand) 
        {
            if (operand is ulong)
            {
                return new ULongCompareOperand()
                {
                    Value = (ulong)operand
                };
            }
    .. .. ..
    .. .. ..
    .. .. ..
            return new DefaultCompareOperand()
            {
                Value = (DefaultType)operand
            };
        }
        public static int operator -(CompareOperand operand1, CompareOperand operand2)
        {
            return operand1 - operand2;
        }
    }
    


    As you can see, the static Create method, in accordance with the type of the argument passed, creates an instance of one of the descendants of the CompareOperand class. All its heirs will differ only in the type of the Value property and in the implementation of the subtraction operator.

    And his heirs will be like this:

    public class LongCompareOperand : CompareOperand
    {
        public long Value { get; set; }
        public static int operator -(LongCompareOperand operand1, LongCompareOperand operand2)
        {
            return Math.Sign(operand1.Value - operand2.Value);
        }
    }
    


    The scheme looks beautiful except that the subtraction operator is implemented in the form of a static method, which means that polymorphism will not work in this case (the static method cannot be virtual, which means that polymorphism will not work in its respect). VisualStudio immediately told us about this fact after running a test case - calling the subtraction operator caused Stack Overflow, since the subtraction implemented in CompareOperand called itself, and not the polymorphic subtraction operator of the base class. Well, we do the subtraction not with the “-” operator, but with the Subtract () method.

    public abstract class CompareOperand
    {
        public static CompareOperand Create(object operand) 
        {
            return new DefaultCompareOperand()
            {
                Value = (DefaultType)operand
            };
        }
        public abstract int Subtract(CompareOperand operand2);
        public static int operator -(CompareOperand operand1, CompareOperand operand2)
        {
            return operand1.Subtract(operand2);
        }
    }
    public class LongCompareOperand : CompareOperand
    {
        public long Value { get; set; }
        public override int Subtract(CompareOperand operand2)
        {
            return Math.Sign(this.Value - ((LongCompareOperand)operand2).Value);
        }
    }
    


    The only thing I don’t like about this approach is the chain of if else statements in the CompareOperand.Create () method, but I don’t know yet how to fix the situation. Now, if it becomes necessary to add support for operands for another type, you will need to implement the descendant of the CompareOperand class, and add another If else to the Create () method, which is impossible for a third-party developer who does not have access to the source code. An exception is also thrown when trying to use two different types of operands within the framework of one comparison.

    I have so far failed to come up with a universal, flexible solution, and remembering the saying that the best is the enemy of the good, I decided to leave it as it is. Thus, we have a set of arithmetic comparison operators based on the difference between numbers for all numeric types supported in the CLR. I think that it can also be supplemented with non-numeric types that support the difference operation, such as DateTime.
    I hope that the specialists present on the blog will offer the most optimal method for solving the problem of comparing various types of numerical data.

    Link to the fourth part

    Also popular now: