Pull to refresh

Объектно-ориентированная разработка инсталлятора Gin

Reading time 11 min
Views 540
Ссылка на первую часть
Ссылка на вторую часть

Контентные и контейнерные команды


Некоторые команды подразумевают работу с файлами, изначально хранимыми на компьютере разработчика пакета. Понятно, что эти файлы нужно вместе с пакетом (а желательно, прямо внутри пакета) доставить к потребителю пакета. Попробуем для начала представить себе как это будет работать.
У нас есть экземпляр класса PackageBuilder, которому при конструировании мы указываем аргумент PackageBody, содержащий в себе, помимо всего прочего, команду Command, которая представляет собой корневой узел дерева команд пакета. Метод SaveResult() экземпляра класса PackageBuilder должен рекурсивно обойти все дерево, и для тех команд, которые используют контентные файлы, расположенные на компьютере разработчика, включить в тело пакета содержимое всех этих файлов. В тело пакета он также должен включить xml-файл, в который будет сериализован сам PackageBody с полным описанием пакета и выполняемых им команд.

Каким образом метод SaveResult() при рекурсивном обходе дерева команд может узнать, содержит ли текущая команда файлы, которые необходимо включить в пакет? Если мы сделаем это простой проверкой типа такого псевдокода: «Если текущая команда есть CreateFile, то извлекаем из нее файл SourceFile», то в результате мы получим то, что разработчик расширений для инсталлятора, не имея доступа к исходному коду класса PackageBuilder, уже не сможет добавить сюда еще одну проверку для своей новой контентной команды. Здесь нас могло бы выручить создание еще одного абстрактного класса вроде ContentCommand, от которого мы бы и наследовались, однако непонятно куда в иерархии команд мы бы встроили этот класс? На текущий момент команда CreateFile у меня наследуется от TransactionalCommand, и CreateFile подразумевает то что она еще и контентная команда. Но WriteRegistry тоже наследуется от TransactionalCommand, однако она не может быть контентной, потому что просто пишет значение в реестр по определенному ключу. А вот, кстати, команда WriteRegistryFileCommand, тоже является TransactionalCommand, и при этом она еще и контентная, потому что требует для своего выполнения *.reg – файл, описывающий все изменяемые ключи реестра.

Таким образом точно не ясно, в какое место иерархии можно встроить класс ContentCommand, а так как в C# нельзя использовать классы-примеси, как в C++ (источник), то мы решим этот вопрос введением нового интерфейса IContentCommand, который будем реализовывать в каждой контентной команде. К счастью, каждый конкретный класс может реализовывать любое количество интерфейсов, что нам на руку.

Интерфейс IContentCommand в первом приближении такой:
public interface IContentCommand
{
    string ContentPath { get; set; }
}

Обратите внимание что мы предоставляем и геттер и сеттер для единственного элемента интерфейса. Сеттер нам нужен чтобы заменять исходное значение полного пути к файлу на машине разработчика пакета на короткое имя файла внутри готового пакета. Таким образом, на машине потребителя пакета, команда будет извлекать контентные файлы из пакета по их короткому имени внутри пакета. И это логично, потребитель не имеет доступа к машине разработчика, а значит ему необязательно знать полные пути к исходным файлам, да и вообще структуру файловой системы на машине разработчика.

Реализация этого интерфейса для каждой контентной команды проста и очевидна. Например, для команды CreateFile она будет такой:

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


Аналогично она будет выглядеть и для любой другой контентной команды.

Единственный недостаток интерфейса IContentCommand состоит в том, что он оперирует одним единственным файловым путем, то есть с одним файлом или одной папкой. А что делать, если команда содержит в себе указатели на два и более файлов или папок? Тогда интерфейс будет выглядеть так:

interface IMultipleContentCommand
{
    IEnumerable<ContentPath> ContentPaths { get; }
}

Теперь вместо простой строки мне пришлось возвращать коллекцию из объектов типа ContentPath, которые поддерживают запись и чтение в заданное свойство объекта – владельца. Никаких других вариантов, кроме использования рефлексии типов мне придумать не удалось, а потому тип ContentPath я реализовал следующим образом:
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);
        }
    }
}


Здесь я лишь показал возможность создания мульти-контентных команд, однако пока не могу представить себе ни одной команды, требующей такого функционала (команды, оперирующие с папками я собираюсь отнести к обычным контентным командам, оперирующих с одним единственным путем). А поэтому интерфейс IMultipleContentCommand пока останется лишь в черновиках, решение об его использовании я собираюсь принять позже, не усложняя без необходимости интерфейсы классов приложения.

