📜 ⬆️ ⬇️

Object-Oriented Gin Installer Development

Introduction


The cycle of articles offered to your attention has several main objectives:
  1. Create useful software - installer programs and updates.
  2. Show the benefits of an object-oriented approach to software development and teach how to create easily extensible software architectures.

In this series of articles, I want to share the story of creating software that allows you to install and update company software using packages. The need to create your own installer (with the rejection of the use of ready-made solutions) is caused by the specificity of the requirements for the installer. I will not delve into the rationale for the development, since the topic of the article cycle is different.
The main requirements for the architecture being developed will be:
  1. The implementation of the transaction mechanism, and transactions can include not only SQL transactions, but also file transactions, as well as transactions related to changing any other OS resources, such as registry entries, changes to configuration files, etc.
  2. Extensibility of the operating base of the installer, that is, the addition of new types of commands (operations), both with and without transaction support.


So, each package as a first approximation should contain:
  1. Description of the sequence of commands executed.
  2. An arbitrary set of data files.

I intend to save the description of the command sequence in the XML file for the reason that the .NET platform contains convenient and simple classes for serializing / deserializing objects into XML files, which will enable us to focus on the main logic of the application, without delving into the operation of in rows.
The package itself will be in the format of a TAR archive, possibly even subjected to GZIP compression, however, I will try to design the software so that when making a decision to change the way the package is stored (its structure and compression method), we would need minimal interference with the existing code.
Let us estimate the command set of the future software.
  1. File operations
  2. SQL operations
  3. IIS server management
  4. Perhaps something else ...

