Как стать автором
Обновить

GenericObject

Время на прочтение12 мин
Количество просмотров811
Всего голосов 113: ↑76 и ↓37+39
Комментарии56

Комментарии 56

Всё хорошо, но по-моему, это никак не связано с понятием «generic-классы». По крайней мере, в Java/C# этим словом обозначается совсем другое. А у вас получился просто рефлексивный контейнер с кастомной валидацией.

ЗЫ: да, я не php-программер, так что возможно не в теме терминологии…
Прежде всего, хотелось бы сказать спасибо за статью на «верную» тему. В последнее время PHP всё больше двигается в сторону гибкой разработки, но хочется ещё и ещё ;)

Теперь по делу.

Первое конструктивное (надеюсь) замечание — в двух словах хотелось бы услышать зачем это вообще надо и чем удобно, а не просто «Generic-класс». Статья ведь не только на гуру OOAD расчитана (они наверняка такое уже использовали, своё ли, чужое ли).

Далее: приведённый пост не будет работать «из коробки»: не определены исключения, интерфейс, класс Validate и иже с ними. Такого рода решения лучше приводить полностью, чтобы можно было сразу запустить и всё работало. А в качестве примера использования могли бы быть юнит-тесты, которые, как известно, являются лучшими рабочими примерами :)

Третье: если честно, лично мне не очень понравилось то, что в абстрактном классе довольно много реализации той или иной логики. Кроме того, методы зачастую перегружены условиями. А с наличием «обязательной» валидации в GenericObject::_setParam() вообще не согласен: не всегда она нужна. Можем же мы просто через акцессор затулить в объект гарантированно «чистое» значение? :)

Да и сама валидация какая-то дубовая получилась (IMHO, конечно).

Но в целом — хорошо. Продолжайте в том же духе :)
Очень согласен с пунктом номер 3.

У себя я применяю следующую логику:

— во внутреннем хэше класса хранятся «чистые» значения
— при инициализации (или наследовании) класса на некоторые параметры вешаются callback функции, генерирующие «view-значения» в отдельный хэш при установке значений set-методом
— get-метод (для унификации я не испульзую getter-методы наподобии Java) по умолчанию возвращает «view-значения», но может возвращать и «чистые» — зависит от текущего режима работы который устанавливается через SetMode с соответствующим параметром

И валидацию по типам (для данных запроса) я делаю отдельным классом, которому передается нужная «сущность» и у нее уже устанавливаются валидированые значения через бызовы метода set переданного экземпляра.
По поводу третьего пункта:
Интересно, а что, на PHP есть возможность подобную реализацию сделать не абстрактным классом? Объявить конструктор приватным, чтобы класс не могли инстанциировать? Чтобы в каждом дочернем классе извращаться с вызовом __construct() или специального метода инициализации класса?)
Можно. На этом, собственно, и основан паттерн singleton. Но перегружать его условиями инициализации тоже не очень рекомендуется ;)
В силу специфики предметной области в которой я работаю у меня часто появляется необходимость создавать классы с большим количеством скалярных параметров. Работа с ними однотипна (get, set, validate). Если параметров 5 — нужно соответственно 10 почти одинаковых метода. Это усложняет код класса, увеличивает его размер, да и вообще порождает большое дублирование кода. Вот именно в таких ситуациях я и использую данный класс. Использую его очень осторожно, т.к. не люблю такие динамические штуки. Этот класс некий компромисс между желанием избежать сложных динамических решений и нежеланием дублировать рутинные операции.

Класс сделан по принципу Pure Fabrication (чистая синтетика), т.е. не выполняет никакой бизнес-логики. Служит только как вспомогательный инструмент. Набор функциональности отработан на практике и сведен к минимуму для простоты понимания. Именно поэтому используется такой простой (топорный) способ валидации (который, кстати, покрывает 95% потребностей).

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

Постоянная обязательная валидация сделана намеренно. Гарантированно чистых значений не бывает, а если таковые и есть, то они спокойно пройдут валидацию. Это достойная, на мой взгляд, плата за уверенность, что работаешь именно с тем, с чем ожидаешь.
Ларман :)
Хорошо бы в начале статьи в один абзац написать, что такое Generic-класс и зачем он нужен. А всю реализацию, в том числе описание преимуществ данного примера — подкат.
А то вдруг на главную страницу выйдет, то не всем может быть понятно о чем статья.
А зачем вы разделяете две функции _setParam и _setParams?
Можно делать проще, оставить одну функцию _setParam, проверять передали вам массив или нет и если да, то обходить в цикле.
Тогда и в конструкторе не понадобятся проверки. Можно будет просто написать:
$this->_setParams($property);
И самое главное, вся работа с данными будет в одном месте.

