Dlaczego programowanie „szybko” nie zawsze oznacza „dobrze”?

Wpis jest dedykowany programistom oraz innym osobom związany z tworzeniem oprogramowania: analitykom, projektantom, architektom i testerom.

Wstęp

Dla osób nie będących programistami sposób implementacji żądanej funkcjonalności nie ma w zasadzie żadnego znaczenia – ma działać i koniec. Jednak od tego jak to będzie zrobione, wynika wiele ważnych i nieoczywistych konsekwencji. I niestety, oczekiwanie czegoś „na szybko” (ostatnio coraz częściej określane jako „zwinnie”), kończy się właśnie nieoczywistymi i na ogół przykrymi konsekwencjami.

Postaram się to przedstawić na prostym przykładzie: wysyłanie e-maili z aplikacji, na przykład potwierdzenie złożenia zamówienia w sklepie internetowym. Podobne problemy dotyczą integracji z każdym zewnętrznym systemem, z którym musi komunikować się nasza aplikacja: system magazynowy, system płatności kartami, wysyłanie SMS’ów, itp.

Na wstępie zacznę łagodnie, od języka programowania przystępnego już dla ambitnych gimnazjalistów…

Przykład 1: PHP, wersja najprostsza – poziom „newbie”

<?php
mail('mberkan@e-point.pl', 'Potwierdzenie zamówienia' , 
     'Twoje zamówienie zostało przyjęte');
echo "OK";
?>

Czy to zadziała? Tak. Czy spełnia wymaganie funkcjonalne? Tak. Czy jest dobrze oprogramowane? Nie. Dlaczego? Bo tak wysłany email:

  1. Nie ma poprawnie ustawionego nadawcy.
  2. Ma niepoprawnie zakodowane polskie litery w treści.

Przykład 2: PHP, z nadawcą i poprawnym kodowaniem

Jak można napisać to lepiej?

<?php
$headers = "From: sklep@sklep.pl\r\n"
	    ."Content-type: text/plain; charset=UTF-8";
mail('mberkan@e-point.pl', 'Potwierdzenie zamówienia' , 
     'Twoje zamówienie zostało przyjęte', $headers);
echo "OK";
?>

Czy działa lepiej? Owszem:

Czy jest dobrze oprogramowane? Nie. Dlaczego? Ponieważ nie jest obsługiwana sytuacja gdy funkcja mail zwróci błąd. To dosyć często spotykana praktyka w prostych językach programowania i nawet oficjalna dokumentacja PHP w pierwszych przykładach użycia funkcji mail tego nie rekomenduje.

Przykład 3: PHP, z obsługą błędów

W miarę poprawnie napisany kod PHP powinien wyglądać tak:

<?php
$headers = "From: sklep@sklep.pl\r\n"
	."Content-type: text/plain; charset=UTF-8";
$ok = mail('mberkan@e-point.pl', 'Potwierdzenie zamówienia' , 
           'Twoje zamówienie zostało przyjęte', $headers);
if($ok) {
    echo "OK";
} else {
    echo "Błąd wysłania e-maila";
}
?>

Czy jest dobrze oprogramowane? W zasadzie tak, ale nie do końca. Dlaczego? Ponieważ funkcja „mail” korzysta z systemowego wysyłania maili, co niekoniecznie jest poprawne, bo:

  1. Nasz sklep może mieć swój serwer mailowy na zupełnie innej maszynie.
  2. Wysyłanie maili systemowych na serwerze produkcyjnym może być wyłączone lub ich dostarczanie zablokowane. Ponieważ aplikacja nie autoryzuje dostępu to można byłoby wysyłać spam.
  3. W środowiskach developerskich, szczególnie w systemach nie-unix’owych może takiej funkcji w ogóle nie być.
  4. Funkcja mail praktycznie nigdy nie zwraca wyjątku, nawet jeżeli podamy niepoprawny technicznie adres e-mail, np. „AAA”.

