Wyobraź sobie sytuację, w której chciałbyś, aby w twojej aplikacji można było łatwo zmienić bazę danych, z której korzysta – np. z MySQL-a na PostgreSQL-a, a w razie potrzeby także na dowolną inną. Właśnie za to będzie odpowiadał nasz nowy wzorzec projektowy.
Do problemu można podejść na wiele sposobów.
Można użyć drabinki instrukcji warunkowych
wszędzie tam, gdzie wykonywane
jest zapytanie SQL:
if ($strBaza == \'MySQL\') {
return mysql_query($strZapytanie);
} elseif ($strBaza == \'PostgreSQL\') {
return pg_query($strZapytanie);
}
Statement stmt = con.createStatement();
if (baza.equals(\'MySQL\')) {
return stmt.executeQuery(zapytanie_dla_bazy_MySQL);
} else if (baza.equals(\'PostgreSQL\')) {
return stmt.executeQuery(zapytanie_dla_bazy_PostgreSQL);
}
Jednak już na pierwszy rzut oka widać
niedoskonałości tego rozwiązania. Największą
z nich jest problem w dodaniu obsługi kolejnej
bazy – taka operacja wymaga modyfikacji
kodu w zbyt wielu miejscach, przez co łatwo
popełnić błąd.
Można także wszystkie zapytania SQL zamknąć
w osobnym pliku konfiguracyjnym – odrębnym
dla każdej bazy – tak aby jej podmiana sprowadzała
się do podmiany tego pliku. Wówczas kod,
w którym aplikacja komunikuje się z bazą, musi
korzystać z zawartości takiego pliku konfiguracyjnego.
Czyli powinno mieć to postać podobną do
naszego przykładu:
Plik XML z zapytaniami SQL:
SELECT * FROM users;
SELECT * FROM posts;
function getUsers() {
//pobranie kodu z pliku XML
return mysql_query($zapytanie_z_pliku_XML);
}
function getPosts() {
//pobranie kodu z pliku XML
return mysql_query($zapytanie_z_pliku_XML);
}
class BazaDanych {
public ResultSet getUsers() {
//pobranianie zapytania z pliku XML
Statement stmt = con.createStatement();
return stmt.executeQuery(zapytanie_z_pliku_XML);
}
public ResultSet getPosts() {
//pobranianie zapytania z pliku XML
Statement stmt = con.createStatement();
return stmt.executeQuery(zapytanie_z_pliku_XML);
}
}
Rozwiązanie to, mimo iż wydaje się idealne,
ma wiele wad. O ile w przypadku prostych zapytań
sprawdza się dość dobrze, o tyle w przypadku
bardziej złożonych zapytań, które muszą być
wygenerowane podczas działania programu,
staje się kłopotliwe lub wręcz niemożliwe do
wykorzystania. Ponadto użytkownik aplikacji ma
łatwy dostęp do pliku z zapytaniami, co może
powodować, że będzie on na własną rękę starał
się te zapytania modyfikować, co niekoniecznie
może mu się udać.
Z pomocą przychodzą nam na szczęście jak
zawsze wzorce projektowe. W naszym przypadku
idealnie sprawdzi się wzorzec fabryki.
Wzorzec fabryka
Wzorzec Fabryka pozwala na łatwą podmianę
rodziny klas (nawet w trakcie działania aplikacji).
W naszym przypadku będą to klasy odpowiedzialne
za komunikację z bazą danych (nie musi być to
koniecznie komunikacja w bazą danych, może to
być np. wygląd aplikacji lub dowolna inna rodzina
klas). Jak zawsze, zacznijmy od przykładu (pamiętaj,
że w naszych przykładach nie pokazujemy pełnych
aplikacji, a jedynie fragmenty kodu niezbędne
do zrozumienia idei działania danego wzorca):
abstract class Fabryka {
protected static $objFabryka = null;
public static function setDefault(Fabryka $objFabryka) {
self::$objFabryka = $objFabryka;
}
public static function getDefault() {
return self::$objFabryka;
}
public abstract function getUsers();
public abstract function getPostes();
}
public abstract class Fabryka {
protected static Fabryka fabryka;
public static void setDefault(Fabryka fabryka) {
Fabryka.fabryka = fabryka;
}
public static Fabryka getDefault() {
return Fabryka.fabryka;
}
public abstract Users getUsers();
public abstract Posts getPosts();
}
Jak widzisz, powyższa klasa fabryki jest
abstrakcyjna. Konkretną implementację funkcji tej
klasy dostarczą już fabryki poszczególnych grup
klas (u nas to będzie grupa klas odpowiedzialna
za komunikację z bazą MySQL czy też z bazą
PostgreSQL). Dodatkowo, jak widać po kodzie Javy,
metody getUsers oraz getPosts zwracają obiekty.
Users oraz Posts są interfejsami:
interface Users {
public function getAll();
public function getByID($intID);
}
interface Posts {
public function getAll();
public function getByID($intID);
}
import java.sql.ResultSet;
public interface Users {
public ResultSet getAll();
public ResultSet getByID(int id);
}
import java.sql.ResultSet;
public interface Posts {
public ResultSet getAll();
public ResultSet getByID(int id);
}
Dlaczego interfejsy? Po to, aby kod naszej
aplikacji, niezależnie od grupy klas, z jakich będzie
korzystał, zawsze znał interfejs tych klas, a więc
wiedział, z jakich metod może korzystać. Dzięki
użyciu interfejsów nasz kod zawsze będzie wiedział,
z jakich metod może korzystać.
Stwórzmy teraz na podstawie naszej fabryki
klasy operujące na danych w bazie MySQL. Na
początek tworzymy fabrykę:
class MySQLFabryka extends Fabryka {
public function getUsers() {
return new MySQLUsers();
}
public function getPostes() {
return new MySQLPosts();
}
}
public class MySQLFabryka extends Fabryka {
public Users getUsers() {
return new MySQLUsers();
}
public Posts getPosts() {
return new MySQLPosts();
}
}
Jak widzisz, fabryka MySQL-a dostarcza już
konkretne implementacje metod getUsers() oraz
getPosts(). Jedyne, co robią te metody, to zwracają
obiekt odpowiednich klas – MySQLUsers i MySQLPosts:
class MySQLUsers implements Users {
public function getAll() {
//pobranie wszystkich użytkowników
return $arrUzytkownicy;
}
public function getByID($intID) {
//pobranie użytkownika o danym ID
return $arrUzytkownk;
}
}
class MySQLPosts implements Posts {
public function getAll() {
//pobranie wszystkich postów
return $arrPosty;
}
public function getByID($intID) {
//pobranie postu o danym ID
return $arrPost;
}
}
import java.sql.ResultSet;
public class MySQLUsers implements Users {
public ResultSet getAll() {
//pobranie wszystkich użytkowników
return users;
}
public ResultSet getByID(int id) {
//pobranie użytkownika o danym ID
return user;
}
}
import java.sql.ResultSet;
public class MySQLPosts implements Posts {
public ResultSet getAll() {
//pobranie wszystkich postów
return posts;
}
public ResultSet getByID(int id) {
//pobranie postu o danym ID
return post;
}
}
I co jeszcze? Nic! Nasza fabryka już działa. Teraz
wystarczy ustalić domyślną fabrykę:
Fabryka::setDefault(new MySQLFabryka());
Fabryka.setDefault(new MySQLFabryka());
Pobieramy obiekt domyślnej fabryki:
$objFabryka = Fabryka::getDefault();
Fabryka fabryka = Fabryka.getDefault();
Korzystając z obiektu fabryki, tworzymy obiekty
klas komunikujących się z bazą danych:
$objUsers = $objFabryka->getUsers();
$objUsers->getAll();
Users users = fabryka.getUsers();
ResultSet = users.getAll();
W jaki sposób dodać teraz obsługę nowej bazy
danych? Na pewno już to wiesz: należy utworzyć
nową klasę fabryki dla danej bazy (np. PostgreSQLFabryka),
dziedziczącą po abstrakcyjnej fabryce.
Następnie trzeba zbudować klasy obiektów komunikujących
się z bazą i implementujących nasze
interfejsy (a więc na przykład PostgreSQLUsers
i PostgreSQLPosts). Na koniec pozostaje już tylko
zmodyfikować domyślną fabrykę, zmieniając:
Fabryka::setDefault(new MySQLFabryka());
Fabryka.setDefault(new MySQLFabryka());
na
Fabryka::setDefault(newPostgreSQLFabryka());
Fabryka.setDefault(newPostgreSQLFabryka());
Podsumowanie
Fabryka jest popularnym i bardzo pomocnym
wzorcem projektowym. Z powodzeniem wykorzystywana
jest w celu zaprezentowanym wyżej
– a więc obsługi baz danych – ale nie tylko.
Możliwości zastosowania fabryki jest bardzo wiele.
Przeanalizuj ten wzorzec dokładnie, zastanów się
też nad tym, gdzie mógłbyś go wykorzystać.