Pull to refresh

Comments 49

Статические переменные — зло.

Перефразирую. Статические переменные = глобальное состояние. А работа с глобальным состоянием приводит к побочным эффектам.


Пишите юнит-тесты.

Вы мельком упомянули тестируемый код… и тут стоит упомянуть что проще этого доиться когда сначала пишутся тесты. Ну и отсюда всплывает еще куча радостных вещей что далеко не все стоит тестировать юнит тестами. Например query builder-ы мокать никто в здравом уме не будет и большая часть инфраструктурных вещей должны покрываться уже интеграционными тестами (потому что так проще выходит).


А отсюда мы еще и должны вводить какое-то разделение ответственности и вещи становятся намного сложнее.


p.s. В целом статья будет полезна многим)

UFO just landed and posted this here

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

Спасибо за комментарий! По правде говоря, основным побуждением к написанию этой статьи было желание поделиться необычным поведением static'ов в PHP. А с вашими замечаниями я могу только согласиться.
$isClientPropsOriginal = $reflector->getDeclaringClass()->getName() === 'AbstractEntity'? get_class($this): false;

А не лучше ли сделать вот так? Так оно читабельней.
$isClientPropsOriginal = false;
if($reflector->getDeclaringClass()->getName() === 'AbstractEntity') {
$isClientPropsOriginal = get_class($this);
}
Вы правы, обычно if, конечно, читабельнее, чем тернарный оператор. Но я привёл одну строчку, а в полном коде в этом месте уже стоит выше проверка

if ($isClientPropsOriginal === null) {

}


В тот момент, когда я писал текст, присвоение внутри одного оператора казалось мне более понятным.
> 1. Статические переменные — зло.
> 1. Пишите юнит-тесты.
> 1. Не бойтесь влезть в дебри.

а у вас -ус отклеился- для статьи юнит тесты не написаны! ;)
Будем считать ваш комментарий первым таким тестом =)

А вообще, мне только с третьей попытки удалось заставить ХабраМаркдаун отобразить нумерованный список как надо.
Гуглить «позднее/раннее статическое связывание». В статье описано нормальное поведение статических полей при наследовании.
В статье речь идёт о статических переменных внутри метода класса, а не о статических методах/полях классов.
Спасибо за статью, однако мне показалось, что ничего нового. Ну кроме постоянного смешивания понятий «статическая локальная переменная» и «статическое свойство»

То, что методы (обычные, не статические!) связаны с классом, где они написаны, а не с объектом и про вызове в них просто прокидывается $this — мне думалось, что общеизвестно…

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

То, что методы (обычные, не статические!) связаны с классом, где они написаны, а не с объектом и про вызове в них просто прокидывается $this — мне думалось, что общеизвестно…

Вы правы, но на деле всё получается несколько сложнее. Контекст статических переменных (и некоторая другая информация) создаётся отдельно для каждого класса, даже если сам метод в нём не объявлен (см. мой пример 1).
Я бы сделал нормальную статическую переменную (свойство) в отдельном классе ReflectionCache. Так проще и понятнее, и работает как надо. Стараюсь избегать использования статических переменных внутри функций, как раз из-за таких возможных багов.

Скрытый текст
class ReflectionCache {
    static $counts;
    
    public static function incrementCount($key) {
        if (!isset(self::$counts[$key])) self::$counts[$key] = 0;
        ++self::$counts[$key];
        return self::$counts[$key];
    }
}

class A {
    function printCount() {
        printf("%s: %d\n", get_class($this), ReflectionCache::incrementCount(static::class));
    }
}

class B extends A {
    function printCount() {
        parent::printCount();
    }
}

$a = new A();
$b = new B();

$a->printCount(); // A: 1
$a->printCount(); // A: 2
$b->printCount(); // B: 1
$b->printCount(); // B: 2
$b->printCount(); // B: 3


UFO just landed and posted this here
«Клиент» здесь — это браузер пользователя, то есть сущность знает, какие её поля нужно отдавать по REST API (ну и какие записать в базу — тоже знает)
UFO just landed and posted this here
… то этот вопрос надо будет обдумать :-)