Как мы помним, для генерации файла инсталляционного пакета у нас используется экземпляр класса PackageBuilder. До сих пор он отвечал только за генерацию XML-файла, содержащего тело пакета с включенным в него деревом команд. После того, как мы осознали необходимость хранения в пакете не только XML-файла, но и остальных контентных файлов, нам необходимо пересмотреть алгоритм работы класса PackageBuilder. Теперь он отвечает за следующие вещи:
  1. Генерация XML-файла, содержащего команды пакета
  2. Включение в пакет всех контентных файлов, изначально хранящихся на машине разработчика пакета
  3. Объединения массива файлов в один архивный файл.

Чтобы не закладываться в своей реализации сразу на конкретный тип архива (zip, rar, tar или что-то свое), я инкапсулирую это поведение в отдельный класс PackageContent.
image
Как видим, теперь PackageBuilder ничего не знает о методах формирования результирующего пакета, то есть о том, как именно из многих разрозненных файлов создается один. Также нужно будет сделать так, чтобы класс Package во время выполнения пакета также не опирался на эти знания, а использовал открытые методы класса PackageBuilder. Таким образом мы обеспечиваем полную инкапсуляцию упаковки-распаковки пакетов, и не оставляем классам-пользователям никаких шансов узнать о внутренней реализации этого алгоритма. Пусть у класса PackageContent будет два конструктора: первый создает пустой пакет для нужд PackageBuilder-а, второй конструктор – получает на входе путь к файлу пакета, и создает уже заполненный контентом экземпляр для нужд экземпляра Package, и его метода Execute().
image
В моем случае я предлагаю воспользоваться tar-архивированием, как наиболее простым методом упаковки файлов. Я не собираюсь углубляться в детали этого метода, так как смысл статьи – не в этих деталях.

Замечу следующее. При формировании пакета возможен такой вариант, что один и тот же контентный файл используется в нескольких различных командах. Чтобы не сохранять его одинаковые копии внутри пакета, необходимо чтобы PackageContent хранил в себе список всех загруженных в него файлов, и мог всегда ответить на вопрос – а есть ли уже внутри меня данный файл? Проверка эта будет производиться по схожести абсолютного пути к файлу. И еще одно замечание. Также возможна ситуация, когда внутрь пакета добавляются два разных файла из разных папок, но с одинаковыми именами. Так как внутри пакета вряд ли будет существовать (а точнее, может существовать, а может и нет) файловая иерархия, то следует внутри пакета файлы сохранять под вновь сгенерированными именами, желательно генерировать их при помощи класса Guid, чтоб уж наверняка имена файлов внутри пакета не совпали. Соответственно, после переименования файла и сохранения его внутри пакета под новым именем, нужно изменить исходный путь в соответствующей команде на новый относительный путь.

Для того, чтобы PackageBuilder мог разобраться, имеет ли текущая команда вложенные в нее команды, сделаем так, чтобы все команды (назовем их контейнерными), содержащие в себе другие команды, могли сообщить об этом вызывающему коду. Для этого они будут реализовать интерфейс IContainerCommand, вот его описание:

public interface IContainerCommand
{
    IEnumerable<Command> InnerCommands { get; }
}


Реализация этого интерфейса для контейнерных команд будет тривиальна. Например, CommandSequence просто вернет свое свойство Commands:

public IEnumerable<Command> InnerCommands
{
    get
    {
        return Commands;
    }
}


А ExecuteIf вернет список, состоящий из одной команды Command:

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


Приведу теперь код методов SaveResult и ProcessIncludedFiles для точного понимания того, как все это функционирует:

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;
}


Чуть выше я уже описывал подробно работу этого кода.

Операторы сравнения


Операторы сравнения строк я рассматривал в первой части. Здесь я попытаюсь реализовать операторы сравнения для числовых данных. Как мы помним, команда ExecuteIf требует для своего выполнения наличие в контексте исполнения (ExecutionContext) булевой переменной, представляющей собой результат сравнения двух операндов. Операнды эти могут иметь различные типы (int, double, и т.д.). Сам оператор сравнения в свою очередь тоже может быть различным(LessThan, GreaterThan, Equal, e.t.c), причем, как можно заметить, к каждому конкретному типу операндов может быть применим не весь набор операторов, например оператор StartsWith вряд ли стоит применять к целым числам. Как видно, создание команды, реализующей сравнение операндов, является весьма нетривиальной задачей, особенно если учесть наше желание спроектировать команду так, чтобы конечный пользователь-программист, смог расширить базовый набор операторов сравнения своими собственными операторами.