W tej sytuacji należałoby wysyłać e-maile bezpośrednio do serwera SMTP, co w PHP jest możliwe z użyciem dodatkowego pakietu PEAR Mail. Jednak, aby kontynuować przykłady zmienię język programowania na bardziej „Enterprise”, czyli Javę, i pokaże, że tam też można popełniać wiele błędów.

Przykład 4: Servlet Java, wersja najprostsza – poziom „easy”

protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws IOException {
    String recipient = "mberkan@e-point.pl"; 
    try {
        Properties p = System.getProperties();
        p.setProperty("mail.smtp.host", "mail.e-point.pl");
        Session session = Session.getDefaultInstance(p);
        MimeMessage m = new MimeMessage(s);
        m.setFrom(new InternetAddress("sklep@sklep.pl"));
        m.addRecipient(RecipientType.TO, 
             new InternetAddress(recipient));
        m.setSubject("Potwierdzenie zamówienia");
        m.setText("Twoje zamówienie zostało przyjęte");
        Transport.send(m);
    } catch(Exception e) {
    } finally {
        resp.getWriter().write("OK");
    }
}

Czy to zadziała? Tak. Czy spełnia wymaganie funkcjonalne? Tak. Czy jest dobrze oprogramowane? Nie. Dlaczego? Ponieważ:

  1. Jeżeli któraś z wywoływanych metod zwróci błąd, to mail się nie wyśle a użytkownik i tak zobaczy wynik „OK” – niepoprawna polityka „łap wszystko i udawaj że działa”. Przyczyn może być tu bardzo wiele, np. niepoprawny adres e-mail odbiorcy czy niedostępność serwera SMTP.
  2. Adres serwera SMTP jest zdefiniowany na stałe i aplikacja zarówno w środowisku developerskim jak i produkcyjnym będzie próbować korzystać z tego samego serwera SMTP.
  3. Brak logowania zdarzeń – nie ma żadnego śladu, że aplikacja wysłała e-mail i do kogo.

Przykład 5: Servlet Java, z obsługą wyjątku, sesją z JNDI i logowaniem zdarzeń

Poprawiona wersja pobiera z serwera aplikacyjnego przez JNDI obiekt sesji e-mail z poprawnymi namiarami na serwer SMTP. Dzięki temu konfiguracja może być inna dla środowiska produkcyjnego i inna dla stacji developerskiej. Ponadto dodane zostało logowanie zdarzeń: rozpoczęcia wywołania funkcji, poprawnego jej zakończenia oraz wystąpienia błędu podczas wysyłania. Dodatkowo, przy wystąpieniu błędu, ustawiany jest właściwy kod HTTP odpowiedzi (500 – wewnętrzny błąd serwera) oraz generowany losowy kod błędu, który jest prezentowany użytkownikowi i może przez niego być zgłoszony do obsługi sklepu. Gdy zgłoszenie trafi do administratora, ten na podstawie kodu błędu może znaleźć szczegóły w logach aplikacji.

protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws IOException {
    
    String recipient = "mberkan@e-point.pl"; 
    try {
        log.debug("Wysłanie e-maila do {}", recipient);
        Context c = new InitialContext(new Hashtable<Object, Object>());
        Session s = (Session) c.lookup(
                "java:comp/env/mail/defaultMailSession");
        MimeMessage m = new MimeMessage(s);
        m.setFrom(new InternetAddress("sklep@sklep.pl"));
        m.addRecipient(RecipientType.TO, new InternetAddress(recipient));
        m.setSubject("Potwierdzenie zamówienia");
        m.setText("Twoje zamówienie zostało przyjęte");
        Transport.send(m);
        log.info("E-mail poprawnie wysłany do {} ", recipient);
    } catch(Exception e) {
        String errorCode = RandomStringUtils.randomAlphanumeric(16);
        log.error("Błąd wysyłania maila, kod: " + errorCode, e);
        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        resp.getWriter().write("Blad aplikacji, kod: " + errorCode);
    } finally {
        resp.getWriter().write("OK");
    }
}

