Я отвечаю за рефакторинг некоторого кода, который анализирует похожие (но разные файлы). Они отличаются тем, что имеют разное количество столбцов. Скажем, типы файлов называются MODEL_FILE
а также COMPANY_FILE
,
MODEL_FILE
имеет следующий формат:
CAR_MODEL CAR_COMPANY MILEAGE
COMPANY_FILE
имеет следующий формат:
CAR_COMPANY MILEAGE
Результат разбора MODEL_FILE
было бы std::map<Car_Model, std::map<Car_Company, double> >
; результат разбора COMPANY_FILE
было бы std::map<Car_Company, double>
,
Заголовочный файл выглядит примерно так:
typedef std::map<Car_Model, std::map<Car_Company, double> > Model_Data;
typedef std::map<Car_Company, double> Company_Data;
struct Data
{
Model_Data data_model;
Company_Data data_company;
};
bool parse_company_file(const std::string& path, Company_Data& data); // 1
bool parse_model_file(const std::string& path, Model_Data& data); // 2
bool parse_generic_file(bool is_company_file, const std::string& path, Data& data); // 3
Код синтаксического анализа действительно находится в 3
, И то и другое 1
а также 2
внутренне вызывать 3
, который знает (через логический параметр), имеет ли файл, который он собирается анализировать, 2 или 3 столбца. Только одно из полей в Data
будет заполнен (который зависит от параметра bool). Затем функция, вызывающая 3
извлечет соответствующее поле структуры из заполненного Data
структурировать и использовать, чтобы заполнить карту, которая была пройдена.
Таким образом, код для разбора файла только в одном месте (3
). Снаружи, код в порядке (две разные точки входа, которые возвращают соответствующие данные), но внутренняя реализация мне не подходит (уловка использования структуры как способа использования единственного метода для потенциального заполнения два разных и независимых типа объектов).
Я думал об использовании наследования, так что универсальный метод получает указатель на общий базовый класс, который имеет два метода (add_model_data()
а также add_company_data()
). Это вызвало бы один или другой в зависимости от bool
пары. Однако это более сложно и запутанно, и подразумевает нарушение абстракции, когда базовый класс должен знать о методах низших классов, он подвержен ошибкам и т. Д.
Вопрос в том, Можно ли каким-то образом сохранить логику синтаксического анализа в одном месте, но использовать другой (и, возможно, лучше) подход, чем struct
для того, чтобы иметь дело с разными файлами?
std::variant
а также boost::variant
предназначены для типов «или» — тип, который является A или B. Так что это один из подходов.
Еще один более изощренный подход — помнить, что существует 3 числа — 0, 1 и бесконечность.
Этот подход сложнее, но реорганизует ваш код синтаксического анализа в очень общий. Я бы не стал беспокоиться об этом решении, поэтому я просто делаю набросок ниже, но оно позволит вам добавить версию этого формата в 4 или 20 столбцов с минимальными затратами труда после того, как вы ее напишите.
Анализатор столбцов принимает строку и возвращает значение типа T:
std::string -> T
или же
template<class T>
using column_parser = std::function<T(std::string)>;
(Мы можем сделать это более эффективным позже).
Учитывая N парсеров столбцов, мы можем построить map<T0, map<T1, map<T2, map<..., map<TN-2, TN-1>...>>>>
,
template<class T0, class...Ts>
struct nested_map {
using type=T0;
};
template<class T0, class...Ts>
using nested_map_t = typename nested_map<T0, Ts...>::type;
template<class T0, class T1, class...Ts>
struct nested_map<T0, T1, Ts...> {
using type=std::map<T0, nested_map_t<T1, Ts...>>;
};
Это позволяет нам брать пачку типов и создавать карту.
template<class...Ts>
nested_map_t<Ts...> parse_file(std::string path, column_parser<Ts...> columns);
Это разбирает любое количество столбцов на вложенную карту.
Вы выставляете:
bool parse_company_file(const std::string& path, Company_Data& data) {
column_parser<Car_Company> company = // TODO
column_parser<double> milage = // TODO
try {
data = parse_file( path, company, milage );
} except (some_error) {
return false;
}
return true;
}
bool parse_model_file(const std::string& path, Model_Data& data) {
column_parser<Car_Model> model = // TODO
column_parser<Car_Company> company = // TODO
column_parser<double> milage = // TODO
try {
data = parse_file( path, model, company, milage );
} except (some_error) {
return false;
}
return true;
}
Теперь, чтобы написать parse_file
мы делаем что-то вроде (псевдокод)
template<class...Ts>
nested_map_t<Ts...> parse_file(std::string path, column_parser<Ts...> columns) {
nested_map_t<Ts...> retval;
auto f = open_file(path);
for( std::string line: get_lines(f)) {
std::vector<std::string> column_data = split_into_columns(line);
if (sizeof...(Ts) != column_data.size()) throw some_error;
index_upto<sizeof...(Ts)>()([&](auto...Is){
recursive_insert(retval, columns(column_data[Is])...);
});
}
return retval;
}
где index_upto
либо это в C ++ 14 или заменен ручным расширением пакета и вспомогательной функцией, и recursive_insert(m, t0, ts...)
берет «вложенную карту» M& m
и куча элементов T const&
и рекурсивно делает recursive_insert(m[t0], ts...)
пока не будет 1 элемент, и это делает m = t0
,
Других решений пока нет …