Pull to refresh

Comments 36

Сама идея далеко не нова, используется во многих ORM.
Особую мощь и красоту технике придают магические get/set методы.

Что касается Ваших примеров, то, например, этот поток команд:

>> Application::Control()->Members()->Students()->Group(123)->Student(431)->Show();

очень груб с точки зрения интуитивного наследования управления. К тому же, чисто логически, непонятно, зачем делать выборку по группе, если после этого студента все равно получают по ID — нонсенс и один лишний шаг?

Предлагаю дополнить Вашу статью примерами правильного наименования и иерархического распределения классов и методов, когда не только применяется техника потока команд, но и архитектура приложения в целом продуманны.

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

Vendors::GetById (APP()->vendorId) -> Delete();
Может
Vendors::Delete(APP()->vendorId))?
>> К тому же, чисто логически, непонятно, зачем делать выборку по группе, если после этого студента все равно получают по ID — нонсенс и один лишний шаг?

Это не выборка по группе, и этот код не нужно сравнивать с точки зрения ORM (здесь нет скоупов), это просто цепочка инициализации все новых и новых объектов с целью приравнять логику кода к URL.
Т.е. порядок не имеет значения?
имеет, в контексте последовательности инициализации объектов. Но URL определяет последовательность, в данном случае он вроде-бы логичный (с точки зрения структурирования сайта, типа мембер такой-то группы), но как Вы сами указали, порождает много лишних действий.

А, с другой стороны, почему не так:
Application::Control()->Group(123)->Members()->Students()->Student(431)->Show();

?
Ответ в URL.

Также гляньте на:
Members::Students()->Group(123)->Student(327)->Tree()->Toolbar()->Add();

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

Дело в следующем:
Вы сказали, что подобный прием используется в Javascript библиотеках, таких, как, например, jQuery.
К слову, такое еще используется во всяких библиотеках, которые строят чарты вроде openflashchart.

И там, и там цепочки выстраиваются в рамках первого главного узлового объекта и заканчиваются сразу, как только мы исчерпали скоуп. Да, не смотря на то, что это не ORM, скоупы никто не отменял.

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

Веду к тому, что архитектурно работу Вашего приложения нужно выстраивать следующим образом:
function APP(){return Application::Instance();}
// при создании экземпляра объекта приложения (синглтон), автоматически вызывается метод Init(), который отвечает за загрузку конфигов и первичную инициализацию переменных

App()->group=123; 
App()->student=431;

// к слову, две предыдущие строки - лишние. Если Вы хотите работать с $_REQUEST, имеет смысл загнать все параметры в свойства объекта Application автоматически на этапе инициализации, либо забросить их в массив и получать доступ к ним через волшебные методы, ну или воспользоваться кодом ниже:
/*
public function __get($name){
  return isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : null);
}
*/

// А теперь самое интересное:
APP()->addTree () // добавили в приложение дерево
  ->InitFromConfig () // проинициализировали дерево из конфига
  ->ActivateGroup (APP()->group) // сделали активным узел с группой
  ->ActivateStudent (APP()->student); // подсветили текущего пользователя

APP()->addContentField(APP::C_PERSONAL_PAGE) // добавили в приложение область контента с типом "персональная страница"
  ->AttachUserProfile( APP()->sudent ); // и отобразили профиль выбранного студента


Все дальнейшие действия точно так же атомарно бьются, а за атом принимается любая самостоятельная группа элементов интерфейса.

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

APP()->Crumple()
  ->SetTemplate('template1')
  ->DisableCache()
  ->EnableGoogleAds()
  ->Render()
  ->Print();


Что касается запросов на изменение отдельно аватарки или выборочных полей в профиле студента, то здесь тоже все просто и красиво с теми же объектно-ориентированными цепочками команд.

// уровень обработки данных.
// грубый пример реализации функционала выборочного изменения частей профиля
if (APP()->ajax){
  switch (APP()->action){
    case 'changeAvatar':
      APP()->Students()->getById(APP()->studentId)
        ->setAvatar($_FILES['avatar'])
        ->Save();
        break;
    case 'changeAvatar':
      APP()->Students()->getById(APP()->studentId)
        ->setName(APP()->name) // все проверки и санация переменных внутри метода
        ->Save();
        break;
  }
  die(); // это был всего лишь ajax-запрос на изменение. Здесь перед самоликвидацией можно поставить вывод JSON-ответа о том, что все прошло хорошо.
}


