Чистый код: Аргументы командной строки

Поскольку эта статья завершает цикл про SOLID с примерами, хотелось бы показать, как эти принципы позволяют создавать что-то большее. В этой статье создадим небольшой модуль, каркас (framework), для работы с аргументами командной строки.

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

Разработку модуля начнем с определения цели, очень тривиально, но правильно поставленная цель, значительно упрощает реализацию. Основной целью модуля будет создание инструмента удобного для пользователя. Легко добавлять, изменять значения аргументов, легко перебирать значения.

Опишем функционал модуля.

  • Модуль должен принимать все аргументы разом, а не использовать каждый аргумент в отдельности.

  • Перебор аргументов по указателям необходимо реализовать внутри модуля, скрыв сложное действие от пользователя.

  • Модуль должен хранить список возможных аргументов.

  • Модуль должен выдавать список действий, алгоритмов аргументы которых были перечислены в параметрах командной строки.

На данном этапе хотелось бы сделать отступление и пояснить, то что логично, но не очевидно с первого взгляда. Поддерживаемых аргументов командной строки может быть много, однако случаи когда они все используются разом, очень редки. Поэтому список фактически переданных аргументов почти всегда короче полного списка.

Но когда мы как программисты используем аргументы, для нас важны те действия, алгоритмы, которые необходимо выполнить, а не сами аргументы. Отсюда можно сделать логический вывод. Даже пару. Первое, необходимо отделить логику создания списка возможных аргументов. Второе, список переданных аргументов не важен, важно получить список алгоритмов которые необходимо выполнить.

То есть мы в самом начале создаем список всех возможных аргументов. К каждому аргументу прикрепляем алгоритм, который необходимо выполнить, а позже, из модуля получаем список алгоритмов на которые указали подставленные при запуске программы значения.

Когда с тонкостями разобрались, можем приступить к проектированию. Поскольку наш функционал является отдельным модулем, ему необходимо придать фасад. Тот самый Фасад (Facade) из паттернов проектирования. Его еще можно назвать API нашего модуля.

На первом этапе он будет выглядеть так.

Диаграмма классов<br />
Диаграмма классов

Класс Arguments в конструкторе, принимает аргументы из функции main, тем самым пряча от нас все действия с ними.

Метод add() позволит создать тот самый список всех возможных аргументов. Стоит прикинуть, какие параметры будет иметь метод. В первую очередь, аргумент командной строки в виде строки. Однако и здесь есть «подводные камни», которые необходимо обсудить.

Какой аргумент использовать. Принято, что аргументы бывают короткие “-h” и длинные “--help”. Взгляните, при наборе из командной строки удобнее вводить так.

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

Однако при написании скриптов строка содержащая длинные аргументы выглядит лучше.

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

Будем придерживаться устоявшихся правил и вводить их вместе.

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

Диаграмма классов<br />
Диаграмма классов

Хочу обратить внимание, подставив в качестве аргумента абстракцию, мы выполнили инверсию зависимости, теперь можно создавать любое количество алгоритмов не меняя сам модуль.

Следующим шагом, необходимо определиться, как мы будем работать со списком алгоритмов, которые будут выбраны по аргументам командной строки. Как по мне, самый простой способ, это перебрать их в цикле.

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

Реализовать такой подход достаточно просто, необходимы два метода begin() и end() которые возвращают итераторы, паттерн Итератор (Iterator). Точнее один итератор, с позиционированный на начало и конец списка.

Диаграмма классов<br />
Диаграмма классов

Обратите внимание, мы не стали выполнять инверсию зависимостей для итератора, так как он входит в состав модуля и не будет меняться. К тому же мы соблюли принцип единственной ответственности когда один класс отвечает за перебор значений. Также не стали бездумно использовать метод разделения интерфейсов, хотя можно было отделить добавление аргументов от их перебора. Мы проектируем отдельный модуль (framework), а излишнее разделение сделает API модуля сложнее.

На данном этапе мы определили все методы необходимые для работы нашего модуля (framework). Осталось описать поля класса, завершив проектирование модуля.

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

Можно было бы сразу использовать подходящий нам стандартный контейнер, но это нарушит правило единственной ответственности. Поэтому создадим отдельный класс FlagStorage который будет выполнять всего два действия, добавлять значения в список аргументов и искать подходящее значение по переданному аргументу.

Диаграмма классов<br />
Диаграмма классов

Если вы сравните эту диаграмму с предыдущей, можно заметить, что мы заменили метод добавления на метод algorithm, который возвращает хранилище. В хранилище есть свой метод для добавления значений, поэтому нет необходимости дублировать его в разных классах.

Помимо этого, хранилище является приватным, а значит ему необходим геттер. Вообще, в этом методе сосредоточено множество особенностей связанных с чистым кодом, которые я хотел бы озвучить.

Выше было указано, что напрямую использование в качестве хранилища стандартного контейнера нарушает принцип единственной ответственности. Это утверждение конечно, важно, но это не единственная причина. Когда мы используем контейнер на прямую, у программиста появляется соблазн, использовать не только метод добавления, но и другие его возможности. На первый взгляд, классов становится меньше, но вместе с этим увеличивается горизонтальная связанность. Что значительно увеличивает проблемы при внесении изменений в будущем.

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

Вместо того, чтобы просто поменять в хранилище контейнер на, допустим, SQL сервер, придется выявлять все зависимости и переписывать значительную часть функционала.

Ну и последний момент, который хотелось бы уточнить. Можно сделать поле хранилища публичным, и тем самым отпадет необходимость делать отдельный геттер, и данном примере это вполне приемлемо. Только вот это не правильно.

Согласно парадигме объектно-ориентированного программирования, объект после его конструирования должен быть полностью готов к работе. На его состояние мы должны влиять только через методы.

Открытые поля позволят влиять на объект без использования методов, а это опять ведет к излишней связанности кода. Поэтому когда можно убрать лишний геттер, лучше этого не делать.

Привычка все сразу делать «правильно» и есть чистый код, все остальные правила, просто правила.

Ну и поскольку проектирование каркаса (framework) завершилось до этого отступления, ниже приведен полный код модуля.

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(); } }
22
1 комментарий

Статья хорошо показывает применение SOLID на практике, особенно через паттерны Фабричный метод и Фасад. Однако слишком много теории для простой задачи, что может утомить новичков. Местами решение выглядит как overengineering, можно было бы упростить. Плюс за гибкость и чистый код, но переход на итераторы кажется избыточным. Хотелось бы больше кода и меньше слов. :))

Ответить