Подтвердить что ты не робот

Какую работу должен выполнять конструктор для класса разбора HTML?

Сколько работы разумно для конструктора объектов? Должна ли она просто инициализировать поля и фактически не выполнять какие-либо операции с данными, или это нормально, чтобы выполнить какой-либо анализ?

Фон: Я писал класс, который отвечает за разбор HTML-страницы и возвращает различную информацию на основе анализируемой информации. Конструкция класса такова, что конструктор класса выполняет разбор, бросая исключение, если возникает ошибка. После инициализации экземпляра анализируемые значения доступны без дальнейшей обработки через аксессоры. Что-то вроде:

public class Parser {

    public Parser(final String html) throws ParsingException {
        /* Parsing logic that sets private fields */
        /* that throws an error if something is erroneous.*/
    }

    public int getNumOfWhatevers() { return private field; }
    public String getOtherValue()  { return other private field; }
}

После разработки класса я начал задаваться вопросом, была ли это правильная практика OO. Если код синтаксического анализа должен быть помещен в метод void parseHtml(), и аксессоры вернутся только к допустимым значениям после вызова этого метода? Я чувствую, что моя реализация правильная, но я не могу не чувствовать, что некоторые пуристы OO могут найти ее неправильной по какой-то причине и что реализация, такая как следующее, будет лучше:

public class Parser {

    public Parser(final String html) {
        /* Remember html for later parsing. */
    }

    public void parseHtml() throws ParsingException { 
        /* Parsing logic that sets private fields */
        /* that throws an error if something is erroneous.*/
    }

    public int getNumOfWhatevers() { return private field; }
    public String getOtherValue()  { return other private field; }
}

Существуют ли случаи, когда код инициализации, такой как информация о синтаксическом анализе, не должен возникать внутри конструктора, или я просто глупый и второсортный?

Каковы преимущества/недостатки разделения синтаксического анализа на конструктор?

Мысли? Insights?

4b9b3361

Ответ 1

Я обычно следую одному простому принципу:

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

Каждое другое действие выполняется другими способами.

Конструктор никогда не должен:

  • использовать другие методы класса с целью использования переопределения поведения
  • действует по своим личным атрибутам с помощью методов

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

В вашем случае я бы использовал метод Parser:: parseHtml (файл). Конкретизация анализатора и синтаксический анализ - это две разные операции. Когда вы экземпляр парсера, конструктор помещает его в условие для выполнения своей работы (синтаксический анализ). Затем вы используете свой метод для синтаксического анализа. У вас есть два варианта:

  • Либо вы разрешаете синтаксическому анализатору содержать результаты синтаксического анализа, и давайте клиентам интерфейс для получения анализируемой информации (например, Parser:: getFooValue()). Методы возвратят Null, если вы еще не выполнили парсинг, или если синтаксический анализ завершился неудачей.
  • или ваш Parser:: parseHtml() возвращает экземпляр ParsingResult, содержащий найденный Parser.

Вторая стратегия предоставляет вам более гранулярность, поскольку Parser теперь не имеет состояния, и клиенту необходимо взаимодействовать с методами интерфейса ParsingResult. Интерфейс Parser остается гладким и простым. Внутренности класса Parser будут следовать шаблону

Не совсем. Если вы вернете экземпляр Parser, конечно, он будет разбираться. В Qt, когда вы создаете экземпляр кнопки, конечно, это будет показано. Однако у вас есть метод QWidget:: show() для ручного вызова, прежде чем что-то будет видно пользователю.

Любой объект в ООП имеет две проблемы: инициализация и операция (игнорировать финализацию, это не обсуждается прямо сейчас). Если вы держите эти две операции вместе, вы оба рискуете неприятностями (имея неполный объект) и теряете гибкость. Существует множество причин, по которым вы должны выполнить промежуточную настройку своего объекта перед вызовом parseHtml(). Пример: предположим, что вы хотите настроить свой Parser как строгий (чтобы сбой, если данный столбец в таблице содержит строку вместо целого) или разрешающий. Или зарегистрировать объект-прослушиватель, который предупреждается каждый раз, когда выполняется или завершается новый синтаксический анализ (думаю, индикатор выполнения GUI). Это необязательная информация, и если ваша архитектура ставит конструктор как übermethod, который делает все, у вас появляется огромный список необязательных параметров и условий метода для обработки в методе, который по своей сути является минным полем.

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

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

