- Регистрация
- 29.07.2015
- Сообщения
- 148
- Благодарностей
- 291
- Баллы
- 63
Сегодня я хочу поговорить о наработках в области разработки шаблонов, которые облегчают жизнь как на стадии разработки и отладки, так и на стадии поддержки уже готового шаблона.
Ускоряем разработку с методами расширения
В каждом шаблоне нам приходится выполнять одни и те же действия. Типичный алгоритм:
Данный код загружает страницу Яндекса, вводит запрос в поле для поиска и нажимает кнопку "Найти".
После каждого поиска элемента сначала проверяем был ли он найден. В противном случае выбрасываем исключение с сообщением для упрощения отладки в будущем.
С этим кодом, на мой взгляд, есть несколько проблем:
Создаем метод расширения
Механизм методов расширения позволяет добавлять свои методы к существующим типам. Это позволяет нам сильно сократить объем кода и, как следствие, повысить читаемость и скорость разработки.
Чтобы расширить существующий тип своими методами нам нужно создать статический класс со статическими методами и первым аргументом в методе через ключевое слово this указать расширяемый тип.
Вот так мы можем добавить в системный тип string свой метод, который будет конвертировать строку в число:
Теперь мы можем воспользоваться этим методом в любом месте нашей программы:
Перерабатываем код с Яндексом
Начнем с этой части кода:
В шаблонах очень часто приходится использовать метод Navigate для загрузки страницы и я не помню ни одного случая, когда мне не требовалось бы сразу дождаться загрузки страницы. Так почему бы не вынести этот кусок кода в метод расширения типа Tab ?!
Сказано – сделано.
Создаем статический класс ZennoExstensions и добавляем в него метод расширения для типа Tab. Назовем этот метод NavigateAndWait и он будет принимать в параметры адрес и необязательный параметр реферер:
Теперь вместо стандартного метода Navigate мы можем использовать наш тюнингованный:
Шесть строчек схлопнулось в одну и при этом читаемость кода, как минимум, не ухудшилась. Ну а то, что одну строчку проще написать, чем шесть и ежу понятно.
Fluent Interface
Обратите внимание, что метод NavigateAndWait возвращает объект Tab. А это значит, что теперь мы можем делать цепочки вызовов таким образом:
Этот паттерн называется Fluent Interface. Суть в том, что на возвращаемом из метода объекте мы можем вызывать другой метод, объединяя их в такие вот цепочки. Более подробно можете почитать здесь.
В данном случае пока не слишком очевидна полезность этого паттерна, но далее вы увидите, что это очень удобная штука.
Перерабатываем поиск и проверку элементов
В примере с Яндексом у нас идет поиск 2-х элементов. Это поле ввода поискового запроса и кнопка "Найти":
Для начала сделаем метод расширения поиска элемента по XPath.
Сейчас мне не нравится, что всегда приходится передавать вторым параметром номер совпадения в методе FindElementByXPath. В моих шаблонах в 99.9% случаев это всегда будет 0. Поэтому можно сделать этот параметр необязательным, чтобы лишний раз не указывать его. Если в ваших шаблонах ситуация иная то, можно пропустить этот шаг.
Итак, добавляем в наш класс ZennoExtensions новый метод. Лучше для каждого расширяемого типа создавать отдельный класс, как на скрине. В статье я буду писать все одном классе для удобства.
Метод будет принимать 2 параметра: xpath и номер совпадения. Мы не можем назвать наш метод FindElementByXPath, т.к. сигнатуры будут полностью совпадать. Поэтому назовем наш метод GetElementByXpath:
Не отходя от кассы добавляем метод расширения для типа HtmlElement, который будет бросать исключение, если элемент не найден:
Метод ThrowIfNull бросает исключение с указанным сообщением, если элемент не найден. Если элемент существует, то метод возвращает его же для объединения вызовов методов в цепочки.
Теперь мы можем использовать эти методы так:
Итого первоначальный код преобразился до такого:
Такой код более читабельный, а главное его быстрее, проще и просто приятнее писать.
Какие еще методы расширения можно сделать?
Вариантов методов расширений, которые улучшат ваш процесс разработки огромное множество.
Например, можно сделать более удобное логирование таким образом:
Теперь мы можем вместо методов Send***ToLog мы можем вызывать:
Можете также черпать идеи из документации к библиотеке ZennoExtensions.
Вот код метода WaitFor, который реализован в библиотеке:
Этот метод удобен в использовании при динамическом изменении страницы, когда нужно дождаться появления, исчезновения или изменения элемента. Первые две перегрузки метода WaitFor выполняют ожидание, пока не будет найден элемент, либо до истечения указанного времени. Третья перегрузка выполняет ожидание, пока переданный предикат не вернет true, либо до истечения указанного времени.
Пример использования:
В видео рассматривается как применение методов расширения может выглядеть на практике.
Ускоряем разработку с методами расширения
В каждом шаблоне нам приходится выполнять одни и те же действия. Типичный алгоритм:
- Загрузить страницу;
- Подождать пока страница полностью прогрузится;
- Найти элемент;
- Проверить, что элемент был найден;
- Кликнуть/получить значение/вызвать событие/что-то еще;
- Перейти к шагу 1 или 3.
C#:
instance.ClearCache();
instance.ClearCookie();
var tab = instance.ActiveTab;
tab.Navigate("ya.ru");
if(tab.IsBusy)
{
tab.WaitDownloading();
}
var searchInput = tab.FindElementByXPath("//input", 0);
if(searchInput.IsVoid)
{
throw new Exception("Поле ввода запроса не найдено.");
}
searchInput.SetValue("test request", "full", false);
var findButton = tab.FindElementByXPath("//button" , 0);
if(findButton.IsVoid)
{
throw new Exception("Кнопка \"Найти\" не найдена");
}
findButton.Click();
Данный код загружает страницу Яндекса, вводит запрос в поле для поиска и нажимает кнопку "Найти".
После каждого поиска элемента сначала проверяем был ли он найден. В противном случае выбрасываем исключение с сообщением для упрощения отладки в будущем.
С этим кодом, на мой взгляд, есть несколько проблем:
- Код неудобно писать;
- Код неудобно читать;
- Код громоздкий;
- Есть однотипные куски кода, которые можно и нужно переработать;
Создаем метод расширения
Механизм методов расширения позволяет добавлять свои методы к существующим типам. Это позволяет нам сильно сократить объем кода и, как следствие, повысить читаемость и скорость разработки.
Чтобы расширить существующий тип своими методами нам нужно создать статический класс со статическими методами и первым аргументом в методе через ключевое слово this указать расширяемый тип.
Вот так мы можем добавить в системный тип string свой метод, который будет конвертировать строку в число:
C#:
public static class StringExtensions
{
public static int ToInt(this string str)
{
return int.Parse(value);
}
}
C#:
int azino = "777".ToInt();
Перерабатываем код с Яндексом
Начнем с этой части кода:
C#:
tab.Navigate("ya.ru");
if(tab.IsBusy)
{
tab.WaitDownloading();
}
Сказано – сделано.
Создаем статический класс ZennoExstensions и добавляем в него метод расширения для типа Tab. Назовем этот метод NavigateAndWait и он будет принимать в параметры адрес и необязательный параметр реферер:
C#:
public static class ZennoExstensions
{
public static Tab NavigateAndWait(this Tab tab, string url, string referer = "")
{
tab.Navigate(url, referer);
if(tab.IsBusy)
{
tab.WaitDownloading();
}
return tab;
}
}
C#:
var tab = instance.ActiveTab;
tab.NavigateAndWait("ya.ru");
Fluent Interface
Обратите внимание, что метод NavigateAndWait возвращает объект Tab. А это значит, что теперь мы можем делать цепочки вызовов таким образом:
C#:
tab.NavigateAndWait("ya.ru")
.NavigateAndWait("mail.ru")
.NavigateAndWait("zennolab.com");
В данном случае пока не слишком очевидна полезность этого паттерна, но далее вы увидите, что это очень удобная штука.
Перерабатываем поиск и проверку элементов
В примере с Яндексом у нас идет поиск 2-х элементов. Это поле ввода поискового запроса и кнопка "Найти":
C#:
var searchInput = tab.FindElementByXPath("//input", 0);
if(searchInput.IsVoid)
{
throw new Exception("Поле ввода запроса не найдено.");
}
//манипуляции с searchInput
var findButton = tab.FindElementByXPath("//button" , 0);
if(findButton.IsVoid)
{
throw new Exception("Кнопка \"Найти\" не найдена");
}
//манипуляции с findButton
Для начала сделаем метод расширения поиска элемента по XPath.
Сейчас мне не нравится, что всегда приходится передавать вторым параметром номер совпадения в методе FindElementByXPath. В моих шаблонах в 99.9% случаев это всегда будет 0. Поэтому можно сделать этот параметр необязательным, чтобы лишний раз не указывать его. Если в ваших шаблонах ситуация иная то, можно пропустить этот шаг.
Итак, добавляем в наш класс ZennoExtensions новый метод. Лучше для каждого расширяемого типа создавать отдельный класс, как на скрине. В статье я буду писать все одном классе для удобства.
Метод будет принимать 2 параметра: xpath и номер совпадения. Мы не можем назвать наш метод FindElementByXPath, т.к. сигнатуры будут полностью совпадать. Поэтому назовем наш метод GetElementByXpath:
C#:
public static HtmlElement GetElementByXpath(this Tab tab, string xpath, int number = 0)
{
return tab.FindElementByXPath(xpath, number);
}
Не отходя от кассы добавляем метод расширения для типа HtmlElement, который будет бросать исключение, если элемент не найден:
C#:
public static HtmlElement ThrowIfNull(this HtmlElement he,
string exceptionMessage = "HtmlElement не найден (IsVoid).")
{
if (he.IsVoid)
{
throw new Exception(exceptionMessage);
}
return he;
}
Метод ThrowIfNull бросает исключение с указанным сообщением, если элемент не найден. Если элемент существует, то метод возвращает его же для объединения вызовов методов в цепочки.
Теперь мы можем использовать эти методы так:
C#:
tab.GetElementByXpath("//a")
.ThrowIfNull("Элемент не найден") //Сообщение можно не указывать, т.к. мы задали значение по умолчанию
.Click();
Итого первоначальный код преобразился до такого:
C#:
instance.ClearCache();
instance.ClearCookie();
var tab = instance.ActiveTab;
tab.NavigateAndWait("ya.ru");
tab.GetElementByXpath("//input").ThrowIfNull("Поле ввода запроса не найдено.").SetValue("test request", "full", false);
tab.GetElementByXpath("//button").ThrowIfNull("Кнопка \"Найти\" не найдена").Click();
C#:
public static class ZennoExtensions
{
public static Tab NavigateAndWait(this Tab tab, string url, string referer = "")
{
tab.Navigate(url, referer);
if(tab.IsBusy)
{
tab.WaitDownloading();
}
return tab;
}
public static HtmlElement GetElementByXpath(this Tab tab, string xpath, int number = 0)
{
return tab.FindElementByXPath(xpath, number);
}
public static HtmlElement ThrowIfNull(this HtmlElement he, string exceptionMessage = "HtmlElement не найден (IsVoid).")
{
if (he.IsVoid)
{
throw new Exception(exceptionMessage);
}
return he;
}
}
Какие еще методы расширения можно сделать?
Вариантов методов расширений, которые улучшат ваш процесс разработки огромное множество.
Например, можно сделать более удобное логирование таким образом:
C#:
public static class ProjectExtensions
{
public static IZennoPosterProjectModel Debug(this IZennoPosterProjectModel project, string message, bool showInPoster = false)
{
project.SendInfoToLog(message, showInPoster);
return project;
}
public static IZennoPosterProjectModel Info(this IZennoPosterProjectModel project, string message, bool showInPoster = true)
{
project.SendInfoToLog(message, showInPoster);
return project;
}
public static IZennoPosterProjectModel Warning(this IZennoPosterProjectModel project, string message, bool showInPoster = true)
{
project.SendWarningToLog(message, showInPoster);
return project;
}
public static IZennoPosterProjectModel Error(this IZennoPosterProjectModel project, string message, bool showInPoster = true)
{
project.SendErrorToLog(message, showInPoster);
return project;
}
}
Теперь мы можем вместо методов Send***ToLog мы можем вызывать:
C#:
project.Debug("сообщение1"); //будет выведено информационное сообщение только в PM
project.Info("сообщение2"); //будет выведено информационное сообщение в PM и ZP
project.Warning("сообщение3"); //будет выведено предупреждение в PM и ZP
project.Error("сообщение4"); //будет выведена ошибка в PM и ZP
Можете также черпать идеи из документации к библиотеке ZennoExtensions.
Вот код метода WaitFor, который реализован в библиотеке:
C#:
/// <summary>
/// Выполняет ожидание, пока на странице не будет найден HTML элемент по указанному XPath, либо до истечения таймаута.
/// </summary>
/// <param name="tab"></param>
/// <param name="xpath">XPath для поиска.</param>
/// <param name="number">Номер совпадения.</param>
/// <param name="timeout">Длительность поиска в миллисекундах. По умолчанию 5000.</param>
/// <returns>Возвращает true, если элемент был найден.</returns>
public static bool WaitFor(this Tab tab, string xpath, int number = 0, int timeout = 5000)
{
if (string.IsNullOrWhiteSpace(xpath))
{
throw new ArgumentException("XPath должен быть задан.");
}
for (var i = 0; i < timeout / 100; i++)
{
if (!tab.FindElementByXPath(xpath, number).IsVoid)
{
return true;
}
Thread.Sleep(100);
}
return false;
}
/// <summary>
/// Выполняет ожидание, пока на странице не будет найден HTML элемент по указанном атрибутам, либо до истечения
/// таймаута.
/// </summary>
/// <param name="tab"></param>
/// <param name="tags">Список тегов. Если количество тегов больше одного, их необходимо разделить ";".</param>
/// <param name="attrName">Имя атрибута.</param>
/// <param name="attrValue">Значение атрибута.</param>
/// <param name="searchKind">Тип поиска. Доступно: text, notext, regex.</param>
/// <param name="number">Номер совпадения.</param>
/// <param name="timeout">Длительность поиска в миллисекундах. По умолчанию 5000.</param>
/// <returns>Тот же объект <see cref="Tab"/>для Fluent Interface</returns>
public static Tab WaitFor(this Tab tab, string tags, string attrName, string attrValue, string searchKind,
int number = 0, int timeout = 5000)
{
for (var i = 0; i < timeout / 100; i++)
{
if (!tab.FindElementByAttribute(tags, attrName, attrValue, searchKind, number).IsVoid)
{
return tab;
}
Thread.Sleep(100);
}
return tab;
}
/// <summary>
/// Выполняет ожидание, пока предикат не вернет true, либо до истечения таймаута.
/// </summary>
/// <param name="tab"></param>
/// <param name="predicate">Условное выражение.</param>
/// <param name="timeout">Длительность проверки выражения в миллисекундах. По умолчанию 5000.</param>
/// <returns>Тот же объект <see cref="Tab"/>для Fluent Interface</returns>
public static Tab WaitFor(this Tab tab, Func<bool> predicate, int timeout = 5000)
{
if (predicate == null)
{
throw new ArgumentNullException("predicate");
}
for (var i = 0; i < timeout / 100; i++)
{
if (predicate.Invoke())
{
return tab;
}
Thread.Sleep(100);
}
return tab;
}
Пример использования:
C#:
var tab = instance.ActiveTab;
var isLoginButtonFinded = tab.WaitFor("//button[@id='login']", 0, 10000); //Ожидаем появления элемента на странице 10 секунд
if(!isLoginButtonFinded)
{
tab.Refresh();
}
- Тема статьи
- Нестандартные хаки
- Номер конкурса статей
- Девятый конкурс статей
Для запуска проектов требуется программа ZennoPoster или ZennoDroid.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...
Для того чтобы запустить шаблон, откройте нужную программу. Нажмите кнопку «Добавить», и выберите файл проекта, который хотите запустить.
Подробнее о том, где и как выполняется проект.
Последнее редактирование: