Должен ли элемент управления конструктором разбираться?

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

Например, если объект является Car, тогда файл может представлять собой группу строк, каждая из которых содержит имя, скорость и цвет (три обязательных параметра конструктора), разделенные вкладками:

My car          65       Red
Arthur car    132      Pink
Old junk car    23       Rust brown

Это легко проверить визуально, изменить или сгенерировать другую программу. Затем программа может загрузить файл, взять каждую строку, проанализировать соответствующие параметры, передать их в конструктор Car(string name, int speed, uint color) и создать объект.

Обратите внимание на то, что на входе должна выполняться некоторая работа, прежде чем она будет совместима с конструктором. Скорость должна быть преобразована из string в int с вызовом int.Parse. Цвет должен соответствовать значению RGB, просматривая английское имя цвета (возможно, программа будет получать доступ к Википедии, чтобы выяснить каждое значение цвета, или где-нибудь проконсультируется с предопределенной картой имени → RGB).

Мой вопрос: с точки зрения ООП, кто должен делать этот разбор? Конструктор или метод, вызывающий конструктор?

При первом варианте преимущество - простота. Вызывающая функция должна выполнять только:

foreach(var row in input_file)
     list_of_objects_that_i_am_populating.Add(new Car(row));

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

Недостатком является то, что повторное использование кода выходит из окна, потому что теперь мой объект присоединяется к бедеру к входному формату (хуже того, поскольку формат ввода является ad-hoc и составлен вручную, он является эфемерным и потенциально не гарантируется тоже самое). Если я повторно использую этот объект в другой программе, где я решаю, что удобно слегка изменить форматирование входного файла, две версии определения объекта теперь расходятся. Я часто обнаруживаю, что определяю форматы ввода в разделе комментариев конструктора, который кажется немного вонючим.

Другим недостатком является то, что я потерял способность выполнять пакетные операции. Вспомните предыдущую примерную проблему сопоставления имен цветов со значениями. Что делать, если я использую веб-службу, которая занимает 1 минуту для обработки каждого отдельного запроса, независимо от того, просит ли этот запрос преобразовать одно имя цвета или миллион. С очень большим входным файлом я резко сократил бы свое приложение, обратившись к службе один раз для каждой строки, вместо того, чтобы отправлять один большой запрос для всех строк, а затем создавать экземпляры объектов в соответствии с ответом.

Каков "правильный" способ справиться с такой ситуацией? Должен ли я разбирать ввод конструктора и рассматривать вышеперечисленные проблемы как исключительные проблемы, которые необходимо решать в каждом конкретном случае? Должен ли я позволить моему методу вызова выполнять разбор (хотя он может уже раздуваться с помощью много запутанной программной логики)?

Ответ 1

Я бы создал и использовал методы factory для загрузки через настройки/файл, csv и NOT помещал такой код в сам конструктор.

Factory версия 1:

public class Car
{
     ... your existing methods and data ...


     public static Car CreateFromCsv(string csv ) { .... }
     public static Car CreateFromFile(string fileName) { ...}
 }

Или используйте выделенный Factory:

public static class CarFactory
{
   public static Car CreateFromCsv(string csv ) { .... }
   public static Car CreateFromFile(string fileName) { ...}
}

Или выделенный класс бизнес-логики:

namespace BusinessLogic;

public class LoadCars
{

    public Car ExecuteForCsv(string csv) { ...}
    public Car ExecuteForFile(string fileName) { ... }
}

Ответ 2

Мой вопрос: с точки зрения ООП, кто должен делать этот разбор? Конструктор или метод, вызывающий конструктор?

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

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

Ответ 3

Я считаю, что лучше всего сделать ваш FileParser отдельно от вашего класса Car. Я лично проанализировал бы файл и вернул бы List<string[]> или что-то в этом роде, а затем перегрузил конструктор Car, например:

Car(string[] values)
{
    // do error handling here like
    if (values.Length != 2)
        // error
    if (int.TryParse(values[1], out tempVar))
        // set int param, if not then throw error      
}

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

Ответ 4

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