Wzorzec architektoniczny MVC
Projektowanie i programowanie systemów internetowych I
wykład 6 z 15

Collegium Witelona
mgr inż. Krzysztof Rewak
Zakład Informatyki, Wydział Nauk Technicznych i Ekonomicznych
Collegium Witelona Uczelnia Państwowa
Blumilk sp. z o.o.
Agenda

  1. Wzorce architektoniczne i MVC
  2. Model
  3. Widok
  4. Kontroler
  5. Podsumowanie
Wzorce architektoniczne i MVC
Czym są wzorce?

Świat inżynierii oprogramowania wyróżnia dwa typy wzorców: wzorce projektowe oraz wzorce architektoniczne.
Wzorce projektowe tyczą się pojedynczych elementów programu i o nich szerzej porozmawiamy w przyszłym semestrze. Wzorce architektoniczne natomiast obejmują najczęściej cały system informatyczny - stąd też mowa o architekturze aplikacji.
Na szczęście czasy, gdy każdy system informatyczny od strony kodu wyglądał całkowicie inaczej, słusznie minęły. Po latach pracy metodą prób i błędów, wśród inżynierów wykiełkowały tzw. dobre praktyki oraz właśnie wzorce.
To nie jest tak, że są to konkretne zasady, których nie można łamać. Zarówno dobre praktyki programistyczne, jak i wzorce projektowe czy architektoniczne to sugestie oraz szablony, które dobrze znać i które dobrze umieć identyfikować i używać.
Architektura systemu informatycznego

W takim kontekście wzorzec architektoniczny to opisany sposób rozwiązania problemów stojących przed projektowanym systemem informatycznym.
Wzorce architektoniczne będą sugerowały stosowanie konkretnego nazewnictwa, relacji między zaprojektowanymi klasami czy też ich organizację. Nie będą to natomiast żadnego rodzaju biblioteki czy kawałki kodu.
Wzorce to idee.
MVC

Jednym z popularniejszych wzorców architektonicznych stosowanych przy systemach internetowych jest Model-View-Controller (MVC). Implementuje go w jakimś stopniu większość frameworków webowych, więc warto znać jego założenia.
Tutaj główną ideą jest podział systemu na trzy główne warstwy: wartswę tzw. logiki aplikacji, warstwę interfejsu oraz warstwę komunikacji.
Schemat przepływu request-response w implementacji wzorca MVC
schemat przepływu request-response w implementacji wzorca MVC
Model
Odpowiedzialność modelu

Model to element warstwy logiki biznesowej. Do zakresu jego obowiązków należy obsługa procesów oraz danych związanych z domeną projektową.
Model powinien odpowiadać na pytania

W idealnym projekcie informatycznym model jest tak zaprojektowany, że jego wykorzystanie można czytać prawie jak prozę (wrócimy do tego w VI semestrze podczas omawiania Czystego kodu Roberta Martina).
Przykładowo, w najgorszym scenariuszu, źle zbudowana aplikacja PHP obsługuje wszystko za pomocą nienazwanych, nieopisanych tablic asocjacyjnych. Czyli: request jest magiczną tablicą, z której wyciąga się jakieś dane, następnie wywołuje na przykład zapytanie bazodanowe, którego wynik znowu jest tablicą; a na koniec robi się echo $data i tak naprawdę tylko autor wie co się tam dzieje.
$request = [...$_GET, ...$_POST];
$slug = $request["slug"] ?? $request[0] ?? throw new Exception();
$query = new Query("SELECT * FROM articles WHERE slug = ?", $slug);
$results = $query->run();

return $results[0] ?? throw new Exception();
Czyż nie lepiej byłoby móc przeczytać to "po angielsku"?
$request = Request::createFromGlobals();
$slug = $request->getOrFail("slug");

$article = (new ArticlesQuery())->where("slug", $slug)->firstOrFail();

