Zasada Otwarty/Zamknięty w zarządzaniu WebDriverem

Zasada Otwarty/Zamknięty z angielskiego Open/Closed Principle

Jest to druga zasada, pięciu zasad SOLID . Została ona zamknięta w jednym zdaniu, które oryginalnie brzmi:

„software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification".

W wolnym tłumaczeniu:

„elementy oprogramowania (takie jak klasy, moduły, funkcje itp.) powinny być otwarte na rozszerzenia, ale zamknięte na modyfikację".

Zasada mówi, że raz stworzony kod, nie powinien ulegać modyfikacji. Wszelkie zmiany powinny odbywać się przez dokładanie nowych elementów, nie zaś przez modyfikację starych.

Powstaje więc pytanie -- jak tworzyć kod który, jest otwarty na rozszerzenia, ale zamknięty na modyfikację? Odpowiedzią na to pytania z reguły jest abstrakcja.

Zasada OCP wymusza wykorzystywanie abstrakcji . Przedstawiając zasadę Open/Close Principle inaczej, moglibyśmy powiedzieć, że powinniśmy tworzyć kod, w taki sposób aby:

  • Elementy wspólne -- zamykać w ramach interfejsu lub klasy abstrakcyjnej
  • Elementy specyficzne -- zamykać w ramach konkretnych klas, które wykorzystują abstrakcję

Przejdźmy w takim razie do przykładu.

Przykład łamiący zasadę OCP

Kod poniżej prezentuje, prostą fabrykę (wzorzec fabryka), wyboru WebDrivera w zależności od podanej wartości browserType:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class DriverFactory {

    private String browserType;

    public DriverFactory(String browserType) {
        this.browserType = browserType;
    }

    public WebDriver getBrowser() {
        switch (browserType) {
            case "chrome":
                System.setProperty("webdriver.chrome.driver", "C:/drivers/chromedriver.exe");
                return new ChromeDriver();
            case "firefox":
                System.setProperty("webdriver.gecko.driver", "C:/drivers/geckodriver.exe");
                return new FirefoxDriver();
            default:
                throw new IllegalStateException("Unknown browser type! Please check your configuration");
        }

    }
}

Jeżeli teraz chcielibyśmy dodać wsparcie dla kolejnej przeglądarki, jaką jest na przykład Internet Explorer, lub bardziej spersonalizować ustawienie na przykład Chroma, musielibyśmy zmodyfikować kod metody getBrowser() . Krótko mówiąc klasa DriverFactory nie spełnia zasady OCP.

Przejdźmy zatem do poprawnej implementacji.

Lepsza implementacja klasy DriverFactory

Z wstępu teoretycznego wiemy, że powinniśmy wykorzystać abstrakcję. Pierwszym elementem, który musimy wykonać jest wyciągnięcie elementu wspólnego, do interfejsu lub klasy abstrakcyjnej. Elementem wspólnym jest oczywiście metoda getDriver() , która dostarcza nam WebDrivera. I tak tworzymy interfejs DriverFactory:

import org.openqa.selenium.WebDriver;

public interface DriverFactory {

    WebDriver getDriver();
}

Mając interfejs DriverFactory , tworzymy klasy, które kolejno go implementują. I tak mamy klasę ChromeBrowser:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class ChromeBrowser implements DriverFactory {

    @Override
    public WebDriver getDriver() {
        System.setProperty("webdriver.chrome.driver", "C:/drivers/chromedriver.exe");
        return new ChromeDriver();
    }
}

Klasę FireFoxBrowser:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class FireFireBrowser implements DriverFactory {

    @Override
    public WebDriver getDriver() {
        System.setProperty("webdriver.gecko.driver", "C:/drivers/geckodriver.exe");
        return new FirefoxDriver();
    }
}

Klasę InternetExplorerDriver:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;

public class InternetExplorerBrowser implements DriverFactory {

    @Override
    public WebDriver getDriver() {
        System.setProperty("webdriver.ie.driver", "C:/drivers/IEDriverServer.exe");
        return new InternetExplorerDriver();
    }
}

Mając gotową abstrakcję musimy utworzyć klasę, która będzie nam dostarczać odpowiednią implementację, w zależności od parametru. W tym celu wykorzystamy prostą metodę fabrykującą (prosta fabryka), tak jak poprzednio. I tak mamy klasę DriverFactoryProvider:

import example.solid.o.good.DriverFactory;

public class DriverFactoryProvider {

    public static DriverFactory getDriverFactory(String browserType) {
        switch (browserType) {
            case "chrome":
                new ChromeBrowser();
            case "firefox":
                new FireFireBrowser();
            case "internetexplorer":
                new FireFireBrowser();
            default:
                throw new IllegalStateException("Unknown browser type! Please check your configuration");
        }

    }
}

Implementacje możemy wykorzystać w następujący sposób:

DriverFactory driverFactory = DriverFactoryProvider.getDriverFactory("chrome");
driver = driverFactory.getDriver();