Таким образом все получается аккуратно:
— отдельные аспекты описаны отдельно
— потоки команд применены для всех объектов
— можно четко отделить обработку данных от непосредственного вывода

Главное — не переусердствовать и видеть границу между «красиво» и «понятно и работает»
1) это не мой топик!!!
2) я не оправдываю автора, я просто указал, что Ваше сравнение с ORM немного некорректно, несмотря на то, что здесь тоже цепочечное построение «запроса».
3) за код, подобный приведенному в топике, бью по рукам, больно ;)
в принципе, если рассматривать скоупы в общем виде, как переход от «общего к частному», то да, похоже, но структура, приведенная автором топика по своей логике завязана на URL, а не на объекты. Здесь нет явного ORM, просто структура оказалась близка, поэтому вызывает ассоциации
Ну бяка же, согласитесь)
case 'changeAvatar': — у вас оба раза
>> К тому же, чисто логически, непонятно, зачем делать выборку по группе, если после этого студента все равно получают по ID — нонсенс и один лишний шаг?

Нумерация студентов может быть не сквозной)
круто! И получаем в итоге монолитный сайт с монолитным кодом (причем где почти все классы знают обо всех) и строгим запилом интерфейса страницы в коде классов, это называется «говнокод», хоть идея вроде бы и простая.
Вы все-таки определитесь со static методами, а то некрасиво как-то
Долго думал, что такое «текучий интерфейс». Fluent interface что ли? А почему он расширенный?
А вы в курсе, что interface в выражении «fluent interface» и «интерфейс» в «интерфейс пользователя» — это разные вещи?

А вообще, вы изобрели Domain Model и зачем-то смешали его с отображением, нарушив single responsibility principle.
Fluent interface — это, мягко выражаясь, другое.
> Теперь наш сайт имеет дерево, кучу вкладок, форму, несколько тулбаров и т.д. Таблицы, что вставлены во вкладки,
> сортируются во все направления. А поле Аватар в форме может без перезагрузки всей страницы загружать и удалять
> картинку. Так как же организовать запросы ко всем этим элементам?

Откройте для себя REST-архитектуру.
«Мыши плакали, кололись, но продолжали грызть кактус»
может я конечно не понял в чем соль зачем setter возвращает сам объект?
public function SetWidth($value) {
        $this->width = $with;
        return $this;
}

класс который возвращает объект другого класса, это вроде как антипатерн какой -то там, да и не очень понятно зачем он нам вообще нужен.
class Students {
    
    public function Group($number) {
        return new Group($number);
    }
    
}

Вот у нас целых 6 классов, непонятно правда за чем, думаю нам хватит 3-4
class Members 
class Teachers
class Students 
class Group 
class Teacher
class Student 

1) abstract class Member
2) сlass Teacher extends Member
3) сlass Students extends Member
4) class Group

да и как то такая цепочка рвёт мозг
Application::Control()->Members()->Students()->Group(123)->Student(431)->Show();
вместо, например, Student::getByGroupAndId(123,431) ну или чего либо подобного. 


По поводу сеттера — Вы тему не догнали. Чтобы можно было не отпуская педали гнать по методам объекта дальше через " -> ".

А в остальном правы, выше оставил комментарий.
спасибо за разъяснения, но «Чтобы можно было не отпуская педали гнать по методам объекта дальше через » -> "" думаю автору топика стоит задуматься над этим, сеттер он на то и сеттер что бы всего лишь установить значение свойства объекта. Как мне кажется
retrun $this;
большее зло, нежели чем
$images = new Images();
$images->SetWidth(100);
$images->SetHeight(100);
Имхо, тут все в порядке. Это просто иной подход к оформлению, который много где используется, включая ORM на PHP и различные фреймворки.
давайте без холиваров, это распространенная практика избавления от постоянных $images церез цепочку вызовов.
Вроде бы даже является хорошим тоном «если метод ничего не возвращает, он возвращает текущий объект», принятым не только в PHP, мне лично больше нравятся такие конструкции:
$images = Images::instance()
    ->setWidth(100)
    ->setHeight(100);