return $article;
I czyż nie lepiej byłoby jako wynik takiej funkcji otrzymać coś konkretnego?
array(9) {
  ["id"]=>
  string(36) "51c868ac-d352-4bdb-b380-08e8aac5f48d"
  ["title"]=>
  string(11) "Lorem ipsum"
  ["slug"]=>
  string(11) "lorem-ipsum"
  ["content"]=>
  string(418) "Lorem ipsum dolor sit amet,
               consectetur adipiscing elit.
               Quisque fermentum vel sem
               vitae eleifend. Aliquam in
               posuere eros, eget imperdiet
               tellus. Quisque imperdiet ligula
                in ullamcorper facilisis. Ut in
               justo pellentesque, ultricies arcu
               id, commodo ex. Suspendisse bibendum
               urna erat, ac molestie nibh congue
               ac. Pellentesque in enim neque.
               Phasellus metus sapien, consectetur
               a fermentum sit amet, aliquam
               eu velit."
  ["author_id"]=>
  string(36) "dc7cdfc7-e050-4287-8652-00210e089249"
  ["status"]=>
  int(3)
  ["created_at"]=>
  string(19) "2024-04-08 17:00:00"
  ["updated_at"]=>
  string(19) "2024-04-08 17:00:00"
  ["deleted_at"]=>
  NULL
}
object(Article)#1 (5) {
  ["id":protected]=>
  string(36) "51c868ac-d352-4bdb-b380-08e8aac5f48d"
  ["title":protected]=>
  string(11) "Lorem ipsum"
  ["slug":protected]=>
  string(11) "lorem-ipsum"
  ["author":protected]=>
  object(Author)#2 (2) {
    ["id":protected]=>
    string(36) "dc7cdfc7-e050-4287-8652-00210e089249"
    ["fullName":protected]=>
    string(14) "Matthew Stover"
  }
  ["status":protected]=>
  enum(Status::Active)
}
Z innej strony: czasami nawet proste akcje mogą składać się z kilkunastu kroków.
class UserRegistrar
{
    public UserRegistrar(Hasher hasher, EventDispatcher dispatcher, CacheManager cache)

    public User register(String email, String password) throws UserAlreadyExistsException
    {
        User user = new User()
        user.setName(email)
        user.setPassword(hasher.argon(password))

        user.createOrFail()
        user.notify(new WelcomeMessage(), Channel.Email)

        dispatcher.send(new UserRegistered())
        cache.refresh(Cache.UserStatistics | Cache.RegisterStatistics)
    }
}
              
Więc korzystanie z dobrych praktyk programistycznych naprawdę ułatwia pracę. Wyobraźmy sobie jak taki proces rejestracji wyglądałby w spaghetti pomieszany z elementami UI czy obsługą protokołu HTTP.
Do rozważenia odnośnie modelu

  • klasy modelu najlepiej konstruować tak, aby dało się ich używać wielokrotnie w wielu miejscach oraz testować
  • hermetyzacja, dziedziczenie, interfejsy, abstrakcja - naprawdę pomagają przy modelowaniu świata rzeczywistego
  • model powinien być całkowicie odseparowany od tego, co się dzieje na frontendzie
Widok
Odpowiedzialność widoku

Widok to element warstwy UI, na którą będą składać się wartswy prezentacji oraz interakcji. Powinien zostać wypełniony danymi pochodzącymi z modelu.
Klasyczne aplikacje webowe vs SPA

W klasycznym podejściu do budowania aplikacji internetowych często można było spotkać się z silnikami renderującymi na podstawie szablonów. Tak działają Twig, Jinja, Smarty, Blade, Razor i wiele innych. Idea jest prosta: w plikach szablonu można korzystać ze specjalnej składni "ulepszonego" HTML-a.
Obecnie często warstwa widoku jest budowana poza główną aplikacją, a więc wykorzystywane są rozwiązania takie jak React czy Vue.js. Wówczas widokiem w kontekście MVC jest... odpowiedź zawierająca JSON?
Tak może wyglądać przykładowy plik szablonu w Django:
{% load static %}
{% extends 'base.html' %}
{% block title %} Lista użytkowników {% endblock %}

{% block content %}
    <table class="table-auto w-full mt-4">
        <thead>
            <tr>
                <th class="px-4 py-2">Imię i nazwisko</th>
                <th class="px-4 py-2">Email</th>
                <th class="px-4 py-2">Akcje</th>
            </tr>
        </thead>
        <tbody>
            {% for user in users %}
                <tr>
                    <td class="border px-4 py-2">{{ user.full_name }}</td>
                    <td class="border px-4 py-2">{{ user.email }}</td>
                    <td class="border px-4 py-2 text-white font-bold ">
                        <button class="bg-blue-500 hover:bg-blue-700 py-2 px-4 rounded">Edytuj</button>
                        <button class="bg-red-500 hover:bg-red-700 py-2 px-4 rounded">Usuń</button>
                    </td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock %}
              
Silniki szablonów

Najczęściej w silnikach szablonów można skorzystać z przypisywania zmiennych, instrukcji warunkowych, pętli czy własnych funkcji silnika.
Niektóre systemy (m. in. laravelowy Blade) pozwalają na dużo więcej, ale warto mieć na uwadze, że korzystanie z mechanizmów modelu wewnątrz widoku zdecydowanie łamie zasadę separacji warstw wedle MVC. Zresztą niewinnie wyglądające {{ $user->posts()->count() }} w Blade uruchomi... zapytanie bazodanowe prosto z widoku. Ała!
Co przekazywać?