Вообще я так с ходу даже не могу придумать, кто третий может прийти в наше уютненькое приложение с фронтендом и бэкендом. Но да, в существующей архитектуре знания сущности умножатся. Это не кажется мне большой проблемой, учитывая что 90% полей передаются как есть из базы, не требуя дополнительного описания, а для оставшихся мы сейчас используем симпатичные аннотации. Механизм из примера с возвратом массива — это старый вариант, который мы ещё не везде переписали. Если же клиенты размножатся до проблемного количества, придётся задуматься над рефакторингом.

А какую архитектуру вы предлагаете?
UFO just landed and posted this here
UFO just landed and posted this here

Да, я вас понял. Но дело в том, что у нас иногда требуется какое-то поле передать на клиент под другим именем, или вообще не нужно передавать. Простой пример: в базе в сущности User хранится поле manager_id, а клиенту удобнее передавать поле manager, чтобы там оперировать полями связанной сущности Manager как user.manager.name, а не user.manager_id.name. Сейчас мы просто пишем в аннотации к полю manager_id


/*
 * @Annotation\MappingClient(alias="manager")
 */

Если для другого клиента понадобится другой набор полей или другие их имена, то всю эту информацию всё равно придётся где-то хранить.


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


Что касается прав доступа, то их проверка находится в ведении контроллера, отдающего сущность клиенту. Он может, например, обнулить секретные поля.

UFO just landed and posted this here
сущности незачем знать о том, что клиенту удобно))

ну по правде говоря, да =)


Такая архитектура, конечно, более гибкая, но придётся делать по преобразователю для каждой сущности и клиента...

Если подумать, то один сериализатор может быть тривиальным. У нас это клиентский сериализатор :)


Для понимания контекста замечу, что речь идет о REST API для одностраничного приложения на JS. Большинство операций — CRUD. Часть данных сохраняется в БД, часть уходит в API других сервисов, например, в биллинг. Кроме валидации особой бизнес-логики у нас нет.


Бывший коллега придумал интерфейс сущности с двумя методами сериализации: EntityInterface::toStoreParams() и EntityInterface::toClientModel(). Я не вижу проблемы в существовании самих этих методов. У нас такое приложение, что каждая сущность передается на клиент и почти каждая сохраняется в БД.


В этом подходе сериализация в JSON для REST API занимает выделенное положение по сравнению с сериализацией для других целей. Она происходит без дополнительных сервисов. Типичный контроллер содержит код вроде


$userCollection = $userDataMapper->findBy(...);
return new JsonResponse([
    'users' => $userCollection->toClientModels()
])

Движение данных в обратную сторону уже идет через дата-маппер:


$user = User::createFromClientModel($request->get('user'));
$userDataMapper->insert($user);

В типовом дата-маппере вызывается toStoreParams(). Но если требуется более сложная логика по сохранению сущности (запись в две таблицы, запись в очередь и т. д.), мы переопределяем метод UserDataMapper::insert() и сериализуем данные, полученные через геттеры сущности.

В Zend Framework есть такая концепция как Hydrator.
Когда передо мной встала задача отделить представление структуры данных для клиента (то что будет сериализовано в json) от самой модели (в моём случае это были AR, ибо Yii), то я позаимствовал данное словечко и концепцию для организации слоя таких гидраторов. В итоге модели не знают о клиенте, и есть единая точка для манипуляций со структурами для json.
Ну и в поддержку ZF можно посоветовать посмотреть в сторону Apigility для вдохновления, там все красиво с точки зрения архитектуры :)
то есть сущность знает, какие её поля нужно отдавать по REST API

То есть элемент бизнес логики знает о UI. Вам не кажется что это не гуд?

А почему вы считаете, что REST API — это UI?


У нас Ember, "MVC" на клиенте и всё такое. API на сервере больше к M относится, а не к V.

UFO just landed and posted this here

V не может быть одновременно и на клиенте, и на сервере.


Вообще, MVC — это шаблон проектирования не любого приложения, а приложения с пользовательским интерфейсом. Об этом часто забывают. У API нет пользовательского интерфейса. Если всё равно внедрять MVC через силу, получится притянутое за уши V и каша в M. Лучше сразу честно признать, что слоев на бекенде REST API много, и они не MVC: https://habrahabr.ru/post/267125/