И ещё, мне кажется, писать в конструкторе так:
$this->_initParam = $property;
не правильно. Доступ до переменных необходимо, во избежании проблем, так же осуществлять через специальные функции. В данном случае: $this->_setParams($property);
Мне, например, кажется, что разделение _setParam() и _setParams() вполне оправдано, тогда методы не перегружаются лишними проверками условий (особенно, если появятся ещё).

В конструкторе вообще лучше не использовать сеттеры, потому что сеттер изначально подразумевает, что объект инициализирован, и при дальнейшем развитии класса может возникнуть неприятная ситуация, когда сеттер обращается к каким-то пустым значениям, которые определены дальше в конструкторе.
Думаю это дело вкуса. =)
На мой вкус, проще запомнить одну функцию, а не две.

Сеттер в конструкторе хорош для «мини-классов».
К примеру:
new $oLog( 'logging' );

Я понимаю, что это может быть малоприменимо, но на моей практике, нужда в таких «мини-классах» встречалась.
Плюс автор явно оговаривает использование паттерна Lazy initialization.
я люблю решения как можно более очевидные. Хотя конечно это дело вкуса, но на мой взгляд конструкция вида $this->_setParams($value); выглядит очевиднее $this->_setParam($value); исходя из той логики которую она выполняет. $this->_initParam — private переменная. Я стараюсь не создавать сущности без необходимости. вероятность повторного использования гетеров и сетеров для этой переменной стремится к нулю.
Работа с нескалярными параметрами выглядит не очень удобной:
$arrayParam = $obj->getArray();
$arrayParam['deeply']['nested']['param']['value'] = 'something';
$obj->setArray($arrayParam);

По-моему, стоит задуматься о возвращении параметров по ссылке.
Или в схеме (тут это _params) создавать типы данных со своими преобразованиями и проверками, которые к тому же автоматически будут передаваться по ссылке.
Этот класс _не_работает_ с нескалярными параметрами. Это сделано специально. Ассоциативные массивы (тем более многомерные) в интерфейсе я не использую, а для объектов создаю гетеры и сетеры вручную. Параметры по ссылки (имхо) — зло, а объекты по ссылкам и так передаются.
Этот класс _не_работает_ с нескалярными параметрами. Это сделано специально.

да, у меня мелькнуло такое подозрение. тогда стоит, наверное, в комментариях к классу сообщить об этом ограничении.
Мне кажется, что лучше использовать не сеттеры и геттеры через __call() (потому что может возникнуть конфликт имён), а __get() и __set(), для которых можно уже внутри устанавливать обработчики onset_email() и т.п. Потому что захочется использовать getXXX() и setXXX() для чего-то ещё, кроме внутренних переменных.
Захочется — используйте :) __call() срабатывает только для необъявленных методов, как впрочем и __get()/__set().
Ээээ… Вы меня, кажется, неправильно поняли.
Мне тоже так кажется :) Если в дочернем объекте понадобится использовать getXXX/setXXX — достаточно его просто добавить, если XXX будет совпадать с именем одного из свойств — магический метод просто не будет вызываться, а будет использоваться объявленный метод, если-же вы про то что захочется __call использовать для других целей — то тогда уж и __get/__set возможно тоже :)
Как вариант в дочернем объекте __call можно переобъявить, добавив необходимый функционал и при непопадании вызванного метода в новые условия — вызвать parent::__call($name, $args), который уже проверит вызванный метод на соот-ие getter'ам/setter'ам, и в случае несоответствия будет ругаться Exception'ами.
Да, у Вас смайлик стоит в качестве точки. А я воспринял всё одним предложением.
Да, дело вкуса.
мне кажется проблема с конфликтом имен надумана. мне за все время использования этого решения (белее года) не встречалась. Доступ к параметрам с использованием паблик переменных (или их эмуляции) я не использую. Считайте это религиозным убеждением. ;)
НЛО прилетело и опубликовало эту надпись здесь
* Видимые недостатки:
* 1. Отсутствует возможность использовать автокомплит в IDE, так как методы явно не определены


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

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