Stąd też dobrze jest sobie rozplanować, co chcemy tak naprawdę przyjąć w widoku. Czasami w bazie danych mamy bardzo dużo informacji, ale niektórych nie chcemy, a jeszcze innych nie powinniśmy przekazywać dalej.
Na przykład źle zorganizowane przekazanie listy użytkowników do tabelki z użytkownikami zwróci ich... hasła? a może listy postów? a może coś jeszcze więcej?
Rozwiązaniem może być wysyłanie do widoku jedynie głupich obiektów, czyli DTO albo Value Objectów.
Do rozważenia odnośnie widoku

  • widok spełnia dwie role: prezentacji danych oraz interakcji
  • prezentacja tych samych danych dla różnych użytkowników może wyglądać całkowicie inaczej
  • interakcje mogą przebiegać w dowolny sposób i lepiej nie zakładać synchronicznego flow
Kontroler
Odpowiedzialność kontrolera

Kontroler to element warstwy komunikacji. Do jego obowiązków należy przyjęcie żądania i zwrócenie odpowiedzi, a realizuje to poprzez wywołanie modelu oraz wypełnienie widoku.
Cienki i gruby kontroler

Istnieją dwie szkoły tworzenia kontrolerów. Cienki kontroler to taki, który jest bardzo krótki i jedynie deleguje akcje do modelu. Gruby kontroler jest zazwyczaj nieco bardziej skomplikowany. Najczęściej wybór między doma podejściami zależy od preferencji lidera lub ogółu programistów w zespole.
Gruby kontroler

class ContactController extends Controller
{
    public JsonResponse send(Request request)
    {
        if(!request.getContext().getUser()) {
            return (new JsonResponse()).withStatus(401)
        }

        RequestValidator validator = new RequestValidator
        validator.setRule("email", "required|min:3|email")
        validator.setRule("author", "required|min:3")
        validator.setRule("message", "required|min:20")

        ValidatorStatus status = validator.validate(request)

        if(!status.passed()) {
            return (new JsonResponse()).withStatus(422).withMessage(status.getMessage())
        }

        ContactMessageSender sender = new ContactMessageSender()
        sender.send(request.get("email"), request.get("author"), request.get("message"))

        User administrator = (new UserQuery()).where("admin", true).first()
        administrator.notify("new message has been sent")

        return (new JsonResponse()).withStatus(200)
    }
}
Cienki kontroler

class ContactController extends Controller
{
    public JsonResponse send(ContactMessageRequest request, ContactMessageSender sender)
    {
        sender.send(request.get("email"), request.get("author"), request.get("message"))

        return (new JsonResponse()).withStatus(200)
    }
}
Jeżeli dany framework jest wyposażony w ORM typu Active Record (o tym na wykładzie #9), niektórych kusić może bezpośrednie wywoływanie zapytań bazodanowych w kontrolerach.
class PageController extends Controller
{
    public function __invoke(Request $request, string $slug): JsonResponse
    {
        $page = Page::query()
            ->where("slug", $slug)
            ->where("is_active", true)
            ->firstOrFail();

        return response()->json($page);
    }
}
Czy to źle? Ponownie zależy od standardów i upodobań zespołu. W małym projekcie nie powinno to być problemem. W dużym? To już zależy od konwencji i preferencji.
Do rozważenia odnośnie kontrolera

  • obiekty Request i Response nie powinny wychodzić poza warstwę komunikacji; zarówno do modelu, jak i widoku, powinny być przesyłane jak najprostsze i najbardziej konkretne dane
  • wiele rzeczy można usunąć z kontrolera na poziomie dobrze zaprojektowanego routingu, stosowania middlewarów, wydzielonej walidacji żądań czy obsługi zdarzeń i wyjątków
  • większość webowych frameworków MVC ma jeszcze inny entrypoint do aplikacji: polecenia z poziomu CLI; warto to mieć na uwadze projektując kontrolery
Podsumowanie
Schemat przepływu request-response w implementacji wzorca MVC
schemat przepływu request-response w implementacji wzorca MVC
Highlights

  • wzorce projektowe to ogólne opisy rozwiązań problemów, a nie konkretne implementacje
  • MVC to nie jedyny, choć jeden z popularniejszych wzorców projektowych w systemach internetowych
  • czasami może się okazać, że MVC wystąpi w połączeniu z innymi wzorcami
  • rozdział warstw w MVC pozwala na budowanie lepszego, łatwiej zarządzanego i bardziej skalowalnego kodu
  • dobrze znać możliwości języka w kontekście obiektowości, aby poprawnie projektować model
Źródła i do dalszego poczytania

Dziękuję za uwagę