Как спроектировать мой C# jQuery API так, чтобы он не мешал использовать?
Я делаю JQuery клон для C#. Прямо сейчас я настроил его так, чтобы каждый метод был методом расширения IEnumerable<HtmlNode>
так что он хорошо работает с существующими проектами, которые уже используют HtmlAgilityPack
, Я думал, что смогу уйти без сохранения состояния... однако потом я заметил, что у jQuery есть два метода .andSelf
а также .end
которые "выталкивают" самые последние элементы из внутреннего стека. Я могу имитировать эту функцию, если я изменю свой класс так, чтобы он всегда работал с объектами SharpQuery вместо перечислимых, но проблема остается.
С JavaScript вы получаете HTML-документ автоматически, но при работе в C# вы должны явно загружать его, и вы можете использовать более одного документа, если хотите. Похоже, что когда вы звоните $('xxx')
по сути, вы создаете новый объект jQuery и начинаете все заново с пустого стека. В C# вам бы этого не хотелось, потому что вы не хотите перезагружать / повторно загружать документ из Интернета. Таким образом, вместо этого вы загружаете его один раз либо в объект SharpQuery, либо в список HtmlNodes (для начала вам просто необходим DocumentNode).
В документах jQuery они приводят этот пример
$('ul.first').find('.foo')
.css('background-color', 'red')
.end().find('.bar')
.css('background-color', 'green')
.end();
У меня нет метода инициализации, потому что я не могу перегрузить ()
оператор, так что вы просто начинаете с sq.Find()
вместо этого, который действует в корне документа, по сути, делая то же самое. Но тогда люди попытаются написать sq.Find()
на одной строке, а затем sq.Find()
где-то в будущем, и (по праву) ожидаю, что он снова будет работать с корнем документа... но если я поддерживаю состояние, то вы только что изменили контекст после первого вызова.
Итак... как мне разработать свой API? Должен ли я добавить еще Init
метод, с которого должны начинаться все запросы, сбрасывающий стек (но как мне заставить их начинать с этого?), или добавить Reset()
что они должны позвонить в конце своей линии? Я перегружаю []
вместо этого и сказать им, чтобы начать с этого? Должен ли я сказать "забудь, никто не использует эти сохраненные государством функции в любом случае?"
По сути, как бы вы хотели, чтобы этот пример jQuery был написан на C#?
sq["ul.first"].Find(".foo") ...
Ошибки: злоупотребляет[]
имущество.sq.Init("ul.first").Find(".foo") ...
Недостатки: ничто действительно не заставляет программиста начинать с Init, если я не добавлю какой-нибудь странный "инициализированный" механизм; пользователь может попробовать начать с.Find
и не получить результат, которого он ожидал. Также,Init
а такжеFind
в любом случае они практически идентичны, за исключением того, что первый также сбрасывает стек.sq.Find("ul.first").Find(".foo") ... .ClearStack()
Недостатки: программист может забыть очистить стек.Не могу этого сделать.
end()
не реализованы.Используйте два разных объекта.
Возможно использоватьHtmlDocument
в качестве основы, с которой должны начинаться все запросы, а затем каждый метод возвращаетSharpQuery
объект, который может быть прикован цепью. Таким образом,HtmlDocument
всегда поддерживает исходное состояние, ноSharpQuery
объекты могут иметь разные состояния. К сожалению, это означает, что мне нужно реализовать кучу вещей дважды (один раз для HtmlDocument, один раз для объекта SharpQuery).new SharpQuery(sq).Find("ul.first").Find(".foo") ...
Конструктор копирует ссылку на документ, но сбрасывает стек.
2 ответа
Я думаю, что основным камнем преткновения, с которым вы сталкиваетесь здесь, является то, что вы пытаетесь избежать неприятностей, имея только SharpQuery
объект для каждого документа. Это не так, как работает JQuery; в общем, объекты jQuery неизменны. Когда вы вызываете метод, который изменяет набор элементов (например, find
или же end
или же add
), он не изменяет существующий объект, но возвращает новый:
var theBody = $('body');
// $('body')[0] is the <body>
theBody.find('div').text('This is a div');
// $('body')[0] is still the <body>
(см. документациюend
для получения дополнительной информации)
SharpQuery должен работать так же. После создания объекта SharpQuery с документом вызовы методов должны возвращать новый SharpQuery
объекты, ссылающиеся на другой набор элементов одного и того же документа. Например:
var sq = SharpQuery.Load(new Uri("http://api.jquery.com/category/selectors/"));
var header = sq.Find("h1"); // doesn't change sq
var allTheLinks = sq.Find(".title-link") // all .title-link in the whole document; also doesn't change sq
var someOfTheLinks = header.Find(".title-link"); // just the .title-link in the <h1>; again, doesn't change sq or header
Преимущества такого подхода несколько. Так как sq
, header
, allTheLinks
и т. д. - это все один и тот же класс, у вас есть только одна реализация каждого метода. Тем не менее, каждый из этих объектов ссылается на один и тот же документ, поэтому у вас нет нескольких копий каждого узла, и изменения в узлах отражаются в каждом SharpQuery
объект на этом документе (например, после allTheLinks.text("foo")
, someOfTheLinks.text() == "foo"
.).
Внедрение end
и другие основанные на стеке манипуляции также становятся легкими. Поскольку каждый метод создает новый, отфильтрованный SharpQuery
объект от другого, он сохраняет ссылку на этот родительский объект (allTheLinks
в header
, header
в sq
). затем end
так же просто, как вернуть новый SharpQuery
содержит те же элементы, что и родительский, например:
public SharpQuery end()
{
return new SharpQuery(this.parent.GetAllElements());
}
(или, однако, ваш синтаксис встряхивает.)
Я думаю, что этот подход даст вам наиболее jQuery-подобное поведение с довольно простой реализацией. Я определенно буду следить за этим проектом; это отличная идея.
Я бы склонялся к варианту по варианту 2. В jQuery $() - это вызов функции. В C# нет глобальных функций, наиболее близок статический вызов функции. Я бы использовал метод, который указывает, что вы создаете оболочку, как..
SharpQuery.Create("ul.first").Find(".foo")
Я не буду беспокоиться о сокращении SharpQuery до sq, поскольку intellisense означает, что пользователям не нужно будет печатать все это целиком (а если у них есть резкость, то в любом случае им нужно только ввести SQ).