Не буду особенно критиковать эти подходы, может не понимаю чего… но во всей этой динамике люди часто начинают забывать про основные парадигмы программирования. А порой достаточно просто лучше продумать архитектуру продукта и многие хитроумные конструкции могут вовсе не понадобиться. Ну это я брюзжу, простите :)
Вообще, конечно, динамика не всегда хорошо, но конкретно для данного случая не соглашусь: здесь интерфейс ясен. Кроме того, для методов акцессоров и мутаторов имена вида (set|get)(\w+) стали де-факто. Понять, что делает такой метод, довольно просто, как мне кажется :)
Вообще-то очень серьезный недостаток, если ты не пишешь домашнюю страничку.

Навигация по 2м-3м сотням классов не такое уж и простое дело в блокноте…
А что, кто-то ещё делает в блокноте проекты на 2-3 сотни классов? :)
ну понятно, что нормальный человек не делает, но последнюю неделю я занимался тем, что прямо на сервере вносил срочные правки в довольно обширный говнокод. Скачивать это всё к себе как-то не очень хотелось.
как бы это смешно не выглядело, этот недостаток для меня весьма серьезен. И дело даже не столько в автокомплите. Все неявные динамические решения я стараюсь сводить к минимуму.
Весьма интересная статья, большое спасибо. Ваш подход мне в целом понравился.

Понравился fluide interface (сколько ж терминов умных).

У меня правда есть ряд замечаний, возможно они покажутся надуманными, но всё же.

GenericObject совсем уж не абстрактный, слишком много реализации заложено в нём. (об этом писали, просто согласен с предыдущим автором).

GenericObject — больше служебный класс, расширяющий и уменьшающий количество кода в других классах, но мне не нравится, что это происходит через наследование, другой способ нужно поискать, но мне кажется что Customer и GenericObject имеют мало общего. Я всегда был сторонником того чтобы в иерархии классов всегда был ещё какой нибудь элемент связанности родитель-потомок. А Customer не имеет ничего общего с по сути рабочей лошадкой GenericObject — хотя это даёт определённые преимущества в уменьшении кода.

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

Ну а в целом повторюсь — весьма интересно и свежо.
> Понравился fluide interface (сколько ж терминов умных).

Это просто автор решил выебнуться баззвордом, да не вышло — не обращайте внимания) На самом деле это называется Fluent Interface.
Приношу свои извинения, действительно «неудачно выебнулся» :). Пытался вспомнить как называется, в итоге нашел framework.zend.com/manual/en/zend.currency.html упоминание fluide interface и успокоился.
Исправил.
Я бы сделал немного «помягче» обработку ошибок, чем вызов при каждой ошибке throw, так как замучаешься на каждую инструкцию ставить try {} catch()
throw должен вызываться в случае ошибки, которая не может быть обработана данной функцией и является критической для дальнейшего выполнения. Если же это просто неверное значение, до достаточно возвращать было бы false или же записывать во внутреннюю переменную код ошибки и скипать выполнение всей цепочки.

Я имею в виду вот такую цепочку:
$customer3->setName('Key')
    ->setEmail('key@gmail.com')
    ->setPhone('555-55-55')
    ->save();


её необходимо заключить в try{}catch() по логике.
Но лучше было бы проверять в таком случае код ошибки
if ( $customer3->isError() )
//...do smth


В целом очень хороший подход
Я бы добавил еще метод validate() в цепочку и избавился бы от setXX, getXX, оставив mutable-метод XX:

$customer3->name('Key')
->email('key@gmail.com')
->phone('555-55-55')
->validate()
->save();

при этом метод validate() генерил бы exception в случае «непрохождения» пропертями валидации данных. А мы уже из exception могли бы вытянуть какие проперти невалидны и почему.

P.S. делал я подобную муть пару лет назад — как ни крути — рано или поздно приходишь к мысли, что PHP ушербен по своей сути, и такие штуки на нем делать — wrong way.
а кто не ущербен?
Скажем так, [Javascript, Python, Lisp] гораздо менее ущербны, нежели PHP.
в одной из первых версий так и было. на практике оказалось что удобнее так, как сейчас. Ну практика у всех разнаю, возможно в каком-то случае удобнее явно вызывать метод валидации. Ну а я предпочитаю инкапсулировать эту логику, полагаясь на презумпцию невиновности :). Ну а если некорректные данные прошли фронтенд-валидацию, то это явно исключительная ситуация.
я про другой подход написал.
как-то слишком много намешано, проще надо быть.
вот, например: d-o-b.ru/? source: core.object
можно устанавливать значения как через свойства, так и через методы. последние можно соединять в цепочки. состояние объекта представляет из себя иерархическую структуру, а не плоский список. ну и прочие плюшки…
а вот пример драйвера базы данных на его основе: d-o-b.ru/? source: db.driver.sqlite
ленивое подключение, автоэкранирование, — стандартный суповой набор.
ну и, наконец, пример использования, если заинтересовал: d-o-b.ru/? source: action.user.response