As I mentioned, the set of commands can be expanded. Moreover, we will make so that the main functionality - the core of the software - is stored in a separate assembly, and the possible executable commands are configured by separate assemblies connected to the main module using the concept of plug-ins.
It already takes my breath away from how many interesting tasks will need to be solved in the software development process. I’ll make a reservation right away that at the moment when I am writing these lines there is already a working prototype of the application, which truth does not yet have all the declared functionality, and making changes to it is a bit more complicated than I would like. So, I still will refactor it. He already has the ability to create transactions for file operations, but does not know how to combine file and SQL operations within a single transaction. It has not yet implemented plugin support. Adding a new command to the command set makes it necessary to edit 4 files, and I want only a new class to be enough for this. In general, the work - plowed field.
As a first approximation, the class diagram of the system looks like this:
image
It has a parent class of all operations (commands) that an application can perform, and several commands inherited from it (CreateFileCommand and DeleteFolderCommand), which in the end will be very much. There is also the PackageBuilder class, which contains a sequence of commands. PackageBuilder can create packages and give them to the user. Packages can save themselves and execute.
As a result, pseudocode using these classes will look like this:
void CreatePackage() { //     PackageBuilder builder = new PackageBuilder(); builder.AddCommand(new CreateFolderCommand() { FolderPath = @"%APPROOT%\files" }); builder.AddCommand(new CreateFileCommand() { SourcePath = @"d:\package\config.xml", //      DestPath = @"%APPROOT%\files\config.xml" //     }); Package package = builder.GetResult(); package.SaveAs(@"d:\package\output\package.pkg"); } void ExecutePackage () { //    Package package = new Package(@"d:\package\output\package.pkg"); package.Execute(); } 

Of course, the structure of classes still has time to undergo many metamorphoses, but with something you need to start.

Basic command system


Let's try to estimate the base system of the installer commands. Here I am not going to list all the installer functionality, because I am going to give users an easy way to extend the functionality.
Here I am going to describe the basic framework commands, allowing to unite the remaining teams into a single whole. The abstract class, from which all commands will be inherited, contains a single Do () method, which itself will perform the action programmed by the command. For example, the DeleteFile command will create a file inside this Do () method, and it will use the public property of the string FilePath as an argument (the path of the file to be deleted). Here is what it will look like:
 public abstract class Command { public abstract void Do(); } public class DeleteFile: Command { public string FilePath { get; set; } public override void Do() { File.Delete(FilePath); } } 

All other commands will be implemented in exactly the same way - the command inherits from the abstract Command class, and its Do () method replaces it, using the public properties of the inherited class as arguments.
So, in the first approximation, I assumed that the teams would be added one by one to PackageBuilder, but I quickly realized the inflexibility of this approach. Suppose that the sequence of installer commands should depend on the version number of the third-party software installed on the target machine. For example, the algorithm for installing a site is very dependent on the version installed on the target IIS machine. Or, for example, the installer should check the presence of the required COM objects on the target machine, and install them if necessary. In all these cases, it is necessary to check some registry values ​​and select further actions depending on the values ​​read from there. From here we came to the need to have a conditional execution command in the basic set, let's call it ExecuteIf. Here is a description of its interface:
 public class ExecuteIf : Command { public string ArgumentName { get; set; } public Command Then { get; set; } public Command Else { get; set; } public void Do(); } 

I mean that the Do () method reads from some execution context a boolean variable named ArgumentName, and if it is True, it executes the Then command, otherwise it executes the Else command. Here we are faced with a new concept - the context of implementation. Let the execution context be an instance of the ExecutionContext class, which (the instance) will be the argument of the call to the Do () method of each command. In order not to inflate the abstract class interface with the overloaded Do (ExecutionContext) method, let's say that the Do () method is always called with the ExecutionContext argument, but those commands that do not change the execution context will simply ignore the Do () method argument.

Thus, now our classes now look like this:
 public abstract class Command { public abstract void Do(ExecutionContext context); } public class DeleteFile: Command { public string FilePath { get; set; } public override void Do(ExecutionContext context) { File.Delete(FilePath); } } public class ExecuteIf : Command { public string ArgumentName { get; set; } public Command Then { get; set; } public Command Else { get; set; } public void Do(ExecutionContext context); } 

')
Let us digress from the description of the basic set of commands, and design the class ExecutionContext.

Execution context


Each team executed within the package, in addition to performing useful work, can also return the results of its execution. For example, an SQL command can return the result of executing a stored procedure or the number of rows changed; reading the registry returns the actual value read. In this case, in the general case, a command can return not one ValueType result, but several, it can also return a complex result, for example, a DataSet. In order for other teams that rely on the results of previous operations to work correctly, you need to provide them with a way to access the results of previous commands. In addition, each specific team can rely not only on the result of the strictly previous command, but in general any previously executed command, including it can rely on the results of the execution of several previous commands. For example, I can create a FindIndexCommand command that defines the index of a certain string (entered by the user) in an array of strings, which was previously returned by the SqlQueryCommand command. As you can see, the result of the FindIndexCommand command in this case rests on two arguments - the DataSet returned by the SqlQueryCommand command, and the string entered by the user using the UserInputCommand command. I use the names of the teams that I haven’t even designed yet, meaning that their purpose is clear from the name of the team.
To give the user such flexibility, I plan to introduce some execution context ExecutionContext. It will give the opportunity to save the value in it under some key, and read the value from it, if it is there. Each executed command will receive it as the first argument to the Do () method. Thus, each team will have access to the results of all previously executed commands.
The ExecutionContext class looks like this:
 public class ExecutionContext { private Dictionary<string, object> _results = new Dictionary<string, object>(); public void SaveResult(string key, object value) { _results[key] = value; } public object GetResult(string key) { return _results[key]; } } 

Perhaps, in the future, other methods will be added to it, but for now it will have the simplest form, as in the listing above.
At first, I was going to use the ExecutionContext like this:
 public class DeleteFile: Command { public string FilePath { get; set; } private ExecutionContext _context; public DeleteFile(ExecutionContext context) { _context = context; } public DeleteFile() { } public override void Do() { } } 


That is, to submit its instance as an argument to the command constructor, save it in a private variable, and then use it as needed in the Do () method. However, I remembered that the command package is usually first serialized and saved, and then the resulting file is deserialized and executed, and at this second step, the deserializer will automatically restore the package in memory, but it does not initialize the _context closed variable (because it is closed), which means and the package will run in an uninitialized context. I thought that the context is needed only at the execution stage of the loaded package, and decided to give context as an argument to the Do () method. This immediately brought another benefit - now we don’t need to write this repeating code in every new team:
 private ExecutionContext _context; public DeleteFile(ExecutionContext context) { _context = context; } public DeleteFile () { } 

After all, now we don’t need constructors with arguments, which means that we don’t need to describe a default constructor, because the compiler will create it for us. Now the command frame looks like this:
 public class DeleteFile: Command { public string FilePath { get; set; } public override void Do(ExecutionContext context) { } } 

That is, there is nothing superfluous in it, only the essence. So a programmer who will support and develop the application, it will become easier to understand its essence.
Now we can write an implementation of the ExecuteIf command:
 public class ExecuteIf : Command { public string ArgumentName { get; set; } public Command Then { get; set; } public Command Else { get; set; } public override void Do(ExecutionContext context) { if ((bool)context.GetResult(ArgumentName)) { Then.Do(context); } else if (Else != null) { Else.Do(context); } } } 


Basic command system.


Let's return to the creation of the installer command frame. We have already described the base abstract class Command, as well as the conditional execution command ExecuteIf. The Then and Else parameters of the ExecuteIf command can be not just one command, but a whole block of consecutive commands. This means that you need to design a command - an analogue of the operator brackets. Let's call this command CommandSequence. Its interface and implementation are simple:
 public class CommandSequence : Command { public List Commands; public override void Do(ExecutionContext context) { foreach (Command command in Commands) { command.Do(context); } } } 

Its only parameter is the list of commands, and the Do () method simply executes them in turn.

Recall now that the ExecuteIf command takes the name of a Boolean variable from the execution context as an input argument. This boolean variable should appear from somewhere else in the context. It should appear there as a result of checking some condition. And so it was the turn to design the command comparison of quantities. Let's design for the beginning of the string comparison command. The command interface and its partial implementation should look like this:
 public class CompareStringsCommand: Command { public string FirstOperandName { get; set; } public string SecondOperandName { get; set; } public string ResultName { get; set; } public void Do(ExecutionContext context) { string operand1 = (string)(context.GetResult(FirstOperandName)); string operand2 = (string)(context.GetResult(SecondOperandName)); bool result = Compare(operand1, operand2); context.SaveResult(ResultName, result); } } 

The team reads two variables from the context by their FirstOperandName and SecondOperandName names, compares them (I compared the comparison itself with the Compare () method), and then stores the comparison result in the context under the name ResultName.
It can be seen that this command is a good contender for becoming the base abstract class for all other string comparison commands, and the Compare method (string, string) is an excellent candidate for becoming an abstract class method and being implemented in the heirs. Next, I will rewrite only the abstract class interface (the implementation of the Do () method remains the same), and also give the code of one of the heirs:
 public abstract class CompareStringsCommand: Command { public string FirstOperandName { get; set; } public string SecondOperandName { get; set; } public string ResultName { get; set; } protected abstract bool Compare(string operand1, string operand2); public override void Do(ExecutionContext context) { //    ,       } } public class StringStartsWith : CompareStringsCommand { protected override bool Compare(string operand1, string operand2) { return operand1.StartsWith(operand2); } } 

As you can see, the set of commands for binary (with two arguments) comparison operators is easy to expand with any other comparison operations (EndsWith, Contains, Equals, NotEquals) just by creating an inheritor of the CompareStringsCommand class and defining the Compare method (string, string) in it.
Unary operations (such as IsNullOrEmpty, for example) at this stage I propose to implement using the same base class, simply ignoring the second operand.
Pay attention to this. The CompareStringsCommand command compares the two arguments stored in the execution context, which will be there only as the results of the execution of other commands. But what if I want to find out, for example, does a string that is read from the registry begin with some given constant substring? I need to ensure that this constant is present in the execution context. We design a command that simply stores the value in the execution context under the specified name. Everything is simple here:
 public class SaveConstantCommand: Command { public string ResultName { get; set; } public object Value { get; set; } public override void Do(ExecutionContext context) { context.SaveResult(ResultName, Value); } } 

In the next chapter, I will look at the implementation of the transaction mechanism.

Link to the second part

Source: https://habr.com/ru/post/134439/


All Articles