Однако обратите внимание, что я не сторонник того, что вы никогда не должны использовать вызов конструктора для синтаксического разбора. Я просто утверждаю, что это потенциально опасно, и вы теряете гибкость. Существует множество примеров, где конструктор находится в центре фактической активности объекта, но есть также множество примеров противоположного. Пример (хотя и предвзятый, он возникает из стиля C): в python я бы счел очень странным что-то вроде этого

f = file()
f.setReadOnly()
f.open(filename)

вместо фактического

f = file(filename,"r")

Но я уверен, что есть библиотеки доступа IO, использующие первый подход (второй - как синтаксис сахара).

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

Ответ 2

"Если код синтаксического анализа должен быть помещен в метод void parseHtml(), и аксессоры возвращают только правильные значения после вызова этого метода?

Да.

"Конструкция класса такова, что конструктор класса выполняет синтаксический анализ

Это предотвращает настройку, расширение и, самое главное, инъекцию зависимостей.

Будут случаи, когда вы хотите сделать следующее

  • Построить парсер.

  • Добавить функции в парсер: бизнес-правила, фильтры, лучшие алгоритмы, стратегии, команды, что угодно.

  • Анализировать.

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


Edit

"Не могут ли расширения просто анализировать дополнительную информацию в своих конструкторах?"

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

"Кроме того, синтаксический анализ в конструкторе позволяет мне неудачно рано"

Вид. Неудача во время строительства - странный случай использования. Неудача во время строительства затрудняет создание такого синтаксического анализатора...

class SomeClient {
    parser p = new Parser();
    void aMethod() {...}
}

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

Вы вынуждены создавать парсер в теле метода, потому что он имеет слишком сложные аргументы.

Вкратце, вы удалили параметры из ваших парсеров.

"Невозможно наследовать от этого класса, чтобы заменить алгоритм".

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

Вы можете претендовать на совершенство, но это редко. Подклассы являются нормой, а не исключением. Кто-то всегда улучшит ваше "совершенство". Если вы ограничите их способность подклассифицировать ваш парсер, они просто отбросят его на что-то более гибкое.

"Я не вижу необходимости в шаге 2, как описано в ответе".

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

Ограничение способности подкласса или расширение вашего синтаксического анализа является плохой политикой.

Нижняя строка.

Нет ничего. Напишите класс с минимальными предположениями о возможных случаях использования. Анализ во время строительства делает слишком много предположений о случаях использования клиентов.

Ответ 3

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

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

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

Если входные значения не сортируются из входящего потока XML, я бы подумал об использовании схем для его проверки.

Ответ 4

Я бы, скорее всего, достаточно прошел, чтобы инициализировать объект, а затем использовать метод "parse". Идея состоит в том, что дорогие операции должны быть настолько очевидными, насколько это возможно.

Ответ 5

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

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

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

В любом случае, если процесс сборки сложный, я бы предложил использовать шаблон создания (factory, построитель).

Ответ 6

Хорошим эмпирическим правилом является только инициализация полей в конструкторах, и в противном случае сделать как можно меньше для инициализации Object. Используя Java в качестве примера, вы можете столкнуться с проблемами, если вы вызываете методы в своем конструкторе, особенно если вы подклассифицируете ваш Object. Это связано с тем, что из-за порядка операций в экземпляре объектов переменные экземпляра не будут оцениваться до тех пор, пока не закончится супер конструктор. Если вы попытаетесь получить доступ к полю во время процесса суперконструктора, вы будете бросать Exception

Предположим, что у вас есть суперкласс

class Test {

   Test () {
      doSomething();
   }

   void doSomething() {
     ...
   }
 }

и у вас есть подкласс:

class SubTest extends Test {
    Object myObj = new Object();

    @Override
    void doSomething() {
        System.out.println(myObj.toString()); // throws a NullPointerException          
    }
 }

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

изменить как ответ на ваш комментарий:

