Архитектура модели. ActiveRecord, обертка и их взаимодействие.

Cellard

Новичок
Господа, прошу вашей помощи в реализации правильной архитектуры модели.

Пусть есть класс, реализующий модель ActiveRecord. Его задача — читать и писать в таблицу базы данных, без каких-либо изысков.

Покажу только методы, которые важны для повествования:

PHP:
class Entity extends arEntity {
  public function __construct($id) {}
  public function delete() {}
}

$o = new Entity(3); // Запрашиваем из базы запись к ключом 3
$o->delete(); // И удаляем ее
Допустим, мы хотим хранить в этой таблице дерево. Для того, чтобы расширить модель, мы обернем ActiveRecord классом NodeExt, в котором и будут реализованы «древесные» методы.

PHP:
class Entity extends arEntity implements iNode {
  public function Node() 
  {
    return new NodeExt($this);
  }
}

$o = new Entity(3); // Запрашиваем из базы запись к ключом 3
$o->Node()->getParent(); // И получаем ее родителя
Дерево реализовано на Nested Sets, а это значит, например, что добавление (и удаление и т.д.) записи происходит в несколько запросов. Рассмотрим удаление.

После того, как запись будет удалена, мы должны будем обновить какие-то другие записи в таблице, отталкиваясь от свойств удаленной записи (а именно от правого и левого ключей, но это не столь важно).

Важно то, что мы хотим сделать это наиболее прозрачным для программиста способом, то есть ничего внешне не меняя:

PHP:
$o = new Entity(3); // Запрашиваем из базы запись к ключом 3
$o->delete(); // Удаляем ее и перестраиваем дерево
Как этого можно добиться?

Ну, конечно же, в классе NodeExt у нас есть метод afterDelete, который все эти дополнительные запросы выполнит, но кто его вызовет?

Можно научить класс Entity видеть свой интерфейс iNode и дергать из метода delete этот специальный метод класса NodeExt — но это противоречит конституции — отвергаем.

Нужно доверить программисту переопределение метода delete, но как нам заставить программиста это сделать?

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

WMix

герр M:)ller
Партнер клуба
дерево это набор строк (не Entity), в наборе и имплементируй!
 

WMix

герр M:)ller
Партнер клуба
создать новый класс, наследуемый не от Entity. он должен представлять набор связаных строк Entities.
в этом классе и происходит магия по выборке основной ветки и ее детей, удаление ветки и ее детей, подкрутка дерева после удаления
 

Cellard

Новичок
Вы предлагаете создать, так скажем, «параллельный» класс для работы с таблицей? Один класс — это представление сущности в модели, а второй — для работы с деревом? Вы предлагаете не внедрять логику дерева в модель, потому что дерево — это не сущность как таковая? Я правильно понял?
 

hell0w0rd

Продвинутый новичок
Вы предлагаете не внедрять логику дерева в модель, потому что дерево — это не сущность как таковая? Я правильно понял?
потому что дерево - это дерево, а строки в базе - это строки в базе.
Я бы еще посоветовал ActiveRecord выбросить
 

Cellard

Новичок
Напомню, что это теоретические выкладки. На практике используем orm, но это несущественно для обсуждаемого предмета.

потому что дерево - это дерево, а строки в базе - это строки в базе.
А удаление объекта ($o->delete()) — это, в конечном итоге, удаление строки из базы, а то и не одной. И если у объекта расширена модель, а он, объект, об этом не знает, то такой вызов может перемолоть данные в таблице. И как прикажете защищаться от программиста, который может забыть сделать вызов на том, а не на этом классе? Получается, что это потенциально опасная для данных архитектура приложения?
 

WMix

герр M:)ller
Партнер клуба
($o->delete()) — это, в конечном итоге, удаление строки из базы,
да а возможно и целого набора строк (детей)

отбрось дерево, как ты получаешь вырезку из таблицы состоящию к примеру из 5 записей?, как ты удаляешь одновременно 3 записи из этой таблички?
 

Cellard

Новичок
Для работы с сущностью у нас есть два класса: один — это представление некоего множества записей; второй — это представление одной записи.

Объект первого класса можно итерировать, итерация порождает объекты второго класса.

Вот удаление n строк:

PHP:
$set = new Collection();
$set->SQL()->null('column');
$set->delete();
 

WMix

герр M:)ller
Партнер клуба
ну вот, дерево это больше "некоего множествао записей" чем одна запись!
 

Cellard

Новичок
И кто же, по вашему, должен делать эту выборку? Я, раз за разом, от проекта к проекту, от контроллера к контроллеру, чего доброго? Или все таки можно реализовать это стандартное поведение однажды, положить куда-нибудь глубоко-глубоко в абстрактный уровень фреймворка, и продолжать вызывать Item::delete(), совершенно не вдаваясь в подробности, сколько и каких именно записей он удалит?