W przypadku gdy jako adres odbiorcy podamy np. napis „AAA”, to po odświeżeniu strony zamiast napisu „OK” zaprezentowany będzie:

Blad aplikacji, kod: kUPRX72FP7pIZ758OK

Natomiast w logach aplikacji znajdziemy związane z tym samym kodem szczegóły:

Błąd wysyłania maila, kod: eMwp8L6WcQG8QxVW
javax.mail.SendFailedException: Invalid Addresses
	at com.sun.mail.smtp.SMTPTransport.rcptTo(SMTPTransport.java:1196)
	at com.sun.mail.smtp.SMTPTransport.sendMessage(SMTPTransport.java:584)
	at javax.mail.Transport.send0(Transport.java:169)
	at javax.mail.Transport.send(Transport.java:98)

Czy taka aplikacja jest już gotowa? W zasadzie tak, ale nie do końca. W tym momencie odchodzimy od problemów poprawności samego kodu, w kierunku kwestii utrzymania: wydajności i niezawodności, czyli świata „enterprise”. I lojalnie ostrzegam: przestanie być łatwo i przystępnie :(.

Problem: wydajność i niezawodność

Wyobraźmy sobie następującą sytuację: używany przez aplikacje serwer SMTP ma „gorszy dzień” i zamiast wysyłać e-maila i zwracać sterowanie do aplikacji w ciągu milisekund, jak robi to normalnie, robi to np. 2 minuty. Stwarza to następujące problemy:

  1. Klient czeka na załadowanie strony widząc cały czas poprzedni lub pusty ekran. Jeżeli jest bystry, to zauważy migającą ikonkę oznaczającą ładowanie strony i być może poczeka na wynik. Jeżeli nie, to może uznać że coś się zepsuło i będzie klikać ponownie w przycisk „Wyślij” albo zacznie przeładowywać naszą stronę. Ostatecznie może opuścić nasz sklep.
  2. W serwerze, wątek obsługujący żądanie klienta czeka na odpowiedź z serwera SMTP, przez to nie może przetwarzać kolejnych żądań. Jeżeli maksymalna pula wątków wynosi np. 100 a nasza aplikacja wysyła więcej niż 100 maili w ciągu minuty, to szybko dojdzie do wyczerpania tejże puli, tzn. wszystkie wątki będą czekały na odpowiedź od serwera SMTP i nie pozostanie żaden wolny wątek do obsługi kolejnych żądań. Oznacza to w praktyce, że nie da się załadować strony głównej naszego sklepu.

Najprostszym i obowiązkowym sposobem minimalizowania szansy na wystąpienie takiego problemu jest ustawienie maksymalnego czasu jaki aplikacja będzie czekać na odpowiedź od serwera SMTP (tzw. timeout). Definiuje się ją przy pomocy dwóch flag, a wartość określa się w milisekundach:

mail.smtp.connectiontimeout=10000
mail.smtp.timeout=10000

Powyższe zapisy oznaczają czas oczekiwania 10 sekund. To gdzie się je umieszcza zależy od sposobu konfiguracji sesji mailowej. Inaczej się to robi jeżeli jest tworzona z kodu aplikacji, a inaczej jeżeli jest pobierana z zasobów serwera aplikacyjnego.

Rozwiązanie to minimalizuje wystąpienie problemu, ale nie rozwiązuje go całkowicie. W szczególności może okazać się że wysłanie e-maila musi trwać dłużej niż 10 sekund i jest to sytuacja poprawna. Łatwiej to zrozumieć, gdy pomyślimy że nie chodzi o proste wysłanie maila, tylko np. sprawdzenie dostępności produktu w centrach magazynowych zlokalizowanych w każdym z 17 województw – to musi trochę potrwać.

Przykład 6: Przetwarzanie asynchroniczne przez kolejkę JMS – poziom „advanced”

Żeby rozwiązać opisany wyżej problem stosujemy następujące rozwiązanie: zamiast fizycznie wysyłać e-maila z wątku użytkownika, tworzymy komunikat „wyślij e-mail” i wkładamy go do kolejki JMS, a użytkownikowi zwracamy komunikat „OK”. Tym samym kończymy aktywny wątek. Inny wątek, z osobnej puli, wyjmuje komunikat z kolejki JMS i wysyła e-mail do serwera SMTP. Ilość wątków w każdej puli możemy oddzielnie konfigurować, więc możemy ustalić że pula wątków wielkości 100 obsługuje żądania użytkowników (jak dotychczas), a pula wątków wielkości „5” wysyła e-maile do serwera SMTP. Więc jeżeli serwer SMTP zacznie wolno odpowiadać, to te 5 wątków się zablokuje i powolutku z kolejki będą pobierać kolejne maile do wysłania, nie blokując przy tym interfejsu użytkownika. Oczywiście wielkość kolejki można monitorować i w przypadku takiego spiętrzenia przystąpić do sprawdzania przyczyn niskiej wydajności.

Kod naszego servleta został podzielony na dwie części: jedna metoda obsługuje zapytanie jak wcześniej, a druga to metoda putMessageToJmsQueue, która wrzuca komunikat do kolejki JMS. Niestety jest ona nieco przydługa, więc taki podział zwiększy czytelność. Ponadto żeby przekazać treść i nadawcę maila została stworzona pomocnicza klasa Email która jest prostym kontenerem na zmienne. Kod servleta wygląda po zmianie tak:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws IOException {
    String recipient = "mberkan@e-point.pl"; 
    try {
        log.debug("Wstawienie do kolejki e-maila do {} ", recipient);
        Email e = new Email(new InternetAddress("sklep@sklep.pl"), 
                new InternetAddress(recipient), 
                "Potwierdzenie zamówienia", 
                "Twoje zamówienie zostało przyjęte");
        putMessageToJmsQueue(e);
        log.info("E-mail do {} wstawiony do kolejki", recipient);
        resp.getWriter().write("OK");
    } catch(Exception e) {
        String errorCode = RandomStringUtils.randomAlphanumeric(16);
        log.error("Błąd wstawienia do kolejki, kod: " + errorCode, e);
        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        resp.getWriter().write("Blad aplikacji, kod: " + errorCode);
    } 
}
    
private void putMessageToJmsQueue(Email email) throws Exception {
    Context ctx = new InitialContext(new Hashtable<Object, Object>());
    ConnectionFactory cf = (ConnectionFactory)ctx.lookup(
            "java:comp/env/jms/QCF");
    Destination d = (Destination)ctx.lookup(
            "java:comp/env/jms/emailQueue");
    Connection c = cf.createConnection();
    Session s = c.createSession(true, Session.SESSION_TRANSACTED);
    MessageProducer p = s.createProducer(d);
    ObjectMessage r = s.createObjectMessage(email);
    c.start();
    p.send(r);
}

Z kolei kod aplikacji (Message Driven Bean) pobierający komunikat z kolejki i wysyłający email do serwera SMTP zawiera w praktyce to, co zawierał wcześniej servlet i wygląda tak:

public void onMessage(Message jmsMessage) {
    try {
        log.debug("Pobrana wiadomość z kolejki");
        Email e = (Email)((ObjectMessage)jmsMessage).getObject();
        log.debug("Wysłanie e-maila z potwierdzeniem do {} ", 
                e.getRecipient().getAddress());
        Context c = new InitialContext(new Hashtable<Object, Object>());
        Session s = (Session) c.lookup(
                "java:comp/env/mail/defaultMailSession");
        MimeMessage m = new MimeMessage(s);
        m.setFrom(e.getSender());
        m.addRecipient(RecipientType.TO, e.getRecipient());
        m.setSubject(e.getSubject());
        m.setText(e.getContent());
        Transport.send(m);
        log.info("E-mail poprawnie wysłany do {} ", 
                e.getRecipient().getAddress());
    } catch (Throwable e) {
        String errorCode = RandomStringUtils.randomAlphanumeric(16);
        log.error("Błąd wysyłania maila, kod: " + errorCode, e);
        throw new RuntimeException(e);
    }
}

Wszystko wydaje się pięknie działać – z jednej strony wątki użytkownika wsadzają dane e-maili do wysłania, z drugiej strony wątki pobierają te dane z kolejki i we własnym tempie wysyłają do serwera SMTP.

Czy to już koniec? Niestety nie – powyższe rozwiązanie ma zasadniczą wadę: nie informuje użytkownika czy wysłanie e-maila zakończyło się sukcesem czy wystąpił błąd.

Przykład 7: Przetwarzanie asynchroniczne z prezentacją wyniku dla użytkownika – poziom „professional”

Z wątku obsługującego pobieranie danych z kolejki i wysyłanie do serwera JMS nie mamy prostej możliwości powiadomienia użytkownika o tym czy wysłanie maila się powiodło czy nie. Jako pośrednika musimy użyć bazy danych. Scenariusz zdarzeń się komplikuje, więc najwygodniej będzie go wytłumaczyć na schemacie.

Schemat asynchronicznego wysyłania e-maila i sprawdzania statusu

Schemat asynchronicznego wysyłania e-maila i sprawdzania statusu

Przetwarzanie przebiega w następujących krokach:

  1. Użytkownik, klikając w przeglądarce przycisk, wysyła zamówienie do servleta.
  2. Servlet zapisuje do bazy danych rekord ze statusem „Przetwarzanie”.
  3. Baza danych generuje i dodaje do rekordu unikalny identyfikator dla e-maila i zwraca do servleta, który zapisuje go w sesji.
  4. Servlet wstawia obiekt e-maila do kolejki (jak dotychczas) ale z unikalnym identyfikatorem.
  5. Servlet zwraca sterowanie do przeglądarki wysyłając stronę pt. „Oczekiwanie”, która najczęściej prezentuje animowany obrazek oznaczający, że użytkownik powinien czekać:

ajax-loader

Od tego momentu przetwarzanie przebiega w dwóch wątkach:

Wątek „A” – użytkownika Wątek „B” – wysyłka e-maila
  1. „A” Kod JavaScript umieszczony na stronie użytkownika w kilkusekundowych odstępach wykonuje zapytanie AJAX-em do serveta.
  2. „A” Servlet sprawdza status wysyłki w bazie danych na podstawie zapamiętanego identyfikatora w sesji i zwraca go do przeglądarki.
  3. „A” Przeglądarka sprawdza otrzymany status i podejmuje jedną z akcji:
    • status „przetwarza” → jeżeli nie upłynął maksymalny czas oczekiwania, to za parę sekund ponawia zapytanie. Jeżeli upłynął zakładany czas oczekiwania, to wyświetla informacje o przekroczeniu czasu i ewentualną sugestie żeby sprawdzić wynik później,
    • status „ok” → wyświetla potwierdzenie wysłania,
    • status „błąd” → wyświetla informacje o wystąpieniu błędu wysłania,
    • brak możliwości skomunikowania się z serwerem → wyświetla informacje o błędzie komunikacji i ewentualną sugestie żeby sprawdzić wynik później.
  1. „B” Aplikacja otrzymuje obiekt e-maila z kolejki i wysyła żądanie do serwera SMTP.
  2. „B” Serwer SMTP potwierdza wysłanie e-maila lub zgłasza błąd.
  3. „B” Aplikacja zapisuje otrzymany status do bazy danych, tak aby mógł go odczytać servlet.

Jak bardzo skomplikuje się kod naszej aplikacji? Dyplomatycznie można odpowiedzieć że „trochę” :-).