Хотя я обычно уклоняюсь от методов в конструкторах, в этом случае у вас есть несколько вариантов:

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

  • Установите HTML как поле на вашем объекте, а затем введите зависимость от parse(), при этом его нужно вызвать либо сразу после завершения конструктора, либо включить какой-то ленивый синтаксический анализ, добавив что-то вроде "обеспечитьParsed()" во главе ваших аксессуаров. Мне не нравится это так много, так как вы могли бы иметь HTML-код после того, как вы разобрались, и ваш вызов ensureParsed() может быть закодирован для установки всех ваших проанализированных полей, тем самым создавая побочный эффект для вашего получателя.

  • Вы можете вызвать parse() из своего конструктора и запустить риск исключения. Как вы говорите, вы устанавливаете поля для инициализации Object, так что это действительно нормально. Что касается Exception, то утверждается, что допустимый незаконный аргумент, переданный в конструктор, допустим. Если вы это сделаете, вы должны быть осторожны, чтобы понять, как ваш язык обрабатывает создание объектов, как обсуждалось выше. Чтобы следить за приведенным выше примером Java, вы можете сделать это без страха, если вы убедитесь, что из конструктора вызываются только методы private (и, следовательно, не могут быть переопределены подклассами).

Ответ 7

Misko Hevery имеет приятную историю на эту тему, с точки зрения модульного тестирования, здесь.

Ответ 8

Конструктор должен создать действительный объект. Если в вашем случае требуется чтение и разбор информации, чем это возможно.

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

Ответ 9

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

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

В случае, если вы говорите о Html Parser, я бы выбрал создание класса, а затем вызвал метод Parse Html. Причина этого заключается в том, что она дает вам возможность добавлять элементы в класс для разбора Html.

Ответ 10

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

public class Parser {
    public Parser() {
        // Do what is necessary to construct a parser.
        // Perhaps we need to initialize a Unicode library, UTF-8 decoder, etc
    }
    public virtual ParseResult parseHTMLString(final string html) throws ParsingException
    {
        // Parser would do actual work here
        return new ParseResult(1, 2);
    }
}
public class ParseResult
{
    private int field1;
    private int field2;
    public ParseResult(int _field1, int _field2)
    {
        field1 = _field1;
        field2 = _field2;
    }
    public int getField1()
    {
        return field1;
    }
    public int getField2()
    {
        return field2;
    }
}

Если ваш парсер может работать с частичными наборами данных, я бы предположил, что было бы целесообразно добавить еще один класс в микс. Возможно, PartialParseResult?

Ответ 11

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

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


Предлагаемый код для обсуждения:

public class MyHtmlScraper {
    private TextReader _htmlFileReader;
    private bool _parsed;

    public MyHtmlScraper(string htmlFilePath) {
        _htmlFileReader = new StreamReader(htmlFilePath);
        // If done in the constructor, DoTheParse would be called here
    }

    private string _parsedValue1;
    public string Accessor1 {
        get {
            EnsureParsed();
            return _parsedValue1;
        }
    }

    private string _parsedValue2;
    public string Accessor2 {
        get {
            EnsureParsed();
            return _parsedValue2;
        }
    }

    private void EnsureParsed(){
        if (_parsed) return;
        DoTheParse();
        _parsed = true;
    }

    private void DoTheParse() {
        // parse the file here, using _htmlFileReader
        // parse into _parsedValue1, 2, etc.
    }
}

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

Это не очень большое дело, но моя склонность - сделать как можно меньше в конструкторе. Это позволяет создавать сценарии, в которых строительство должно быть быстрым. Это, без сомнения, будут ситуации, которые вы не рассматривали, такие как десериализация.

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

Но так как вам не нужно это делать в конструкторе, мой совет просто - не делайте.

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

Ответ 12

Я думаю, что когда вы создаете класс ($ obj = новый класс), класс не должен влиять на страницу вообще и должен быть относительно низкой.

Например:

Если у вас есть пользовательский класс, он должен проверять входящие параметры входа/выхода, а также файлы cookie и назначать их переменным класса.

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

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

Во многих моих классах я проверяю определенные параметры для определения "действия", например, добавления, редактирования или удаления.

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

Ответ 13