UFO just landed and posted this here

Тем не менее, REST API на сервере — это не приложение с пользовательским интерфейсом. Даже название — API — намекает, что это программный интерфейс.

UFO just landed and posted this here
REST API на сервере — это не приложение с пользовательским интерфейсом

API — Application programming interface. То есть делаем вывод — REST API — это как раз таки интерфейс взаимодействия других програмных средств с приложением. А приложение — оно внутри. Если нет явного разделения — мы говорим о smartui.


Только вот "представление" в контексте бэкэнда — это HTTP запросы/ответы. Приложение получает запрос, конвертирует из представления HTTP во воунтреннее представление, с которым уже может работать модель (приложение), на что оно снова выплевывает нам внутренне представление и мы формируем HTTP представление… слишком много слова "представления" но думаю смысл должен быть понятент.


То есть в рамках бэкэнда у нас есть:


  • пассивное представление (HTTP)
  • активный контроллер/медиатор/адаптер (фронт контроллер, мидлвэры, обычные контроллеры и их экшены) который конвертирует асинхронные запросы в синхронные вызовы методов модели и в целом формирует пассивное представление.
  • модель, само приложение, или скорее та часть ее которая граничит с интерфейсом и предоставляет програмное API для работы с ним. А HTTP API — это всего-лишь адаптер к этому интерфейсу.

Если так смотреть то мы можем говорить только о подходе разделения представления под названием mediating-controller MVC или Model-View-Adapter.


В классическом MVC вьюшки у нас активны, а вот контроллеры отвечают только за маршрутизацию событий в вызовы конкретных методов модели.


В MVP/MVVM у нас вьюшки пассивные а ViewModel/Presenter полностью берут контроль за тем как формируется пассивное view и как пользователь взаимодействует с ним.


И все это — просто частные случаи separated presentation.

V не может быть одновременно и на клиенте, и на сервере.

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


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

мы сейчас про какой MVC? который православный 79-ого года? Если да — причем тут он. Если мы про другие виды separated presentation — то уточните тогда.

Я про тот MVC, о котором написано в Википедии.


Можно сколько угодно заниматься казуистикой и утверждать, что json на выходе REST API — это "представление". Это не отменяет принципиальной разницы между организацией (десктопного) приложения, в котором пользователь нажимает на кнопки и HTTP API. В первом действительно можно сделать активную модель, события и настоящее управление представлением из модели. Во втором жизненный цикл "приложения" ограничен обработкой запроса и формированием ответа, по крайней мере на PHP.


Мало того, что MVC "притянут за уши". В контексте веб-программирования MVC — это вредный паттерн. Он приводит к тому, что у неопытных разработчиков роль модели играет слой работы с БД, роль представления — шаблоны, а бизнес-логика располагается в толстых контроллерах.

Можно сколько угодно заниматься казуистикой и утверждать, что json на выходе REST API — это "представление".

Правильно, потому что представлением тут является HTTP) У вас просто еще не возникало задач с версионизацией API, с чего весь разговор и начался. И если смотреть с этой позиции у вас будет две реализации UI (разных версий) для одного приложения. Никакого булшита про MVC и т.д. просто разделение обязанностей.


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


Это не отменяет принципиальной разницы между организацией (десктопного) приложения, в котором пользователь нажимает на кнопки и HTTP API.

Вы считаете что пользователь это почему-то человек который жамкает кнопки. Отсюда и весь конфуз.


Во втором жизненный цикл "приложения" ограничен обработкой запроса и формированием ответа, по крайней мере на PHP.

Вот последнее — правильно, потому что PHP обычно умирает. А теперь представьте что у вас нет базы данных, и все данные лежат просто в памяти, и PHP не умирает а так же висит как демон. Просто представьте.


А теперь подумайте, так ли это сильно отличается? У нас данные между запросами лежат в базе, или кэше или любом другом сторадже. Но это деталь реализации слоя хранения данных. Они могут точно так же в памяти лежать — на функциональность это никак не сказывается. Это сказывается только на надежности и стоимости железяк (все данные в память могут не поместиться).