Первый, самый очевидный, подход к реализации операторов сравнения выглядит как команда с аргументами:

string FirstOperandName,
string SecondOperandName,
CompareOperation Operation.

Для целых чисел выглядит это так:

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);
    }
}


Здесь я описал только два оператора сравнения причем только для целых чисел. Соответственно, если мы захотим сравнивать десятичные (decimal) числа, мы добавим класс DecimalCompareCommand наследующий от Command, почти полностью повторяющий класс IntCompareCommand, за исключением приведения к типу (decimal). А если мы захотим добавить к ним еще и оператор LessThan, то нам уже придется вносить изменения в два класса IntCompareCommand, DecimalCompareCommand в оператор switch метода Do().

Что мне не нравится в этом подходе? Огромное количество повторяющихся шаблонов — код каждого класса вообще практически идентичен. Невозможность добавления сторонним разработчиком новых операторов сравнения в перечисление CompareOperation.

Думаю, что стоит оператор switch заменить на использование полиморфизма. Попробуем воплотить это в жизнь.
Так как в нашей задаче могут со временем измениться два аспекта – тип операндов и тип операции, то следует инкапсулировать каждый из этих аспектов в отдельный абстрактный класс. Пусть они будут называться CompareCommand и 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);
    }
}


В этом классе есть:
  1. два аргумента, заданных своими именами в контексте исполнения,
  2. метод Do(ExecutionContext), который производит сравнение путем нахождения знака разности двух величин (метод Subtract), и передачи этого знака абстрактному методу Compare(int). Метод Compare мы реализуем в конкретных наследниках класса CompareCommand.
  3. Абстрактный метод Compare(int)
  4. Конкретный метод Subtract, который извлекает из контекста два операнда и сохраняет их в экземплярах наследников абстрактного класса CompareOperand, а затем производит вычитание одного из другого. Понятно, что там тоже будет использоваться полиморфизм.


Наследники будут выглядеть так:

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


Опишем теперь класс CompareOperand:

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;
    }
}


Как видим, статический метод Create в соответствии с типом переданного аргумента создает экземпляр одного из наследников класса CompareOperand. Все его наследники будут отличаться лишь типом свойства Value и реализацией оператора вычитания.

А его наследники будут такими:

public class LongCompareOperand : CompareOperand
{
    public long Value { get; set; }

    public static int operator -(LongCompareOperand operand1, LongCompareOperand operand2)
    {
        return Math.Sign(operand1.Value - operand2.Value);
    }
}


Схема выглядит красиво за исключением того, что оператор вычитания реализуется в виде статического метода, а значит и полиморфизм работать в данном случае не будет (статический метод не может быть виртуальным, а значит и полиморфизм в его отношении не сработает). Об этом факте нам тут же и заявила VisualStudio после запуска тестового примера – вызов оператора вычитания вызвал Stack Overflow, так как вычитание реализованное в CompareOperand вызывало само себя, а не полиморфичный оператор вычитания базового класса. Что ж, сделаем вычитание не оператором «-», а методом Subtract().

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);
    }
}


Единственное, что мне не нравится в этом подходе – цепочка операторов if else в методе CompareOperand.Create(), но я пока не знаю, как можно исправить ситуацию. Теперь, если возникнет необходимость добавить поддержку операндов для другого типа, то нужно будет реализовать наследника от класса CompareOperand, и добавить в метод Create() еще один If else, что является невозможным для стороннего разработчика, не имеющего доступа к исходному коду. Также выпадает исключение при попытке использовать в рамках одного сравнения два разных типа операндов.

Универсального, гибкого решения мне пока придумать не удалось, и вспомнив поговорку, что лучшее – враг хорошего, я решил оставить все как есть. Таким образом, мы имеем набор арифметических операторов сравнения, основанных на разности между числами, для всех числовых типов, поддерживаемых в CLR. Думаю, что его также можно дополнить и не числовыми типами, поддерживающими операцию разности, такими как DateTime.
Надеюсь, что присутствующие в блоге специалисты предложат наиболее оптимальный метод решения поставленной задачи сравнения различных типов числовых данных.

Ссылка на четвертую часть
Tags:
Hubs:
+2
Comments 1
Comments Comments 1

Articles