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



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

Предыстория
Несколько недель назад мне понадобилось написать простенький модуль аутентификации клиента. Я не стал использовать сложные алгоритмы безопасности и включать множество полей, таких как 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);
}

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

Обобщение
Для начала я выделил все то общее, что было между моим модулем и механизмом файлового обменника:
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