Analiza po implementacji

Analizując nowo utworzony kod widzimy, że jesteśmy w stanie dodawać kolejne typy przeglądarki, przez dołożenie kolejnych klas, które implementują interfejs DriverFactory . Dzięki temu wspieramy zasadę OCP . Niestety, dokładając kolejny typ przeglądarki, musimy zmodyfikować klasę DriverFactoryProvider , która wykorzystuje wzorzec prostej fabryki. Powstaje więc pytanie czy wzorzec prostej fabryki łamię zasadę OCP? Tutaj zdania są podzielone, jak zawsze programowanie to kwestia kompromisu, umiejętności, wiedzy i wyznawanych zasad.

Zakładając, że DriverFactoryProvider jest użyciem wzorca i nie łamie zasady**OCP,**nasza implementacja się kończy.

Ale jak w takim razie wyglądała by implementacja klasy DriverFactoryProvider , zakładając, że klasa DriverFactoryProvider łamie zasadę OCP?

Odpowiedzią może być refleksja!

Kurs Selenium

Użycie refleksji dla DriverFactoryProvider

Stosując refleksje możemy zmodyfikować klasę DriverFactoryProvide , w taki sposób aby zasada OCP, nie była załamana.

Do implementacji wykorzystamy bibliotekę Reflections:

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.11</version>
</dependency>

Refaktoryzując klasę DriverFactoryProvider mamy:

import org.reflections.Reflections;

import java.util.Set;

public class DriverFactoryProvider {

    public static DriverFactory getDriverFactory(String browserType) {

        //Pobieramy nazwę pakietu w którym znajduje się klasa DriverFactory
        String driverFactoryPackage = DriverFactory.class.getPackage().getName();

        //Tworzymy obiekt klasy Reflections, który posłuży nam do ustalenia odpowiedniej implementacji interfejsu DriverFactory
        // Do implementacji zaliczamy ChromeBrowser, InternetExplorerBrowser, FireFireBrowser
        // W konstruktorze klasy Reflections podajemy nazwę pakietu, w którym znajduje się interfejs DriverFactory oraz wszystkie implementacje tej klasy
        Reflections reflections = new Reflections(driverFactoryPackage);

        //Biblioteka Reflections przeszukuje pakiet, w którym znajduje się interfejs DriverFactory oraz wszystkie implementacje tej klasy
        //Następnie zwraca wszystkie klasy, które są typu DriverFactory
        // Wyniki są w postacie Setu
        Set<Class<? extends DriverFactory>> driverFactories = reflections.getSubTypesOf(DriverFactory.class);

        //Na otrzymanym Set-cie, szukamy klasy, która w nazwie zawiera nazwę przeglądarki, a następnie zwracamy ją.
        Class<? extends DriverFactory> driverClass = driverFactories
                .stream()
                .filter(driver -> driver.getName().toLowerCase().contains(browserType.toLowerCase()))
                .findFirst()

                // Jeśli żadna z klas nie posiada w nazwie szukanego wyrażenia zostanie rzucony wyjątek
                .orElseThrow(() -> new IllegalStateException("Did not find driver class with name " + browserType));

        //Posiadając odpowiednią klasę, wyciągamy z niej nazwę oraz pakiet, w której się znajduję np. example.solid.o.ChromeBrowser i przekazujemy do zmiennej
        String driverClassName = driverClass.getName();

        try {
            //Dla wskazanej nazwy klasy tworzymy nową instancję klasy DriverFactory, dzięki metodzie newInstance()
            return (DriverFactory) Class.forName(driverClassName).newInstance();

            // Metoda forName() rzuca szereg wyjątków, które obsługujemy poniżej
        } catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) {
            throw new IllegalStateException(e);
        }

    }
}

Dzięki nowej implementacji klasy DriverFactoryProvidermożemy bez przeszkód dokładać kolejne implementację przeglądarek nie modyfikując kodu, który wybiera nam specyficzną implementację.

Podsumowanie

Jak widzimy, dzięki zastosowaniu zasady otwarty-zamkniętysprawiamy, że tworzony kod jest mniej podatny na zmiany, oraz bardziej re-używalny.

Dygresja - Page Object Pattern a Open/Close Principle

Na sam koniec powstaje jeszcze pytanie czy Page Object Pattern wspiera zasadę OCP ? Odpowiedź na to pytanie jest oczywiście, nie! Wzorzec POP łamię zasadę OCP.

Jak w takim razie poradzić sobie z tym problemem?

Tego wszystkiego dowiesz się w kolejnych odsłonach na javastart.pl

Potrzebujesz więcej informacji?

Artykuł powstał na bazie kursu Automatyzacja testów z wykorzystaniem Selenium!

O Autorze

Nazywam się Mateusz Ciołek i od 2011 roku zajmuję się testowaniem oprogramowania ze specjalizacją w automatyzacji testów.

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.