https://www.pexels.com/photo/cheese-burger-161675/

MVC i warstwy aplikacji

Właśnie zdałem sobie sprawę, że to już półmetek „Daj się poznać 2017”, a ten wpis jest moim dziesiątym postem konkursowym. Całkiem nieźle, jestem już za połową realizacji postanowienia. Niestety z ręką na sercu nie mogę powiedzieć, że projekt jest zrealizowany w 50%. Niestety ciężko pogodzić pracę zawodową, w dodatku w innej branży niż IT, życie rodzinne i pracę nad „pet projects”. Ostatni tydzień szczególnie obfitował w różne niespodziewane zajęcia, które trzeba było zrobić. Niemniej jednak post na temat struktury aplikacji webowej w Play napisany.

Nie mógłbym pominąć w opisie projektu jego struktury warstwowej i wspomnieć o obecności wzorca MVC (Model – View – Controller). Może to być pewne nadużycie z mojej strony, ale na mój obecny stan wiedzy, ten wzorzec jest jednym z filarów frameworka Play.

W Sterowniku domowym znalazł się model, czyli warstwa domenowa. Klasy już istniejące w projekcie w ramach tej warstwy, to te definiujące fizyczne obiekty (sensory) w systemie, ale również opisujące obiekt reprezentujący pomiar (np. temperatury). Przykłady klas z warstwy domeny: Measurement, Room czy Sensor.

To co widać na pierwszy rzut oka uruchamiając aplikację, to warstwa prezentacji. Są to widoki, które prezentują dane użytkownikowi oraz umożliwiają jego interakcję z aplikacją. W realizowanym projekcie, za widok odpowiadają szablony z osadzonym kodem w języku Scala. Przykłady widoków w moim projekcie: dashboard.scala.html, addSensorForm.scala.html oraz alarmsList.scala.html.

Widok nie komunikuje się bezpośrednio z modelem logiki biznesowej. W tym celu wykorzystywany jest kontroler, jako ogniwo pośredniczące między nimi. Kontroler zaliczył bym również do warstwy prezentacji, gdyż zawiera metody renderujące widoki. „Współpracuje” on z plikiem routes, który zawiera definicję mapowania żądań (adresów wprowadzonych w przeglądarce) na metody kontrolerów realizujące odpowiedź na to żądania. Przykładem kontrolera w Sterowniku domowym jest SensorController realizujący żądania związane z czujnikami zainstalowanymi w systemie.

Tyle było w standardzie, czyli w projekcie domyślnym utworzonym przez wizadra z Netbeans IDE – pakiety (a w zasadzie to foldery) controllers, models oraz views. To co dodałem do projektu to nowe foldery repositories oraz services.

Zawartość pierwszego z nich to warstwa danych, która zajmuje się pobieraniem informacji ze źródła danych (u mnie MySQL), aby następnie uzupełnić obiekty modelu danych. To właśnie tu znajduje się logika realizująca operacje CRUD (Create, Read, Update, Delete). Wprowadzenie tego elementu do modelu warstwowego niesie za sobą korzyść swobodnej zmiany implementacji. Realizacja wiążę się z przygotowaniem interfejsu, który będzie implementowany przez konkretne klasy.

@ImplementedBy(InMemorySensorRepositoryImpl.class)
public interface SensorRepository {
 
    public List<Sensor> getAllSensors();
 
    public Sensor getSensorById(int id);
}

Najistotniejszym szczegółem jest adnotacja @ImplementedBy(InMemorySensorRepositoryImpl.class), która wskazuje jaka implementacja ma być wykorzystana w projekcie w momencie wstrzyknięcia zależności. Dzięki takiemu rozwiązaniu, gdy już uda mi się zintegrować Hibernate z Play, będę mógł w łatwy sposób dokonać rekonfiguracji warstwy danych.

W tej chwili moje repozytorium korzysta z ręcznie dodanych obiektów modelu danych (w konstruktorze klasy InMemorySensorRepository) do prywatnego pola List<Sensor> sensors. Realizacja metod interfejsu SensorRepository sprowadza się do pobrania elementów tej listy. W docelowej wersji metody klasy MySQLSensorRepositoryImpl, którą zastąpię InMemorySensorRepositoryImpl, będą pobierały dane z bazy danych wykorzystując m. in. obiekt klasy EntityManager.

Identyczny mechanizm zastosowałem w warstwie usług, których klasy umieszczone są w folderze services. Jednak w tym przypadku nie przewiduję zmiany implementacji w najbliższej przyszłości, choć nigdy nie wiadomo, kiedy taka możliwość może się przydać.

Na koniec warto wspomnieć w jakim celu została ta warstwa utworzona. Otóż jest to miejsce, w którym realizowane będą operacje biznesowe jako całe procesy mogące składać się z wielu operacji CRUD. Przykładem z takiej warstwy jest na pewno interfejs RoomService oraz klasa go implementująca RoomServiceImpl.

@ImplementedBy(RoomServiceImpl.class)
public interface RoomService {
 
    public List<Room> getAllRooms();
 
    public Room getRoomById(int id);
}

Na koniec pozostaje zadać pytanie – po co to wszystko? Podział aplikacji na warstwy daje przede wszystkim separację. Jeśli będę chciał zmienić wygląd strony, nie muszę kombinować ze zmianą całej reszty, bo ta ma być niezależna. Aplikacja jest przy tym prostsza w zrozumieniu i utrzymaniu, wliczając w to coś, nad czym muszę się w końcu pochylić – testowanie.

W tym wszystkim pomóc ma jeszcze jeden mechanizm – Dependency Injection (DI), czyli po naszemu wstrzykiwanie zależności. Nie czuję się jeszcze na tyle mocny, aby wchodzić w szczegóły i wykładać na ten temat. Dla mnie DI jest procesem dostarczonym przez framework (a opisanym przez specyfikację JSR 300), który wspomaga zarządzanie zależnościami w aplikacji. Plus dla mnie jest taki, że nie będę musiał ręcznie tworzyć odpowiednich obiektów, po prostu framework zrobi to za mnie. Ponadto, jak pokazałem już wyżej, w prosty sposób (bo za pomocą adnotacji) jestem w stanie szybko zmienić implementację danego interfejsu.

Co trzeba wiedzieć aby wykorzystać DI w projekcie? W pliku build.sbt powinniśmy posiadać poniższą linijkę.

routesGenerator := InjectedRoutesGenerator

Zależności wstrzykuję przez konstruktor, który opatrzony jest adnotacją @Inject. Jak widać nie występuje tu ani jedno odwołanie do faktycznej implementacji interfejsów SensorService ani RoomService.

public class SensorController extends Controller {
 
    private final SensorService sensorService;
    private final RoomService roomService;
 
    @Inject
    public SensorController(SensorService sensorService, RoomService roomService) {p
        this.sensorService = sensorService;
        this.roomService = roomService;
    }
    // (...)
}

W metodach kontrolera mogę teraz odwoływać się do metod zadeklarowanych w interfejsie SensorService np. tak: sensorService.getAllSensors().

Literatura obowiązkowa:

Dependency Injection
Amuthan G: Spring MVC. Przewodnik dla początkujących (załączam choć niezwiązana z Play, ale warta przeczytania ze względu na dobre tłumaczenie idei MVC w aplikacjach webowych)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *