За последние 24 часа нас посетил 22701 программист и 1005 роботов. Сейчас ищут 700 программистов ...

Как лучше спроектировать архитектуру классов

Тема в разделе "Прочие вопросы по PHP", создана пользователем Mr. T, 10 май 2012.

  1. Mr. T

    Mr. T Активный пользователь

    С нами с:
    10 ноя 2010
    Сообщения:
    733
    Симпатии:
    0
    Адрес:
    Украина, г. Киев
    Здравствуйте!

    Представьте себе веб-интерфейс, который предоставляет возможность изменять порядок блоков на веб-странице, а также CSS. Веб-интерфейс реализован с помощью JavaScript, а изменения на веб-сервере происходят через AJAX-запросы.

    Если заглянуть в код на веб-сервере, то можно увидеть некий строитель (Builder), который является абстрактным базовым классом. Последний расширяют два дочерних класса Css и Html, соответственно. Также нужно учесть обработку ошибок (валидация данных, пришедших на веб-сервер, проверка файлов и т.п.) и логирование.

    Как лучше спроектировать архитектуру классов для решения описанной выше задачи?
     
  2. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Иными словами, нужен прототип движка или уже что-то установлено? Если ваш Builder является генератором разметки, то это лишь вспомогательный юнит. Нужен блок, который возьмёт на себя все рутинные операции. Используется ли какой-то фреймворк (Zend, Yii, Nette и т.п.)?

    Как бы это могло быть сделано (на примере значительно модифицированного хенд-мейд движка, если принцип знаком - буду рад любой инфе о его "природном" развитии [словно объявление о пропаже котёнка даю, в какого кота он там превратился]):

    Контроллер в index.php (Engine::getInstance()->launch()) запускает движок, которому передаются все URN-запросы к сайту (если это не существующий файл или директория).

    Engine анализирует первый параметр пути, затем в зависимости от ассоциации активирует контроллер (FrontEnd, BackEnd, ...).

    Контроллер разбивает путь запроса на составляющие, а дальнейшие действия зависят от типа контроллера.

    FrontEnd (внешка):
    1. Ищет в базе страницу.
    2. Заполняет контентные блоки ("статический текст", который может быть замещён модулем).
    3. Получает список подключенных к странице модулей.
    4. Опрашивает модули по схеме "Так, хлопцы! Кто из вас содержит метаописание исполняемого метода для этого пути?"
    5. Читает метаописание и активирует модуль. Если модуль не найден, просто отдаёт страницу.
    6. Возвращённый методом модуля массив сливается с массивом блоков.
    7. Массив передаётся рендеру странцы (в Вашем случае - потомкам Builder).

    BackEnd (админка): повторяет действия FrontEnd, начиная с пункта 4, только для всех модулей.

    Метаописание модуля берётся из статического метода get[Front/Back]Actions по принципу

    Код (Text):
    1.  
    2. # В оригинальном движке - extends AbstractModule
    3. #
    4. class MyModule extends [Front]Module { # Front определяет возможность подключения к страницам, иначе системный
    5.  
    6.     public static function getSummary() { # в оригинале - function info
    7.  
    8.         return [
    9.  
    10.             'title' => 'Показательный модуль'
    11.         ];
    12.     }
    13.  
    14.     public static function getBackActions() { # в оригинале - одна function menu на оба -end
    15.  
    16.         // action path starts from page:
    17.         // URN: /mypage/json/move-block-to/?bi=1641802&t=35891
    18.         //
    19.         // The 'json/move-block-to' part is the command to
    20.         // produce some action within a specified classpack.
    21.         //
    22.         return [
    23.  
    24.             'json/move-block-to' => [
    25.  
    26.                 'type' => Engine::ITYPE_JSON,
    27.                 'key' => 'page master', # Rule resolution which user must have to produce the action.
    28.  
    29.                 // 'class' => __CLASS__, # Means current class, if isn't specified.
    30.                 'method' => 'moveBlockTo'
    31.             ],
    32.         ];
    33.     }
    34.  
    35.     // Can be used only when a module has its instance (started).
    36.     //
    37.     // Implements a 'move block' operation which
    38.     // are coming with AJAX-request to /mypage/json/move-block-to/...
    39.     //
    40.     public function moveBlockTo() {
    41.  
    42.         $jsAnswer = [];
    43.  
    44.         // SOME MOVING LOGIC HERE.
    45.         // self::$db->query('SELECT ... '
    46.  
    47.         return $jsAnswer;
    48.     }
    49. }
    Метод moveBlockTo возвращает массив блоков, который в зависимости от Engine::ITYPE_[JSON/PLAIN/TEMPLATED] передаётся либо функции json_encode, либо рендеру, несколько похожему на Zend_View (ассоциативные пары 'ключ-имя-секции' => 'контент').

    В примере выше страница без подключенных модулей и кэша формируется за 0.015 сек. С модулями - в зависимости от бизнес-логики (взаимодействие с базой, расчёты и т.д.).

    Если говорить о среде, в которой система работет и тестируется, то проект неспешно приобретает статус хорошо нагруженного - более 200 тыс. модульно-производных страниц (заполняются другими отделами на специализированном ПО) и около 500 основных, собранных дизайнерами в контрольной панельке, растущая активность посетителей.

    Всё, что внутри moveBlockTo, выполняется как логически завершённый блок кода бизнес-логики. Могут использоваться компоненты фреймворков.

    После разбора основ, т.н. "фундамента", можно приступать к валидации. Логирование какого типа? Системное о сбоях - обеспечивается фундаментом, о загрузке и манипуляциях в режиме редактирования - бизнес-логикой. Фреймворк похож на стройку - хранит строительные конструкции для возведения сайта. Без знания, что у вас в основе, сложно сказать, какие "кирпичи" использовать для той или иной ситуации.

    Для успешного проектирования нужно смотреть на всё это дело "со стороны сервера", воспринимать систему изнутри. Самое простое - расписать последовательность действий и понять, что должно отвечать за тот или иной шаг. Потом, как снежный ком, действия расширяются, группируются в рутинные процессы, автоматизируются и превращаются в такого зверя.

    P.S. Хорошо было бы создать тему, в которой подобным образом расписаны принципы основных фреймворков (возможно, такое уже есть?) - данный хенд-мейд потихоньку приближается к пределу своей занятной неизвестности, хочется выявить и установить перспективные алгоритмы или даже заменить систему, минуя известные сообществу "архитектурные грабли" одиночных раздумий.
     
  3. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Немного о действиях Engine::ITYPE_*

    Есть команды, на которые JavaScript будет ожидать ответа в формате xml/json (я использую json в ядре - ему достаточно передать массив данных, без заморочек, присущих форматированию xml) - Engine::ITYPE_JSON

    Также есть команды, не требующие данных для ответа. Это может быть запись разметки от Builder в некий физический файл на сайте. Для этих целей - Engine::ITYPE_PLAIN, прерывающий скрипт в момент выхода из метода-обработчика.

    Для иных случаев - Engine::ITYPE_TEMPLATED, подключающий к полученным данным рендер представления.

    Добавлено спустя 5 минут 45 секунд:
    Архитектура (связка классов ядра)

    Код (Text):
    1. Repository - хранилище синглтонов
    2.     Engine - пусковой "механизм", возвращающий на уровень Repository статические данные
    3.     Module - модуль контроллера
    4.         Back
    5.         Front
    6.         FrontModule
    7.            MyModule
    Подобная структура позволяет ввести список действий для самих FrontEnd/BackEnd и настраивать их поведение по принципам модулей. Т.е. эти классы отличаются от модулей только наличием метода rend, который вызывается классом Engine в методе launch.

    На уровне Repository:

    Код (Text):
    1. protected function __construct() {}
    2. public static function getInstance() {...}
     
  4. Mr. T

    Mr. T Активный пользователь

    С нами с:
    10 ноя 2010
    Сообщения:
    733
    Симпатии:
    0
    Адрес:
    Украина, г. Киев
    Pran, благодарю за потраченное время на написание ответа. Столько развернутый ответ удивляет. Но все же, я спрашивал не какой использовать фреймворк или как спроектировать свой, а задавал конкретный вопрос по архитектуру классов в контексте конкретной задачи.

    С фреймворками знаком, к тому же, есть собственная реализация паттерна MVC. Для решения на сущей задачи используется самописный фреймворк, реализующий паттерн MVC, не мной написанный, для справки.

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

    Если есть полезные советы относительно решения проблемы, с удовольствием прочту и обдумаю. Спасибо.
     
  5. d1gi

    d1gi Активный пользователь

    С нами с:
    24 май 2009
    Сообщения:
    326
    Симпатии:
    0
    можно по подробнее, что конкретно имеется ввиду под "порядок блоков на веб-странице" и что конкретно вы подразумеваете под "блоком"?
     
  6. Pran

    Pran Активный пользователь

    С нами с:
    15 янв 2011
    Сообщения:
    39
    Симпатии:
    0
    Тогда продолжим :) Интересует интерфейс наподобие Google Analytics? Если число возможных мест под блоки на странице ограничено, можно эти места "пронумеровать". У блока есть заголовок и ID. У секции - некоторый номер. Тянем заголовок в секцию, отпускаем. Получаем пару ID_заголовка - номер_секции. Сохраняем выбор на сервере и перемещаем блок при помощи JavaScript/JQuery на текущей странице. Когда стартует Builder при перезагрузке, ему сообщается: в зону 1 нужно вывести "этот блок", а в зону 2 - "вот эти три".

    Как перечислить возможные зоны на странице? Можно сделать таким образом: есть построитель наподобие Zend_View. Если через него прогнать шаблон "вхолостую", т.е. никуда не отдавая, то зоны вывода последнего будут дёргать метод __get (или offsetGet в случае реализации ArrayAccess, что более удобно для секций с названиями вроде $this['my-new-area']) как рыба, попавшаяся на крючок. Остаётся лишь зафиксировать обращения $name, т.е. названия/номера секций, которые могут себя вывести на страницу.

    В любом случае, будет мастер-каркас и зоны для установки блочного наполнения.
    Вот пример облегчённого рендера на основе Zend:

    Код (PHP):
    1. <?php
    2.  
    3.     class Render implements ArrayAccess, Countable, IteratorAggregate
    4.     {
    5.         protected $blocks;
    6.         protected $path;
    7.  
    8.         public function __construct(array $blocks = array(), $path = null) {
    9.  
    10.             $this->blocks = $blocks;
    11.  
    12.             // Указанный путь или некий путь по умолчанию.
    13.             $this->path = $path ?: DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'views';
    14.         }
    15.  
    16.         public function offsetSet($key, $value) {
    17.  
    18.             $this->blocks[$key] = $value;
    19.         }
    20.  
    21.         public function offsetUnset($key) {
    22.  
    23.             unset($this->blocks[$key]);
    24.         }
    25.  
    26.         public function offsetExists($key) {
    27.  
    28.             return isset($this->blocks[$key]);
    29.         }
    30.  
    31.         public function offsetGet($key) {
    32.  
    33.             return isset($this->blocks[$key]) ? $this->blocks[$key] : null;
    34.         }
    35.  
    36.         public function count() {
    37.  
    38.             return count($this->blocks);
    39.         }
    40.  
    41.         public function getIterator() {
    42.  
    43.             return new ArrayIterator($this->blocks);
    44.         }
    45.  
    46.         public function assign(array $blocks) {
    47.  
    48.             $this->blocks = array_merge($this->blocks, $blocks);
    49.         }
    50.  
    51.         ##
    52.         ## Begin of the compatibility
    53.         ##
    54.  
    55.         public function __unset($key) {
    56.  
    57.             unset($this->blocks[$key]);
    58.         }
    59.  
    60.         public function __set($key, $value) {
    61.  
    62.             $this->blocks[$key] = $value;
    63.         }
    64.  
    65.         public function __isset($key) {
    66.  
    67.             return isset($this->blocks[$key]);
    68.         }
    69.  
    70.         public function __get($key) {
    71.  
    72.             return isset($this->blocks[$key]) ? $this->blocks[$key] : null;
    73.         }
    74.  
    75.         ##
    76.         ## End of the compatibility
    77.         ##
    78.  
    79.         public function rend($filename) {
    80.  
    81.             ob_start();
    82.             require $this->path . DIRECTORY_SEPARATOR . $filename;
    83.  
    84.             return ob_get_clean();
    85.         }
    86.     }
    87.  
    88. ?>
    В файлах представления зоны вывода блоков определяются вставками <?= $this['area_id'] ?> или <?= $this->area_id ?>

    Загрузка блоков в рендер:

    Код (PHP):
    1.     $render = new Render(array(
    2.  
    3.         'content-area' => 'контент зоны content-area, сюда можно слить блоки из базы'
    4.     ));
    5.  
    6.     echo $render->rend('mymaster.php');
    7.  
    8.  
    Можно сделать так: <div id="content-area" data-area-caption="Зона высадки десанта"><!-- Здесь существующий контент --><?= $this['content-area'] ?></div>

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

    Пробовал я модель на базе Observer. Она довольно специфична и подходит в том случае, когда нужно выполнить блок кода в том месте, к которому иным образом не пристроиться в виду его изоляции/инкапсуляции. Дополнительное время тратится на опрос слушателей события. По аналогии представляется бой гладиаторов: нанёс удар (выполнил действие), встал в позу и осматривает трибуны. К настоящему моменту ума не приложу, зачем в связке Builder -> html/css может потребоваться Observer? Недостаёт некой детали в головоломке.

    Валидатор файлов, логирование? Что за файлы, какие логи? Пожалуйста, подробнее о целях. Блоки могут не иметь файловой сути: контент пишется прямо на странице через WYSIWYG-редактор (CKEditor, TinyMCE и т.п.), отправляется в базу. К WYSIWYG подключен заданный набор стилевых правил, остальное прописывается в код редактором.
     
  7. FalkoN

    FalkoN Активный пользователь

    С нами с:
    1 апр 2011
    Сообщения:
    184
    Симпатии:
    0
    Адрес:
    Екатеринбург
    да Вы академик?!