Почему бы просто не передать парсер конструктору? Это позволит вам изменить реализацию без изменения модели:

public interface IParser
{
    Dictionary<string, object> ParseDocument(string document);
}

public class HtmlParser : IParser
{
    // Properties, etc...

    public Dictionary<string, object> ParseDocument(string document){
         //Do what you need to, return the collection of properties
         return someDictionaryOfHtmlObjects;
    }
}

public class HtmlScrapper
{
    // Properties, etc...

    public HtmlScrapper(IParser parser, string HtmlDocument){
         //Set your properties
    }

    public void ParseDocument(){
         this.myDictionaryOfHtmlObjects = 
                  parser.ParseDocument(this.htmlDocument);
    }

}

Это должно дать вам некоторую гибкость в изменении/улучшении работы вашего приложения без необходимости переписывать этот класс.

Ответ 14

В моем случае все содержимое HTML файл передается через String. Строка больше не требуется он анализируется и достаточно велик (a несколько сотен килобайт). Так было бы лучше не хранить его в памяти. объект не должен использоваться для других случаев. Он был разработан для анализа определенная страница. Разбор чего-то еще должны побуждать к созданию другой объект, чтобы проанализировать это.

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

Я не уверен, как здесь помогает "объектно-ориентированная" часть. Если есть только один объект, и он может обрабатывать только одну конкретную страницу, тогда неясно, почему это должен быть объект. Вы можете сделать это так же легко в процедурном (то есть не OO) коде.

Для языков, на которых есть только объекты (например, Java), вы можете просто создать метод static в классе, у которого не было доступного конструктора, а затем вызвать парсер и вернуть все проанализированные значения в Map или подобной коллекции

Ответ 15

Возможная опция - переместить код синтаксического анализа в отдельную функцию, сделать конструктор закрытым и иметь статический метод parse (html), который создает объект и сразу вызывает функцию синтаксического анализа. Таким образом, вы избегаете проблем с разбором конструктора (несогласованное состояние, проблемы при вызове переопределенных функций,...). Но клиентский код по-прежнему обладает всеми преимуществами (один вызов для получения разобранной html или "ранней" ошибки).

Ответ 16

Как уже многие прокомментировали общее правило, нужно только выполнять инициализацию в конструкторах и никогда не использовать виртуальные методы say (вы получите предупреждение о компиляторе, если попытаетесь обратить внимание на это предупреждение:)). В вашем конкретном случае я бы тоже не пошел на метод parHTML. объект должен находиться в правильном состоянии, когда он будет создан, вам нужно будет сделать материал для объекта, прежде чем вы сможете его использовать.

Лично я бы пошел на метод factory. Выявление класса без общих конструкторов и создание его с помощью метода factory. Пусть ваш метод factory выполняет синтаксический анализ и передает обработанный результат в частный/защищенный конструктор.

взгляните на System.Web.WebRequest, если вы хотите увидеть образец некоторой аналогичной логики.

Ответ 17

Я согласен с плакатами здесь, утверждая минимальную работу в конструкторе, на самом деле просто помещая объект в состояние без зомби, а затем выполняет функции глагола, такие как parseHTML();

Один момент, который я хотел бы сделать, хотя я не хочу вызывать пламенную войну, рассматривает случай среды без исключения. Я знаю, что вы говорите о С#, но я стараюсь, чтобы мои модели программирования были как можно более похожими между С++ и С#. По разным причинам я не использую исключения в С++ (думаю, встроенное программирование видеоигр), я использую ошибки кода возврата.

В этом случае я не могу генерировать исключения в конструкторе, поэтому я, как правило, не имею конструктора, делающего все, что может потерпеть неудачу. Я оставляю это для функций доступа.

Ответ 18

В общем случае конструктор должен:

  • Инициализировать все поля.
  • Оставьте полученный объект в допустимом состоянии.

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

Обычно, когда я пишу синтаксический анализатор, я пишу его как одноэлементный. Я не храню никаких полей в объекте, кроме одного экземпляра; вместо этого я использую только локальные переменные в методах. Теоретически это могут быть только статические (класс-уровень) методы, но это будет означать, что я не мог сделать их виртуальными.

Ответ 19

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