Особенности инициализации элементов страниц в PageFactory

Ребята, а кто-то может объяснить вот эту “запутанную схему”, которая происходит в конструкторе родительского класса?

public BasePage()
    {
        // На самом деле, this – это будет объект дочернего класса. 
        // PageFactory умеет с ним работать со столь запутанной 
        // схемой. 
        // Убийца – садовник. Извините. 

        PageFactory.InitElements(Driver, this);
    }

Очень приятно было увидеть, что для каждой дочерней страницы не нужно объявлять в конструкторе свой PageFactory.InitElements(Driver, this);, но я никак не могу понять как эта инициализация распространяется на все дочерние классы будучи объявленной только в конструкторе родительского.

  1. Рефлексии
    PageFactory.InitElements работает со страницами через рефлексии (System.Reflection).
    По по сути, на вход PageFactory.InitElements приходит некий объект X, о котором InitElements ничего не знает. И тогда PageFactory начинает задавать вопросы

PageFactory: Слушай, X, а есть ли у тебя свойства (property), которые возвращают IwebElement или IList
X: Да, конечно есть! Вот они…
PageFactory: О, круто, а давай я их тебе проинициализирую…

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

  1. this – это всегда дочерний объект. Давайте представим следующую скучную структуру наследования… Нет… лучше не надо.
    Просто создайте два класса:

BaseAnimal и DerivedCow :wink:

в BaseAnimal добавьте метод

public void WhoIAm()
{
	Console.WriteLine(this);
}

унаследуйте DerivedCow от BaseAnimal и вызовите этот метод у DerivedCow.

DerivedCow cow = new DerivedCow();
cow.WhoIAm();

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

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

перед вызовом DerivedCow будет вызван BaseAnimal.

Любознательных людей отправлю читать CLR via C#:

А про себя скажу, что нашёл эти закономерности случайно и мне этого хватает.
Му га га га

Добавлю 1 маленький нюанс: this в данном случае всегда будет самым верхним элементом цепочки наследования. К примеру, если class A extends class B, а class B extends class C - this.getClass().getName() конструктора класса C выдаст только A при инициализации A, и B - при инициализации B.

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

Реализация PageFactory (и некоторых других функций) зависит от языка программирования :smiley:
В C#, например, будет инициализироваться вся цепочка.
Как в Java – не проверял.

Неопровержимые доказательства:

Код:

using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Support.PageObjects;

namespace ConsoleApplication1
{
    class Program
    {
        static IWebDriver Driver = new FirefoxDriver();

        public abstract class BasePage01
        {
            [FindsBy(How=How.Id, Using="close")]
            public IWebElement btnClose;

            public BasePage01()
            {
                PageFactory.InitElements(Driver, this);
            }
        }

        public class MainLayout02 : BasePage01
        {
            [FindsBy(How = How.Id, Using = "plusFont")]
            public IWebElement btnIncreaseFont;
        }

        public class LoginPage03 : MainLayout02
        {
            [FindsBy(How = How.Id, Using = "userName")]
            public IWebElement txtUserName;
        }

        public static void PrintElement(IWebElement element)
        {
            Console.WriteLine(element.ToString());
        }
        static void Main(string[] args)
        {
            LoginPage03 page = new LoginPage03();
            PrintElement(page.btnClose);
            PrintElement(page.btnIncreaseFont);
            PrintElement(page.txtUserName);
        }
    }
}

@dzhariy
Оговорюсь сразу, что я рассматриваю вопрос со стороны Java юзера. Собственно мой вопрос касался именно второго пункта Вашего ответа, но благодарю и за подробную информацию по другим пунктам.

Насчет того самого пункта №2, пример с наследованием выглядит очень логично. Так как методы являются наследуемыми, при вызове
DerivedCow cow = new DerivedCow(); cow.WhoIAm();

так как метод вызывается из-под ссылки типа DerivedCow, DerivedCow скажет, что она DerivedCow. Так как класс DerivedCow “имеет” метод WhoIAm унаследовав его из BaseAnimal.

Но вовсе другая ситуация с конструкторами. Конструкторы в Java не наследуются. Раз конструкторы не наследуются, то this в PageFactory.initElements(driver, this);, который находится в конструкторе базового класса, указывает только на базовый класс. И потому, при вызове PageFactory.initElements(driver, this); в конструкторе базового класса был очень удивлен, что элементы и на дочерних классах тоже инициализируются и не “требуя” отдельного вызова PageFactory.initElements(driver, this); в их конструкторах.

Видимо, это особая реализация PageFactory, которая позволяет “пройтись” по всей цепочке дочерних классов.

Надеюсь не запутал :slight_smile:

Нед.

