Pisząc programy, często trzeba zbudować klasy, których obiekty mogą znajdować się w jednym z kilku stanów. Zadanie nie wydaje się trudne, często jednak modyfikacja kodu napisanego w typowy sposób staje się uciążliwa, powodując konieczność modyfikacji wielu metod klasy.
Wiele z nich zaczyna z czasem przybierać postać rosnącej drabinki
warunków, co utrudnia analizę kodu. Na szczęście istnieje rozwiązanie tego problemu.
Zajmijmy się prostym programem: autoryzacją
użytkowników. Użytkownicy podają login
i hasło, po czym kod ma za zadanie sprawdzić,
czy podane dane są poprawne i w zależności
od tego wykonać odpowiednią akcję. Dodatkowo,
dla utrudnienia, po trzykrotnym błędnym logowaniu
zablokujemy możliwość ponownego logowania
na 30 sekund, co powinno utrudnić ewentualną
próbę włamania.
Najprostszym rozwiązaniem problemu jest
jedna klasa z szeregiem metod:
class Autoryzacja {
const SPRAWDZANIE = 1;
const BLAD_AUTORYZACJI = 2;
const BLAD_AUTORYZACJI_3_RAZY = 3;
const AUTORYZACJA_POPRAWNA = 4;
public function sprawdz($strLogin,$strHaslo) {
//...
}
public function autoryzacjaPoprawna() {
//...
}
public function blokada() {
//...
}
//...
}
public class Autoryzacja {
final static int SPRAWDZANIE = 1;
final static int BLAD_AUTORYZACJI = 2;
final static int BLAD_AUTORYZACJI_3_RAZY = 3;
final static int AUTORYZACJA_POPRAWNA = 4;
public void sprawdz(String login, String haslo) {
//..
}
public void autoryzacjaPoprawna() {
//..
}
public void blokada() {
//..
}
//...
}
Kod więc jest dość prosty, może przyjmować
stan określony przez jedną ze stałych. Niestety
będziemy tutaj mieli do czynienia z kilkoma
metodami, z których każda będzie miała szereg
warunków i wymagała zmian w przypadku
dodawania nowego sposobu przeprowadzania
autoryzacji.
Ewentualnie całą logikę można zaszyć
w metodzie sprawdź, co spowoduje stworzenie
potężnego drzewka warunków, jeszcze trudniejszego
w pielęgnacji.
Istnieje jeszcze jeden sposób rozwiązania
problemu – skorzystanie ze wzorca projektowego
stan (ang. state).
Wzorzec stan
Używając wzorca stan, wyodrębniamy do
osobnej klasy każdy ze stanów, w jakim może
znaleźć się obiekt – tak więc stany nie będą
więcej reprezentowane przez proste stałe. Każdy
z obiektów stanu może mieć możliwość wpływania
na stan obiektu Autoryzacji, a więc poszczególne
stany powinny wiedzieć, na jaki inny stan zmodyfikować
stan obiektu w zależności od zaistniałych
okoliczności. Brzmi to skomplikowanie, ale w rzeczywistości
jest bardzo proste. Przyjrzyj się kodowi
programu.
Na samym początku należy wyodrębnić
wspólne cechy wszystkich stanów, które będą
interfejsem stanu. W naszym prosty przykładzie
interfejs jest bardzo prosty i maa jedną metodę
sprawdź:
interface Stan {
public function__construct(Autoryzacja $objAutoryzacja);
public function sprawdz($strLogin,$strHaslo);
}
public interface Stan {
public void sprawdz(String login,String haslo);
}
Sama klasa Autoryzacja powinna dostarczać
metod pozwalających na odczyt oraz zmianę aktualnego
stanu i metody (tzw. gettery), udostępniających
obiekty wszystkich stanów, w jakich może się
znaleźć dana klasa:
class Autoryzacja {
private $objStanSprawdzanie = null;
private $sprawdzobjStanBladAutoryzacji = null;
private $sprawdzobjStanBladAutoryzacji 3Razy = null;
private $objStanAutoryzacjaPoprawna = null;
private $objStan = null;
public function __construct() {
$this->objStanSprawdzanie = new SprawdzanieStan($this);
$this->sprawdzobjStanBladAutoryzacji = new BladAutoryzacjiStan($this);
$this->sprawdzobjStanBladAutoryzacji3Razy = new BladAutoryzacji3RazyStan($this);
$this->objStanAutoryzacjaPoprawna = new AutoryzacjaPoprawnaStan($this);
$this->objStan = $this->objStanSprawdzanie;
}
public function setStan(Stan $objStan) {
$this->objStan = $objStan;
}
public function getStan() {
return $this->objStan;
}
public function sprawdz($strLogin, $strHaslo) {
$this->objStan->sprawdz($strLogin, $strHaslo);
}
public function getStanSprawdzania() {
return $this->objStanSprawdzanie;
}
public function getStanBladAutoryzacji() {
return $this->sprawdzobjStanBladAutoryzacji;
}
public function getStanBladAutoryzacji3Razy() {
return $this->sprawdzobjStanBladAutoryzacji3Razy;
}
public function getStanAutoryzacjiPoprawnej() {
return $this->objStanAutoryzacjaPoprawna;
}
}
public class Autoryzacja {
private SprawdzanieStan sprawdzanieStan;
private AutoryzacjaPoprawnaStan autoryzacjaPoprawnaStan;
private BladAutoryzacjiStan bladAutoryzacjiStan;
private BladAutoryzacji3RazyStan bladAutoryzacji3RazyStan;
private Stan stan;
public Autoryzacja() {
sprawdzanieStan = new SprawdzanieStan(this);
autoryzacjaPoprawnaStan = new AutoryzacjaPoprawnaStan(this);
bladAutoryzacjiStan = new BladAutoryzacjiStan(this);
bladAutoryzacji3RazyStan = new BladAutoryzacji3RazyStan(this);
stan = sprawdzanieStan;
}
public void setStan(Stan stan) {
this.stan = stan;
}
public Stan getStan() {
return stan;
}
public void sprawdz(String login,String haslo) {
stan.sprawdz(login, haslo);
}
public Stan getStanSprawdzania() {
return sprawdzanieStan;
}
public Stan getStanAutoryzacjiPoprawnej() {
return autoryzacjaPoprawnaStan;
}
public Stan getStanBleduAutoryzacji() {
return bladAutoryzacjiStan;
}
public Stan getStanBleduAutoryzacji3Razy() {
return bladAutoryzacji3RazyStan;
}
}
Głównym stanem klasy Autoryzacja jest
SparwdzanieStan – sprawdza on login oraz hasło
i w zależności od wyniku sprawdzania uruchamia
stan:
- AutoryzacjaPoprawnaStan – gdy zarówno login,
jak i hasło są poprawne, - BladAutoryzacjiStan – gdy login lub hasło jest
błędne, - BladAutoryzacji3RazyStan – gdy trzy razy pod
rząd podano błędny login i hasło.
PHP:
Java:
Stany BladAutoryzacjiStan oraz Autoryzacja-
PoprawnaStan są bardzo proste i nie wymagają
szczegółowych wyjaśnień:
class BladAutoryzacjiStan implements Stan
{
private $objAutoryzacja = null;
public function__construct(Autoryzacja $objAutoryzacja) {
$this->objAutoryzacja = $objAutoryzacja;
}
public function sprawdz($strLogin,$strHaslo) {
echo \"Niepoprawny login lub hasło!
\";
$this->objAutoryzacja->setStan($this->objAutoryzacja->getStanSprawdzania());
}
}
class AutoryzacjaPoprawnaStan implements Stan {
public function__construct(Autoryzacja $objAutoryzacja) {
}
public function sprawdz($strLogin,$strHaslo) {
echo \"Zalogowany - witam!
\";
}
}
public class BladAutoryzacjiStan
implements Stan {
private Autoryzacja autoryzacja;
public BladAutoryzacjiStan(Autoryzacja autoryzacja) {
this.autoryzacja = autoryzacja;
}
public void sprawdz(String login, String haslo) {
System.out.println(\"Niepoprawny login lub hasło\");
autoryzacja.setStan(autoryzacja.getStanSprawdzania());
}
}
public class AutoryzacjaPoprawnaStan
implements Stan {
public AutoryzacjaPoprawnaStan(Autoryzacja autoryzacja) {
}
public void sprawdz(String login,String haslo) {
System.out.println(\"Zalogowany - witam!\");
}
}
Bardziej złożony jest stan BladAutoryzacji-
3RazyStan. Jego zadaniem jest zablokowanie
możliwości logowania na 30 sekund. Gdy stan
ten jest ustawiony, przy pierwszym wywołaniu
metody sprawdź (wywołanie to następuje
z poziomu stanu SprawdzanieStan) zainicjowana
zostaje zmienna przechowująca aktualny
czas.
Następnie stan ten staje się domyślny
na 30 sekund, nie pozwalając tym samym na
zalogowanie się. Po upłynięciu 30 sekund stan
BladAutoryzacji3RazyStan sam ustala stan klasy
Autoryzacja ponownie na stan Sparwdzanie-Stan, co pozwala na dokonanie kolejnych prób
logowania (po 3 nieudanych następuje ponowna
blokada na 30 sekund):
class BladAutoryzacji3RazyStan implements
Stan {
private $objAutoryzacja = null;
private $intCzas = null;
public function__construct(Autoryzacja $objAutoryzacja) {
$this->objAutoryzacja = $objAutoryzacja;
}
public function sprawdz($strLogin,$strHaslo) {
if ($this->intCzas === null) {
$this->intCzas = time();
}
if ((time() - $this->intCzas) >=5) {
$this->objAutoryzacja->setStan($this->objAutoryzacja->getStanSprawdzania());
$this->objAutoryzacja->getStan()->sprawdz($strLogin, $strHaslo);
$this->intCzas = null;
return;
}
echo \"Podano trzy razy nieprawidłowy login lub hasło - możliwość logowania zablokowana na 30 sekund!
\";
}
}
import java.util.*;
public class BladAutoryzacji3RazyStan implements Stan {
private Autoryzacja autoryzacja;
private Long czas = -1l;
public BladAutoryzacji3RazyStan(Autoryzacja autoryzacja) {
this.autoryzacja = autoryzacja;
}
public void sprawdz(String login,String haslo) {
GregorianCalendar gc = new GregorianCalendar();
if (czas == -1l) {
czas = gc.getTime().getTime();
}
if ((gc.getTime().getTime() - czas) >= 30000) {
autoryzacja.setStan(autoryzacja.getStanSprawdzania());
autoryzacja.getStan().sprawdz(login, haslo);
czas = -1l;
return;
}
System.out.println(\"Podano trzy razy nieprawidłowy login lub hasło - możliwość logowania zablokowana na 30 sekund!\");
}
}
W taki oto sposób wyeliminowaliśmy niepraktyczne
drzewka warunków, wyodrębniliśmy
zachowanie charakterystyczne dla każdego stanu
oraz ułatwiliśmy modyfikowanie kodu. W tej
chwili dodanie kolejnego stanu wiązało się będzie
zazwyczaj z:
- dodaniem odpowiedniego obiektu w klasie
Autoryzacja, - dodanie gettera do klasy Autoryzacja,
- wprowadzenie niewielkich modyfikacji, najczęściej
tylko w jednym ze stanów, który będzie
ustawiał nowy stan.
Na koniec
Pamiętaj, że przedstawiony tu program został napisany tak, aby był możliwie jak najbardziej prosty do zrozumienia. Hasło i login są w nim zakodowana na stałe, podczas gdy w rzeczywistości powinny być pobierane np. z bazy danych.
Znasz już kilka prostych, lecz podstawowych wzorców projektowych. Dzięki temu możesz pisać lepsze, łatwiejsze w rozwijaniu programy. Zachęcam jednocześnie do lektury kolejnych artykułów z tej serii – przed nami jeszcze długa droga.