Zawartość kodu pobierającego e-mail z kolejki i wysyłającego serwera SMTP zmieniła się tylko o zapisywanie statusu:

public void onMessage(Message jmsMessage) {
    try {
        log.debug("Pobrana wiadomość z kolejki");
        Email e = (Email)((ObjectMessage)jmsMessage).getObject();
        
        log.debug("Wysłanie e-maila z potwierdzeniem do {} ", 
                e.getRecipient().getAddress());
        Context c = new InitialContext(new Hashtable<Object, Object>());
        Session s = (Session) c.lookup(
                "java:comp/env/mail/defaultMailSession");
        MimeMessage m = new MimeMessage(s);
        m.setFrom(e.getSender());
        m.addRecipient(RecipientType.TO, e.getRecipient());
        m.setSubject(e.getSubject());
        m.setText(e.getContent());
        Transport.send(m);
        log.info("E-mail poprawnie wysłany do {} ", 
                e.getRecipient().getAddress());
        EmailSending es = esDAO.getByPK(e.getEmailSendingId());
        es.setStatus(EmailSendingStatus.OK.toString());
        esDAO.update(es);
    } catch (Throwable e) {
        String errorCode = RandomStringUtils.randomAlphanumeric(16);
        log.error("Błąd wysyłania maila, kod: " + errorCode, e);
        EmailSending es = esDAO.getByPK(e.getEmailSendingId());
        es.setStatus(EmailSendingStatus.ERROR.toString());
        es.setErrorCode(errorCode);
        esDAO.update(es);
    }
}