Мне кажется я что-то упускаю в ваших словах... Вы только что сказали мне очевидную банальность, а значит подводите меня к какому-то простому выводу...
 

WMix

герр M:)ller
Партнер клуба
давай подумаем, что происходит если пользователь отдал команды:
1. хочу дерево 2х уровней с корнем 145
2. хочу удалить ветку 42 со всеми детьми

начнем с первого пункта.
 

Cellard

Новичок
Если позволите, то начну с псевдокода, описывающего модель
PHP:
/* Это наши абстрактные классы, в которых реализованы методы работы с базой данных */
abstract class abstractItem {
  public function delete()
  {
    /* Удаляет запись */
  }
}
abstract class abstractCollection{}

/* Это уже реальные классы, они работают с конкретной таблицей; для каждой таблицы мы создаем пару таких */
class Item extends abstractItem implements iNode {
  protected $table = 'table_name';
  public function Node()
  {
    return new NodeExt($this);
  }
}
class Collection extends abstractCollection {}

class NodeExt {
  public function __constract($Item) { /* Сохраним ссылку на расширяемый класс */ }
  /**
   * @param integer $levels количество «поколений» потомков
   */
  public function getChildren($levels = null)
  {
    /* Создадим коллекцию, парную для $Item */
    /* Ограничим множество sql-запросом */
    /* Вернем коллекцию */
  }
  public function delete()
  {
    /* Удаляет узел и потомков */
    /* Перестраивает ключи */
  }
}
Теперь к вашим вопросам.
1.
PHP:
$N = new Item(145);
$SubTree = $N->Node()->getChildren(2);
Создаем представление записи с ключом 145.
Через Node(), который расширяет стандартную модель, запрашиваем потомков узла. Для NS-дерева это займет один запрос. Результат возвращаем объектом типа Collection (о котором я упоминал ранее).

2.
PHP:
$Suspect = new Item(42);
$Suspect->delete();
Вот тут, собственно, и кроется беда. Метод abstractItem::delete() просто удаляет запись из таблицы; он ничего не знает о том, что у него есть потомок с интерфейсом iNode, и что его нужно удалять по «особому». Если программист переопределит метод Item::delete(), то все будет хорошо:
PHP:
class Item extends abstractItem implements iNode {
  protected $table = 'table_name';
  public function Node()
  {
    return new NodeExt($this);
  }
  public function delete()
  {
    $this->Node()->delete();
  }
}
Но архитектура не может проконтролировать программиста, переопределил ли он метод delete, и что он в нем написал, а значит беда не заставит себя ждать.


И вот изначальный мой вопрос как раз об этом. Что можно сделать в данной конкретной ситуации? Есть ли стандартные решения?
 

yanis

Новичок
Пусть программист переопределяет какй-нибудь delete_item(), а сам delete() вызывающую нужную магию и delete_item в том числе, не трогает.
 

AmdY

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


И вот изначальный мой вопрос как раз об этом. Что можно сделать в данной конкретной ситуации? Есть ли стандартные решения?
final
 

Cellard

Новичок
class TreeItem extends Item
Не подходит, потому что оберток может быть много, и каждая из них может захотеть внести свой вклад в метод delete(). Пусть будут обертки Tree, File, Joy. Если наследовать, как вы предлагаете, то будет так:
PHP:
class TreeItem extends Item {}
class FileItem extends Item {}
class JoyItem extends Item {}
class TreeFileItem extends Item {}
class TreeJoyItem extends Item {}
class FileJoyItem extends Item {}
class TreeFileJoyItem extends Item {}
Вы бы стали такой моделью пользоваться? ;)

Я почему-то ненавижу слово «item»
Терпите, у нас тут вообще сферический код.

Пусть программист переопределяет какй-нибудь delete_item(), а сам delete() вызывающую нужную магию и delete_item в том числе, не трогает.
А как архитектура заставит программиста переопределить метод delete_item?

Кажется, вы упустили суть рассуждений. Мы не хотим запретить переопределять метод, нам наоборот, нужно, чтобы программист его обязательно переопределил, а такого в ООП нет.


Давайте, чтобы предвосхитить возможные скоропалительные советы, обозначим следующее.

Задача может быть решена, если Item начнет выбрасывать события, а обертки будут поставлять обработчиков событий (hell0w0rd показал пальцем на Doctrine). Формально это не нарушает парадигмы ООП, но это костыль.

Может быть следует полностью пересмотреть архитектуру модели? Возможно ли в принципе построение такой расширяемой модели, у которой меняется поведение, но не меняется (почти) интерфейс?
 
Сверху