А теперь заменим rest интерфейс на… ну не знаю… граффический интерфейс. Реализация нашей бизнес логики не меняется, меняется только UI layer, с HTTP на обычный GUI.


В контексте веб-программирования MVC — это вредный паттерн.

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


Это все от того что люди не понимают изначальную проблему, которую MVC и другие реализации пытались решить. separated presentation. То есть как организовать взаимодействие различных компонентов системы (приложение как отдельный компонент и GUI как другой) и конвертировать представление из формата модели приложения в то, с которым удобнее работать пользователю.


В целом тут можно много писать, и в принципе можно было бы написать статью. Не про MVC а именно про separated presentation. Скажем описать различные варианты и не давать им названий. Как думаете, имеет смысл?

Вы бы сразу написали про версионирование API :) В подходе, который я описал выше, конечно, не получится сделать версионирование без существенной доработки.


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


Я понимаю ваши аргументы про MVC, но они меня не убеждают. Я всё равно вижу принципиальную разницу между приложениями, в которых есть пользовательский интерфейс, и в котором его нет. Можно взять для примера графический редактор и консольную программу конвертации графики типа ImageMagick. У них может быть много общего (библиотечного) кода. В первом случае MVC говорит, как организовать код, отвечающий за взаимодействие с пользователем. Во втором случае MVC ничего полезного не говорит. Можно, конечно, формально соотнести фрагменты кода с представлением или с моделью. Но зачем?


Даже Фабиен вот пишет:


I don't like MVC because that's not how the web works. Symfony2 is an HTTP framework; it is a Request/Response framework. That's the big deal. The fundamental principles of Symfony2 are centered around the HTTP specification.

I don't like MVC because the web has evolved a lot in the recent years and some projects are much different than projects we had some years ago. Sometimes, you just need a way to create a REST API. Sometimes, the logic is mostly in the browser and the server is just used to serve data (think backbone.js for instance). And for these projects, you don't need an MVC framework. You need something that handles a Request and returns a Response. You need a framework that implements the HTTP specification. HTTP streaming is yet another example that does not fit well with the MVC pattern.

Напишите статью, если есть материал и желание. Вы хорошо объясняете.

Напишите статью, если есть материал и желание. Вы хорошо объясняете.

У меня в черновиках какие-то начинания есть, но после пары месяцев обсуждений такого вопроса есть идея пойти с другого конца… написать статью именно по вопросу представления данных, разобрать несколько подходов но не пользоваться названиями… Ну то есть там полюбому будут описаны и MVC и MVP но названия будут специально опущены. Тогда все становится чуточку проще как мне кажется. Имеет смысл?

Да, было бы интересно прочитать.

На мой взгляд $isClientPropsOriginal хорошо смотрелась бы статическим свойством в AbstractEntity. В смысле расходования кофе эффективнее было бы.

Со статическим свойством в абстрактном классе тоже на самом деле были проблемы. Если я правильно помню, код там примерно такой, и он работает не так как хотелось бы для кеширования в контексте класса:


<?php

class A
{
    protected static $cache;

    public function printClass ()
    {
        if (static::$cache === null) {
            static::$cache = get_class($this);
        }

        echo static::$cache, ' ', get_class($this), "\n";
    }
}

class B extends A
{
}

class C extends A
{
}

class D extends C
{
}

class E extends A
{
    public function printClass()
    {
        parent::printClass();
    }
}

$b = new B();
$b->printClass(); // B B

$c = new C();
$c->printClass(); // B C

$d = new D();
$d->printClass(); // B D

$e = new E();
$e->printClass(); // B E

Я и не говорил что поможет — код выше работает как и должен :)

Так private невозможно унаследовать => контекст не изменяется. Вполне логично, как мне кажется.

Нелогично то, что для наследования контекста достаточно заменить область видимости private на protected/public. Было бы естественнее менять контекст при переопределении метода.

Ага, мы вернёмся от моего примера 3 к примеру 2.


Я придумал, надо этот метод оставить protected, но пометить как final, чтобы никто потом не тянулся к нему грязным ручками.

Sign up to leave a comment.

Articles