единственный минус — всё это не родные возможности пхп, а значит тормозит-с. впрочем, для драйвера бд это вполне допустимо… только вот к хорошему привыкаешь быстро и начинаешь использовать это везде ;-)
мда… пробелы после вопросеков и двоеточиечек нужно убрать…
за такие приколы создателям хабра надо что-нибудь оторвать… ноги, например… чтобы они ими больше ничего не писали… >:-E
Оторвали судя по всему вам, карму. Может за дело, может и нет — не буду разбираться :)
Ссылки для желающих не иметь гемороя:
d-o-b.ru/? source: core.object
d-o-b.ru/? source: db.driver.sqlite
d-o-b.ru/? source: action.user.response

P.S. Традиционное: Парсер — лох.
чем же это по вашему «проше»? можно через свойства, можно через методы, иерархическая структура, прочие плюшки… по-моему это сложнее. разве нет?
у меня основные правила использования укладываются в:
* не используйте подчёркивания в именах свойств
* если требуется перегрузить свойство можете определить методы get_{имя} и set_{имя}
* для доступа в обход гетера и сеттера ставьте подчёркивание перед именем
всё. если требуется чего-то большего — читать доку.

в твоём же случае много нюансов вида «Необходимо переопределять в каждом классе-наследнике». а что будет, если я забуду что-то переопределить?
да, и «ленивая инициализация» на уровне свойств, а не всего объекта — элегантней и практичней (см. реализацию драйвера)
А в каких задачах вы подобные классы используете по причине того, что без данного алгоритма вам не обойтись технически? Или это просто такая удобная фишка, чтобы не париться с проектированием и упростить повторное использование кода?
использую для уменьшения дублирования кода. Использую осторожно и не всегда. Если объект содержит большое количество скалярных параметров. Бывает вначале класс использует GenericObject, затем обрастает собственными гетерами/сетерами, а те значения, которые ранее были скалярными, становятся объектами соответствующих классов. Тем самым необходимость в наследовании от этого класса пропадает
Получился динамический класс — разве это то, к чему нужно стремиться? По моему это лишь позволяет реализовать собственную лень.
согласен. лень создавать десяток-другой однотипных методов и раскидывать логику валидации по ним. Одним из требований, предъявляемых к этому классу, возможность быстро отказаться от его использования не внося изменения в интерфейс класса. Тем самым можно использовать для быстрого прототипирования и введения новых сущностей, окончательная безнес-задача которых не определена. Например у меня есть класс Tourist, который содержит в себе порядка 20 скалярных параметров. 20 сетеров + 20 гетеров писать ой как лень ).
В основе таких «волшебных» php технологий лежат именно магические методы (Magic Methods), иногда бывают полезны, к примеру при сериализации или клонировании. Прошу вас не минусуйте за ссылку, но я не смогу выложить здесь всю статью в одном комментарии. dustweb.ru/log/2008/09/01/php-magic-methods/
Что-то подобное я на Delphi писал давно, только специализация объектов другая была, там внутри была зашита и синхронизация объектов с БД. Я тогда на эксперимент пошел, реализовано было много чего интересного. То что писал, сейчас работает в одной системе для агентств недвижимости, занимающихся арендой. Тогда очень гордился тем, что для того, чтобы научить систему работать с еще одним типом недвижимости (гаражи, например), нужно потратить часиков 4-6, и то большая часть времени уходит на составление списка полей и рисование формы добавления-редактирования, потом все подцепляется автоматом и работает… Даже код не надо было писать. Надо только задать поля, их типы, указать, какой контрол на форме отвечает за поле, еще там кое-чего по мелочи и работать))
Это всё неплохо, но осторожнее с такими классами быть надо. Их использование легко, быстро, соблазнительно, но к тёмной стороне силы ведут они.
Использую очень похожий подход:
Есть класс-предок всех доменных объектов, генерирующий объекты на основе xml-карт.

В xml описаны имена полей и методы доступа ацессор/мутатор… Имена полей соответствуют именам полей в бд. Ну это знакомый, вероятно, вам способ.

В дополнение к xml для любого объекта можно также при необходимости создать php-расширение, в котором буду описаны дополнительные формы данных и методы управления ими. Например, какие-нибудь методы типа getDateAsText() (выведет дату 2008-06-11 в виде 11 июня 2008 года)

А можно и наоборот — без xml-части создавать объекты только при помощи php.
А можно какой-нибудь пример посмотреть?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории