Command Line Arguments

Since this article completes the series about SOLID with examples, I would like to show how these principles allow you to create something more. In this article we will create a small module, a framework, for working with command line arguments.

Processing command line arguments, on the one hand, is one of the simplest examples that novice developers use to solve. On the other hand, there are many solutions that not only do not comply with the SOLID principles, but are also built in such a way that making almost any change requires significant code rework.

Let's start developing the module by defining the goal; it is very trivial, but a correctly set goal greatly simplifies the implementation. The main goal of the module will be to create a user-friendly tool. It's easy to add, change argument values, and iterate over values.

Let's describe the functionality of the module.

  • The module should accept all arguments at once, rather than using each argument separately.

  • Iteration of arguments by pointers must be implemented inside the module, hiding the complex action from the user.

  • The module must store a list of possible arguments.

  • The module should produce a list of actions whose algorithms and arguments were listed in the command line parameters.

At this stage, I would like to make a digression and explain what is logical, but not obvious at first glance. There can be many supported command line arguments, but cases where they are all used at once are very rare. Therefore, the list of actual arguments passed is almost always shorter than the full list.

But when we, as programmers, use arguments, what is important to us are the actions, the algorithms that need to be performed, and not the arguments themselves. From this we can draw a logical conclusion. Even a couple. First, it is necessary to separate the logic for creating the list of possible arguments. Second, the list of passed arguments is not important, it is important to get a list of algorithms that need to be executed.

That is, at the very beginning we create a list of all possible arguments. To each argument we attach an algorithm that needs to be executed, and later, from the module, we get a list of algorithms that were indicated by the values ​​​​substituted when starting the program.

Once we have sorted out the intricacies, we can begin designing. Since our functionality is a separate module, it needs to be given a façade. The same Facade from design patterns. It can also be called the API of our module.

At the first stage it will look like this.

Class diagram

Class diagram

The Arguments class in the constructor accepts arguments from the main function, thereby hiding all actions with them from us.

The add() method will allow you to create the same list of all possible arguments. It’s worth figuring out what parameters the method will have. First of all, the command line argument is a string. However, there are pitfalls here that need to be discussed.

What argument to use. It is customary that the arguments are short “-h” and long “–help”. Take a look, when typing from the command line, it is more convenient to enter this way.

myprogramm.exe -s -i first.csv second.csv -o third.csv -e ivanov

However, when writing scripts, a line containing long arguments looks better.

myprogramm.exe –sort –inside first.csv second.csv –outside third.csv –exclude ivanov

We will adhere to the established rules and introduce them together.

One more parameter that we need when adding is an algorithm that should be associated with this pair of flags. It’s not difficult here either, from the patterns we select the Factory Method, which will allow you to easily create new algorithms for each pair of arguments. We substitute the abstraction itself as a parameter and our method of adding arguments is ready.

Class diagram

Class diagram

I would like to draw your attention to the fact that by substituting abstraction as an argument, we have inverted the dependency; now you can create any number of algorithms without changing the module itself.

The next step is to decide how we will work with the list of algorithms that will be selected based on the command line arguments. In my opinion, the easiest way is to loop through them.

for (auto element : arguments) element->execute();

Implementing this approach is quite simple; you need two methods begin() and end() that return iterators, the Iterator pattern. More precisely, one iterator, positioned at the beginning and end of the list.

Class diagram

Class diagram

Please note that we did not perform dependency inversion for the iterator, since it is part of the module and will not change. In addition, we followed the principle of single responsibility when one class is responsible for enumerating values. They also did not mindlessly use the method of separating interfaces, although it was possible to separate the addition of arguments from their enumeration. We are designing a separate module (framework), and excessive separation will make the module's API more complex.

At this stage, we have defined all the methods necessary for the operation of our module (framework). It remains to describe the class fields, completing the design of the module.

The first two fields are obvious, these are the command line arguments from the constructor. The third field should be a storage that will contain a bunch of short, long argument and algorithm.

We could immediately use a standard container that suits us, but this would violate the rule of sole responsibility. Therefore, we will create a separate FlagStorage class that will perform only two actions, add values ​​to the list of arguments and search for a suitable value based on the passed argument.

Class diagram

Class diagram

If you compare this diagram with the previous one, you'll notice that we've replaced the add method with an algorithm method, which returns the store. The store has its own method for adding values, so there is no need to duplicate it in different classes.

In addition, the store is private, which means it needs a getter. In general, this method contains many features related to clean code that I would like to highlight.

It was stated above that directly using a standard container as storage violates the single responsibility principle. This statement is of course important, but it is not the only reason. When we use a container directly, the programmer is tempted to use not only the adding method, but also its other capabilities. At first glance, there are fewer classes, but at the same time horizontal connectivity increases. Which significantly increases problems when making changes in the future.

It's not for nothing that refactoring issues come up so often in discussions. Now we’ve done it because it’s easier for us; in the future, let others figure it out. But as the amount of code written from scratch becomes less and less. Refactoring is becoming increasingly important.

Instead of simply changing the container in the storage to, say, a SQL server, you will have to identify all the dependencies and rewrite a significant part of the functionality.

Well, there is one last point that I would like to clarify. You can make the storage field public, thereby eliminating the need to create a separate getter, and in this example this is quite acceptable. Only this is not correct.

According to the object-oriented programming paradigm, an object, after its construction, must be completely ready for use. We must influence its state only through methods.

Public fields will allow you to influence an object without using methods, and this again leads to excessive code coupling. Therefore, when it is possible to remove an extra getter, it is better not to do this.

The habit of doing everything “correctly” at once is pure code, all other rules are just rules.

Well, since the design of the frame was completed before this digression, the full code of the module is given below.

namespace CommandLineArguments {
	class AbstractAlgorithm
	{
	protected:
		std::list<std::string>  parameters;
	public:
		virtual void parameter(std::string param) {
			parameters.push_back(param);
		}
		virtual void execute() = 0;
	};

	namespace {
		class FlagStorage
		{
		private:
			std::list<std::tuple<std::string, std::string, std::shared_ptr<AbstractAlgorithm>>> storage;
		public:
			FlagStorage() {}
			void add(std::string shortflag, std::string fullflag, std::shared_ptr<AbstractAlgorithm> flagalgorithm) {
				storage.push_back(std::make_tuple(shortflag, fullflag, flagalgorithm));
			}
			std::shared_ptr<AbstractAlgorithm> algorithm(std::string flag) const {
				for (auto& element : storage)
					if (std::get<0>(element) == flag or std::get<1>(element) == flag)
						return std::get<2>(element);
				throw std::invalid_argument("Флаг " + flag + " не используется.");
			}
		};


		class ArgIterator
		{
		private:
			int index;
			int count;
			const char** ptr;
			FlagStorage storage;
		public:
			ArgIterator() : index(1), count(0), ptr(nullptr) {}
			ArgIterator(const int count, const char** ptr, FlagStorage& algorithms) : ArgIterator() {
				this->count = count;
				this->ptr = ptr;
				this->storage = algorithms;
			}
			ArgIterator(const int count, const char** ptr, const int index, FlagStorage& algorithms) : ArgIterator(count, ptr, algorithms) {
				this->index = index;
			}
			ArgIterator& operator++()
			{
				if (index < count && ptr != nullptr) {
					index++;
					while (index < count && (ptr[index])[0] != '-')
						index++;
				}
				return *this;
			}
			std::shared_ptr<AbstractAlgorithm> operator*() const {
				if (index < count && ptr != nullptr) {
					int inindex = index + 1;
					auto answer = storage.algorithm(std::string(ptr[index]));
					while (inindex < count && (ptr[inindex])[0] != '-') {
						answer->parameter(ptr[inindex]);
						inindex++;
					}
					return answer;
				}
				throw std::out_of_range("Выход за пределы массива.");
			}
			bool operator!=(const ArgIterator& obj) const {
				if (index < count && ptr != nullptr)
					return this->index != obj.index;
				else
					return false;
			}
		};
	}

	class Arguments
	{
	private:
		int argcount;
		const char** arglist;
		FlagStorage storage;
	public:
		Arguments() : argcount(0), arglist(nullptr) {}
		Arguments(int argc, const char** argv) : argcount(argc), arglist(argv) {}
		ArgIterator begin() {
			return ArgIterator(argcount, arglist, storage);
		}
		ArgIterator end() {
			return ArgIterator(argcount, arglist, argcount - 1, storage);
		}
		FlagStorage& algorithms() {
			return storage;
		}
	};
}


using CommandLineArguments::AbstractAlgorithm;

class SortAlgorithm : public AbstractAlgorithm
{
public:
	virtual void execute() {
		std::cout << "Выполнение алгоритма сортировки Параметр: ";
		for (auto& prmtr : parameters)
			std::cout << prmtr << " ";
		std::cout << std::endl;
	}
};

class InsideAlgorithm : public AbstractAlgorithm
{
public:
	virtual void execute() {
		std::cout << "Выполнение алгоритма загрузки данных Параметр: ";
		for (auto& prmtr : parameters)
			std::cout << prmtr << " ";
		std::cout << std::endl;
	}
};

class OutsideAlgorithm : public AbstractAlgorithm
{
public:
	virtual void execute() {
		std::cout << "Выполнение алгоритма выгрузки данных Параметр: ";
		for (auto& prmtr : parameters)
			std::cout << prmtr << " ";
		std::cout << std::endl;
	}
};

class ExcludeAlgorithm : public AbstractAlgorithm
{
public:
	virtual void execute() {
		std::cout << "Выполнение алгоритма отбора данных Параметр: ";
		for (auto& prmtr : parameters)
			std::cout << prmtr << " ";
		std::cout << std::endl;
	}
};

int main(int argc, const char* argv[]) {
	setlocale(LC_ALL, "rus");
	for (size_t i = 0; i < argc; i++)
		std::cout << argv[i] << std::endl;

	using CommandLineArguments::Arguments;
	Arguments arguments(argc, argv);
	arguments.algorithms().add("-s", "--sort", std::make_shared<SortAlgorithm>());
	arguments.algorithms().add("-i", "--inside", std::make_shared<InsideAlgorithm>());
	arguments.algorithms().add("-o", "--outside", std::make_shared<OutsideAlgorithm>());
	arguments.algorithms().add("-e", "--exclude", std::make_shared<ExcludeAlgorithm>());

	try {
		for (auto element : arguments)
			element->execute();
	}
	catch (const std::exception& exc) {
		std::cout << exc.what();
	}
}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *