Практическое обобщение



Введение
Как часто вас преследует чувство дежавю в программировании? Многие программисты не догадываются насколько важно это чувство. Оно свидетельствует о том, что пришло время обобщать классы для повторного использования кода!
Толчком, побудившим меня к написанию этой статьи, стала сегодняшняя ночь, а именно то самое чувство.

Предыстория
Несколько недель назад мне понадобилось написать простенький модуль аутентификации клиента. Я не стал использовать сложные алгоритмы безопасности и включать множество полей, таких как IP или логин пользователя, просто в этом не было никакой необходимости. Все клиенты делились на: неавторизированных пользователей и администраторов - а значит, достаточно было
использовать только два поля: идентификатор и пароль. Когда администратор регистрируется, он передает в систему пароль, на что система отвечает ему идентификатором. В будущем администратору необходимо ввести идентификатор и пароль, что позволит ему войти в систему и получить необходимые полномочия.
Архитектура, как и логика модуля, проста до невозможности, и я даже представить не мог, что ее можно еще больше упростить.

Первые шаги
И так я взялся за дело. Сначала реализовал сущность, представляющую пользователя. Все очень просто и лаконично:

<?php
calss User extends LongObject{
protected $pass;

function getPass(){...}
function setPass($pass){...}
}

Родительский класс LongObject используется здесь потому, что все его дочерние классы могут быть сохранены в БД. Этот класс и содержит идентификатор OID.
Менеджер, содержащий логику аутентификации пользователя, был несколько сложнее, но по сравнению с другими менеджерами системы, он был тривиален. Приведу его
краткий интерфейс:

<?php
interface AuthManager{
/* Метод возвращает объект User, восстановленный из сессии.
То есть, если пользователь был уже аутентифицирован,
то метод вернет его состояние*/
function identify();
// Метод выполняется при отключении администратора от системы
function closeSession();
// Метод регистрирует нового администратора, возвращая ему идентификатор
function register();
/* Метод пытается аутентифицировать клиента по полученной
от него ключевой паре - идентификатору и паролю */
function authenticate($OID, $pass);
}

Все предельно просто. За пару часов я полностью реализовал модуль, написал под него тесты (в действительности на написание тестов ушло больше времени, чем на разработку и реализацию модуля D.gif ), документацию и интерфейс. Все работало прекрасно.

Дежавю
Сегодня ночью, скачивая кое-что из известного файлового обменника, я задумался о механизме, позволяющем предоставлять клиентам файлы только по паролю. Мне стало интересно, как это проще всего реализовать, и тут ба-бах - Дежавю! Я понял, что написанный мною ранее модуль аутентификации использует тот же механизм!
Посмотрим внимательно на механизм ограничения доступа к файлам по паролю: доступ к файлу предоставляется только тогда, когда клиент предоставит как информацию о запрашиваемом файле, так и пароль к этому файлу. Модуль аутентификации работает аналогично: доступ к сессии предоставляется только тогда, когда клиент предоставит как информацию о сессии, так и пароль к ней. Замечаете связь?
В эту ночь у меня не было никаких причин переписывать модуль аутентификации, но программист во мне требовал чистоты кода! Тогда я решил прибегнуть к "Обобщению классов".

Обобщение
Для начала я выделил все то общее, что было между моим модулем и механизмом файлового обменника:
1. Идентификатор;
2. Пароль;
3. Доступ только при правильной ключевой паре (идентификатор + пароль).
Казалось бы, что загвоздка в идентификаторе, в моем случае это ID пользователя, а в случае файла, может показаться, что его физический адрес, но это не так! Достаточно вспомнить, что в ООП все является объектом, как становится ясно, что идентификатором файла может являться не только физический адрес, но и ID, как у пользователя:

<?php
class File extends LongObject{
protected $address;
...
}

То есть поиск в БД будет произведен по OID, а после восстановления файла из БД можно найти его по физическому адресу ($address).
Теперь, чтобы обобщить общее этих двух механизмов, мне достаточно использовать один-два класса, инкапсулирующих те общие части, что присутствуют в обоих механизмах. Отмечу сразу, что модуль аутентификации у меня отличается от механизма ограничения доступа к файлам тем, что он добавляет логику работы с сессией, для сохранения состояния сеанса после аутентификации пользователя.
Пришло время обобщать!

<?php
/*
Класс представляет аутентифицируемую сущность. Все дочерние классы могут быть
восстановлены из БД только если клиент предоставит правильный идентификатор и
пароль.
Так как все аутентифицируемые сущности работают с базой данных, правильным
решением было вынести уровень наследования от LongObject в данный класс.
*/
abstract class AuthenticatedEntity extends LongObject{
protected $password; // Общее для всех сущностей свойство - пароль

// Setters и Getters
...
}

Так же можно обобщить и менеджер, реализующий логику аутентификации:

<?php
class AuthenticationManager implements Singleton{
protected $db; // Интерфейс взаимодействия с БД
// Всяческие вспомогательные методы
...

/* Общий метод, позволяющий определить, верна ли ключевая пара
Если ключевая пара верна, сущность автоматически восстанавливается из БД
а метод возвращает true */
public function authenticate(AuthenticatedEntity &$entity){
try{
// Попытка восстановления сущности из БД по ключевой паре
$this->dataMapper->recoverFinding($entity, ['OID' => $OID, 'password' =>
$password]);
// Если исключений не выброшено, значит, аутентификация успешна
return true;
}
/* Выброс данного исключения свидетельствует о том, что сущность не
восстановлена, а значит, аутентификация не пройдена */
catch(UncertaintyException
$e){
return false;
}
}
}

Как видно, классы получились довольно общими, и использовать их без наследования и частных сущностей не представляется возможным, но зато они позволяют повторно использовать код. Ниже пример обновленного модуля аутентификации с использованием обобщенных классов:

<?php
/*
Наследование свидетельствует о том, что User может восстановить свое
состояние из БД только при правильной ключевой паре
*/
class User extends AuthenticatedEntity{
}

Как видно, все внутренности User наследуются от AuthenticatedEntity.
Запомните, часто класс может вообще не иметь тела, но само его наличие создает в системе новую сущность.

<?php
class SessionManager implements Singleton{
/* Класс использует обобщенный менеджер аутентификация для делегирования ему
части обязанностей */
protected $authManager;
/* Класс использует менеджер сессий для делегирования ему части обязанностей */
protected $sessionProvider;

function identify(){}

function closeSession(){}

function register(){}

function authenticate($OID, $pass){
$user = new User();
$user->setOID($OID);
$user->setPassword($password);
// Использование обобщенного менеджера аутентификации
if($this->authManager->authenticate($user)){
// Аутентификация успешна
// Запись данных в сессию
$this->sessionProvider->start();
$this->sessionProvider->set('SessionManager::OID',
$user->getOID());
return $user;
}
else{
// Аутентификация неудачна
throw new
AuthentifyException(
'Аутентификация не пройдена.', 1);
}
}
}

Как видно, интерфейс класса совершенно не поменялся, но теперь часть его логики вынесена в обобщенный менеджер аутентификации, и все схожие классы, такие как менеджер доступа к файлам, могут использовать тот же принцип без дублирования кода!

Заключение
При использовании ООП, важно помнить одно золотое правило - если вам кажется, что этот код вы уже когда то писали - настало время обобщать!

URL: http://visavi.net/blog/blog.php?act=view&id=445