В C++ и Java если производный класс не вызывает явным образом конструктор базового класса (в C++ в списке инициализации, в Java используя super() в первой строчке), то конструктор по умолчанию вызывается неявно.

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

Все ответы в реализации initElements. Мы проходим в цикле по всем суперклассам цепочки (попутно инициализируя все элементы), пока не дойдем до родителя.

  public static void initElements(FieldDecorator decorator, Object page) {
    Class<?> proxyIn = page.getClass();
    while (proxyIn != Object.class) {
      proxyFields(decorator, page, proxyIn);
      proxyIn = proxyIn.getSuperclass();
    }
  }

  private static void proxyFields(FieldDecorator decorator, Object page, Class<?> proxyIn) {
    Field[] fields = proxyIn.getDeclaredFields();
    for (Field field : fields) {
      Object value = decorator.decorate(page.getClass().getClassLoader(), field);
      if (value != null) {
        try {
          field.setAccessible(true);
          field.set(page, value);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }

Вот только эти “пляски с бубном” никак не связаны с наследованием/конструкторами/this, а продиктованы спецификой работы метода getDeclaredFields.

У Баранцева есть

<dependency>
        <groupId>ru.stqa.selenium</groupId>
        <artifactId>webdriver-factory</artifactId>
        <version>1.1.43</version>
    </dependency>

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

@heartwilltell
Это немного о другой factory, WebDriverFactory. Тут поднята тема о [PageFactory][1] :blush:
[1]: GitHub - SeleniumHQ/selenium-google-code-issue-archive: Archive, please see main selenium repo

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

Пример №1

public class A {
    public void test() {
        System.out.println(this);
    }
}

public class B extends A {
    public static void main(String[] args) {
        new B().test();
    }
}

//Output: B@<hashcode>

Здесь все ясно — метод test(), который теперь “пренадлежит” и классу B (после наследования от A) вызывается из-под ссылочной переменной B, потому на выходе и получем, что this относится к B.

Пример №2

public class A {
    A() {
        System.out.println(this);
    }
}

public class B extends A {
    public static void main(String[] args) {
        new B();
    }
}

//Output: B@<hashcode>

Здесь конструктор класса B по умолчанию вызывает родительский конструктор в классе A. Но почему при выполении инструкций в конструкторе класса A, this указывает на класс B? Это как-то связано с тем, что this указывает на runtime class? То есть тот класс, который вызывает его?

Буду благораден за ответ.

А вы объект какого класса создаете? Правильно, B. Так что тут удивительного в том, что ссылка на текущий объект будет является ссылкой класса B? Если бы вы создавали объект класса A, то this ссылался бы на A. Все логично. При этом, this всегда будет содержать в себе всю информацию о родителях: если вы в цикле из базового класса начнете вызывать конструкцию вида this.getClass().getSuperclass() (ну точнее, вам придется сохранять промежуточное состояние, this будет нужен только первый раз), то дойдете вплоть до родителя всех родителей java.lang.Object.

@ArtOfLife
Немного смущает вот такое поведение:

public class A {
    private int a = 1;

    A() {
        System.out.println(this.a + " + " + this);
    }
}

public class B extends A {
    public static void main(String[] args) {
        new A();
        new B();
    }
}

//Output:
//1 + A@<hashcode>
//1 + B@<hashcode>

Получается, что при инициализации new B() неявно вызывается конструктор родительского класса и в выражении System.out.println(this.a + " + " + this); в this.a this относится к А (так как для B приватная переменная int a не видна), а во второй части выражения this уже относится к B. Почему так?

Чтобы понять, почему так, вам нужно немного модифицировать ваш пример. Ответ кроется в понятии полиморфизма. Ввиду того, что в отличие от методов, поля не бывают виртуальными, this.a будет всегда ссылаться на филд текущего класса при подобном обращении. Но если, к примеру, вы определите такую же переменную в классе B и создадите соответствующие геттеры в обоих классах, возвращающие this.a, то при вызове геттера объекта потомка, будет напечатано значение переменной именно потомка. JVM в рантайме определит, чей метод ему вызывать по типу создаваемого объекта.

П.С. Не стоит обсуждать все эти вопросы в теме БЗ. Создайте отдельную тему.

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

И еще интересно - сделайте в классе A конструктор с параметром и увидете веселую штуку)))
Про this в том же разделе будет.

@ArtOfLife
Да, я как-раз после ответа хотел извиниться за то, что наоффтопал в теме.

С пониманием и использованием наследования и полиморфизма вроде как проблем нет, та и с теми же ключевыми словами this и super все ясно. Но пример с this заключенным в конструктор родительского класса в примерах выше — немного обескуражил. Возможно таки стоит заглянуть еще раз в учебник :slight_smile:

Еще раз спасибо всем за ответы.