Servlet wkładający przyjmujący pierwsze żądanie zmienił się tylko o:

  • zapisywanie statusu do bazy,
  • przesłanie identyfikatora statusu w obiekcie e-maila,
  • wyświetlenie strony oczekiwania zamiast statusu OK.

Pominąłem implementacje metody putMessageToJmsQueue bo nie uległa ona zmianie:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws IOException {
    
    String recipient = "mberkan@e-point.pl"; 
    
    EmailSending es = new EmailSending(
            EmailSendingStatus.PROCESSING.toString());
    esDAO.insert(es);
    req.getSession().setAttribute(SENDING_ID, es.getId());
    
    try {
        log.debug("Wstawienie do kolejki e-maila do {} ", recipient);
        Email e = new Email(new InternetAddress("sklep@sklep.pl"), 
                new InternetAddress(recipient), 
                "Potwierdzenie zamówienia", 
                "Twoje zamówienie zostało przyjęte",
                e.getId());
        putMessageToJmsQueue(e);
        log.info("E-mail do {} wstawiony do kolejki", recipient);
        resp.getWriter().write(getWaitingPageContent());
    } catch(Exception e) {
        String errorCode = RandomStringUtils.randomAlphanumeric(16);
        log.error("Błąd wstawienia do kolejki, kod: " + errorCode, e);
        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        resp.getWriter().write("Blad aplikacji, kod: " + errorCode);
    } 
}

Do tego dochodzi nam drugi servlet sprawdzający status wysyłki:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws IOException {
    try {
        log.debug("Sprawdzenie statusu wysyłki e-maila");
        Integer id = (Integer)req.getSession().getAttribute(ID_KEY);
        assert id != null : "No id";
        EmailSending es = emailSendingDAO.getByPK(id);
        assert es != null : "No emailSending with id " + es;
        log.info("Status wysyłki e-maila: {}", es.getStatus());
        resp.getWriter().write(es.getStatus());
    } catch(Throwable e) {
        String code = RandomStringUtils.randomAlphanumeric(16);
        log.error("Błąd sprawdzania statusu, kod: " + code, e);
        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        resp.getWriter().write("Blad aplikacji, kod: " + code);
    } 
}

… oraz kod JS wysyłający żądania AJAX’em ze strony użytkownika do powyższego servleta:

  $package.checkStart = function() {
      setTimeout($package.checkStatusStatus, 2000);
  },

  $package.checkStatusCallback = function(data) {
	  $package.attempt++;
      if (data.status == 'PROCESSING') {
    	  if($package.attempt >= 60) {
    		  alert('Timeout');
    	  } else {
	          $package.checkStart();
	      }
      } else if (data.status == 'OK') {
          alert('OK');
      } else if (data.status == 'ERROR') {
    	  alert('ERROR');
      } else {
          console.log("Unknown status: " + data.status);
      }
  },