Я придерживаюсь правила «явное лучше неявного». Потому что так легче прочесть код и понять что он делает.
В предлагаемой технике видно, что все вызовы будут происходить неявно. И если что-то пойдёт не так, то отлаживать такое будет непросто.
«Расширенный текучий интерфейс»
$images->SetWidth(100)->SetHeight(100);
Обычно называется «chained calls». разве нет?

«Это (MVC) прекрасная модель построения сайта. Именно сайта. Но вот когда речь заходит о построении, на его основе веб-приложения, увы, ощущаются его ограничения»
Вы меня простите, но MVC появился когда и о сайтах то мало кто задумывался и продолжает сейчас успешно применяться. И уж тем-более MVC и «chained calls» — это вещи ну совсем уж параллельные, никак друг на друга не влияющие

Это Ваша версия построителя MySQL запроса, причем без защиты от SQL-injection, лучше делать DB Abstraction Layer и подключать нужный в зависимости от базы данных. А еще можно просто сделать DAO обертку для PDO и поиметь много вкусностей.
А я просто пишу «нормально» запросы, экранирую руками и поля и значение. Ну вот как то приучил себя. Хотя, тоже использую для запросов к базе Текучий интерфейс.

Database::Exec("\n SELECT * FROM `members`")->Read()
SQL injection до сих пор и существует из-за того что кто-то просто «нормально» пишет запросы, достаточно один раз ошибиться чтобы он был, и достаточно один раз написать экранирующую обертку чтобы его не было никогда, тем более что большинство современных SQL РСУБД поддерживают prepared statements, где данные не нужно писать напрямую в запрос.
«Скажите, а что делать, если, вы разрабатывает не сайт, а веб-приложение. Интерфейс веб-приложения на много сложней приведенного выше. » — ну лично я воспринимаю контроллер без всякой привязки к странице. Контроллер это совокупность действий. Ничто не мешает с одной страницы обратится (если речь о ajax) к нескольким контроллерам и экшенам и наоборот.

А где граница кстати между сайтом и веб-приложением? :)
Делали подобное для генерации HTML.
Получилось что-то вроде:
class ReportSummary extends DocumentsClass {
	protected static function makeSummaryReport($report) {
		$document = jQuery('div')->addClass('RContainer');
		$body = jQuery('div')->addClass('RBody');

		# Титул
		$document->append(self::documentTitle($reportData, $report));
		# Данные
		$body->append(self::documentHeader($reportData));
		$body->append(self::commonParagraph($reportData, $report, $paragraphCounter));
		
		// ... и так далее ...
		
		return $document->append($body);
	}
	
	// ... //
	
}

Нужно быть очень аккуратным с «Текучими интерфейсами».
Animals->getDog()->getLeg()->run(10)
Подобный код нарушает инкапсуляцию, не надо пытаться приказывать ногам собаки, лучше ей напрямую приказать, а как бежать она и сама разберется.

А код
Application::Control()->Members()->Students()->Group(123)->Student(431)->Show();
имеет очень большую связанность, которую сложно рефакторить.
В Идеале должен выглядеть так:
Application::Control()->showStudentsMembers(431);

Если попробовать написать тесты на эти оба случая, разница будет более чем заметна.

Правда есть и другая сторона, сильная инкапсуляция увеличивает количество кода и количество классов оберток.
«Текучий интерфейс»? Вы серьезно? :) Мне кажется, что «Fluent-интерфейс» — это выражение не требует дословного перевода.
Чета я не понял, или оверлодинг больше не катит?
<?php

class a
{
   function __get($v)
   {
       $this->$v = new $v;
       return $this->$v;
   }
}

class b
{
    function say($word){
        echo $word;
    }
}
$a = new a();
$a->b->say('hello world');

// echo 'hello world'
?>

ни или в вашем случае через __call
В первом исходнике наверное подразумевалось "$this->width = $value"?
Sign up to leave a comment.

Articles