Tak oprogramowana funkcjonalność powinna działać bezbłędnie, być odporna na niedostępność systemu zewnętrznego, zapisywać zdarzenia do logów oraz informować użytkownika o statusie wysyłania e-maila.
Podsumowanie

Jeżeli porównamy ilość kodu z przykładu pierwszego, czyli jedną linijkę kodu PHP, do przykładu ostatniego, który dyplomatycznie pomija urok JEE polegający na konfiguracji servletów, datesource’ów, kolejek, EJB, itp., to i tak widać dramatyczną różnicę. Jednak znaczną część tego kodu może być przeniesiona do klas abstrakcyjnych, z których będą korzystać klasy specyficzne obsługujące konkretne przypadki integracyjne. Innymi słowy, jeżeli w aplikacji mamy kilka miejsc wymagających wzorca asynchronicznego (popularnie zwanego „kręciołkiem” od zachowania ikonki oczekiwania), to większość z powyższego kodu będzie wspólna, a poszczególne przypadki użycia będzie się już implementować szybko.

Trzeba również pamiętać, że powyższy wzorzec to nie tylko dodatkowy wysiłek dla programisty. Również projektanci muszą pamiętać, że zamiast jednej strony potwierdzenia należy zaprojektować aż cztery strony:

  1. stronę oczekiwania („kręciołka”),
  2. stronę potwierdzenia,
  3. stronę błędu wysyłania e-maila,
  4. stronę przekroczenia czasu na odpowiedź (timeout).

Do tego dochodzi jeszcze obsługa sytuacji braku możliwości nawiązania komunikacji z serwerem przez JavaScript.

Pytanie czy warto się w to bawić? Odpowiedź ma charakter czysto polityczny: czy wolimy zrobić tańszy system ale nieodporny na błędy wynikające z dużego obciążenia, czy ważność systemu i koszt utraconych zysków z powodu potencjalnych awarii uzasadnia programowanie takich zabezpieczeń.

Witamy w świecie aplikacji klasy „enterprise” :-).

O autorze

Marek Berkan Marek Berkan: programista, entuzjasta tworzenia oprogramowania, zarządzania zespołami technicznymi. Prywatnie motocyklista, kolarz MTB, biegacz, żeglarz, rekreacyjny wspinacz, zamiłowany turysta. Witryny: , , .

2 komentarze do “Dlaczego programowanie „szybko” nie zawsze oznacza „dobrze”?

  1. Norbert Pabiś

    Piękne przybliżenie enterprise’a 🙂 Nic ująć, a dodać jeszcze można że warto adres nadawcy ustawić na zgodny z MX domeny z której pochodzi adres nadawcy lub zadbać o odpowiednie rekordy SFP aby nasze maile nie były odrzucane. Ewentualne zwrotki przychodzące na adres nadawcy powinny nie trafiać w próżnię lecz do serwera który jest w stanie je odebrać. Aplikacja może zrobić użytek z tych informacji np. przez usunięcie nieaktywnego odbiory (klienta) lub zaalarmować dział obsługi klienta aby podjęli próbę skontaktowania się inną drogą.
    A jako wisienka na czubku tego tortu, można zadbać o odpowiednie tempo wysyłki. W dobie koncentracji adresów mailowych, może się zdarzyć że w naszym sklepie będzie 100000 adresów od jednego dostawcy. Próba wysyłki takiej ilości maili w krótkim czasie może sprawić że dostawca odmówi dalszego przyjmowania maili od naszego serwera pocztowego uznając nas za spamerów.

    Odpowiedz
  2. Przemek P

    Świetne ukazanie problemy począwszy od „czubka góry lodowej”. Artykuł bardzo wciąga.
    Wkradły się chyba jakieś zawirowania do przykładowego kodu JS.

    Odpowiedz

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *