[ Поиск ] - [ Пользователи ] - [ Календарь ]
Полная Версия: AcniveRecord в DDD
twin
Вот такая встала задачка, сделать слоистую архитектуру на Yii2.

Исторически так сложилось, что нужно по максимуму использовать как синтаксис, так и правила Yii2. Для того, чтобы снизить порог вхождения в проект. В том числе и AR. Но как уже известно, AR плохо сочитается с DDD.

Самым подходящим решеним, на мой взгляд, использовать схему CQRS, тогда в части Read можно бы и заюзать её, исключив мутирующие методы. Однако делать над ней общий репозиторий - большая глупость. А в чистую нельзя, ибо DDD.

Простым решением мне кажется было бы переопределить их заглушками:
class Repository extends ActiveRecord
{
public function save($runValidation = true, $attributeNames = null)
{
throw new \Exception('Низя.');
}
}


Кто как считает, имеет это право на жизнь и чем грозит?

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

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Настаивал, настаиваю и буду настаивать на своем. На кедровых орешках.

user posted image
bestxp
Ну не использовать AR уровне соглашения по проекту smile.gif не пропускать ревью если юзают AR, повесить хуки на коммиты) Посмеялись, и дальше)

DDD это имхо не про то что у вас там моделька не соотвествует, вопрос как это подать, AR не самый оптимальный вариант, и проще тогда было бы использовать построитель запросов что есть в YII, а я верю что он есть
twin
Я думал никто не владеет)) Померла думал темка.
Да, я разобрался и сделал иначе. Нафига тянуть AR, когда она сама наследует ActiveQuery smile.gif
Со всеми фишками плана связей, жадных и отложенных загрузок и прочей фигни. Как раз кроме мутаций.

Правда просто отнаследоваться от неё не получится, пришлось помудрить. Но результатом я доволен.

Теперь три варианта. DAO, QueryBuilder и AqtiveQuery на выбор, и никаких мутаций. Yii'шники будут довольны. smile.gif

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

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Настаивал, настаиваю и буду настаивать на своем. На кедровых орешках.

user posted image
Michael
Цитата (twin @ 28.07.2018 - 06:13)
Кто как считает, имеет это право на жизнь и чем грозит?

Для командной работы думаю будет ценным.
Я раньше думал о таком же, чтобы физически запрещать вызов на AR моделях ее БД-методов , везде кроме репозитария(слоя БД).

Можно делать не наследованием а трейтом, а в репозитории уже через рефлексию вызывать на модели ее базовый save/insert/update/delete.

Но проблема тут походу такая что люди часто хотят сохранять AR модель вместе с ее связанными моделями, через например такие расширения, а они из коробки такого не умеют.
Вот никто особо и не озабачивается этой защитой от дурака, да и вообще всей этой слоистой хренью в связке с yii

_____________
There never was a struggle in the soul of a good man that was not hard
twin
Цитата (Michael @ 17.08.2018 - 06:28)
Для командной работы думаю будет ценным.

Я бы уточнил - для командной работы с низкоквалифицированным персоналом.

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

А беда в том, что любой фреймворк провоцирует писать лапшу. 99% проектов на биг-фреймворках - процедурные, хотя и используют объекты. А это еще более жуткая лапша, чем просто процедурка. А еще и таблицы по овер 100 полей. Что мне и досталось в наследство, ибо "сочиняли" это все несколько поколений Yii-шных "дешевых" мидлов. Кроме того, проект намертво прирос к Yii-1. Теперь приходится переписывать с нуля на Yii-2.

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

Вот и встала задача:
1. Сохранить низкий порог вхождения и невысокие требования к персоналу
2. Сделать проект прозрачным для заказчика
3. Отвязать его от фреймворка, но оставить синтаксис (см. п. 1).
4. Сделать "защиту от дурака" хотябы в плане архитектуры.

Вот тут решение с CQRS оказалось очень в строчку. Правда пришлось помудрить.

С одной стороны (чтение) все плюшки AR со связями, ленивой загрузкой и так далее, но без возможности изменения базы данных (читай: без возможности костылить и создать импеданс). С другой стороны (запись) - все прелести DDD, но без плюшек чтения (опять же без возможности костылить, сделав из сервиса God-object).

В итоге получились штатный AQ с его привычным синтаксисом с одной, и легкий datamaper без использования монструозных Доктрин - с другой. При этом сохранилась привычная плоская РСУБД.

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

Ну и слоистая архитектура. Это гораздо больше свободы в использовании сторонних библиотек. Я вообще поставил голый Yii2, без всяких Advanced. Это сохранило базовый синтаксис и дало возможность юзать любые либы. А так как там все через адаптеры, то и с другими можно работать как с Yii-шными.

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

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

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

Так что пишите велосипеды! Это очень полезно, всегда говорил. smile.gif

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

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Настаивал, настаиваю и буду настаивать на своем. На кедровых орешках.

user posted image
Michael
Цитата (twin @ 18.08.2018 - 03:50)
В итоге получились штатный AQ с его привычным синтаксисом с одной, и легкий datamaper без использования монструозных Доктрин - с другой. При этом сохранилась привычная плоская РСУБД.

Так что, оставил вот этот переопределенный метод как в первом сообщении?
И не используешь нативное сохранение AR через ->save() ?

_____________
There never was a struggle in the soul of a good man that was not hard
twin
Нет, я вытащил чистую ActiveQuery. Вообще три варианта. Первый DAO, там запрещен метод execute()
<?php 

namespace Core\domain\system\yii2\readers;

use Yii;
use yii\db\Command;

/**
* Ридер DAO Yii2
*
*/

class CommandReader extends Command
{

/**
* Использование чистых запросов
*
*
@param string $query
*/

public function createCommand($query)
{
$this->db = Yii::$app->db;
$this->setSql($query);
return $this;
}

/**
* Запрет модификации
*/

public function execute()
{
throw new \BadMethodCallException('Modifying the model in the read portion is forbidden.');
}
}

Второй чисто с Query. В нем по определению нет возможности использовать мутирующие запросы. А почему не напрямую - ибо адаптер. :)
<?php 

namespace Core\domain\system\yii2\readers;

use yii\db\Query;

/**
* Ридер для Query Yii2
*
*/

class QueryReader extends Query
{

}


Третий - с ActiveQuery:
<?php 

namespace Core\domain\system\yii2\readers;

use Yii;
use yii\db\ActiveQuery;
use Core\domain\system\Reader;

/**
* Ридер для ActiveQuery Yii2
*
*/

class ActiveQueryReader extends ActiveQuery
{
protected $attributes = [];

/**
* Связь один-ко-иногим
*
*
@param string $class
*
@param array $link
*
*
@return object Query
*/

public function hasMany($class, $link)
{
return $this->createRelationQuery($class, $link, true);
}

/**
* Связь один-к-одному
*
*
@param string $class
*
@param array $link
*
*
@return object Query
*/

public function hasOne($class, $link)
{
return $this->createRelationQuery($class, $link, false);
}

/**
*
@param string $class
*
@param array $link
*
@param boolean $multiple
*
*
@return object Query
*/

protected function createRelationQuery($class, $link, $multiple)
{
$query = $class::find();
$query->primaryModel = $this;
$query->link = $link;
$query->multiple = $multiple;
return $query;
}

/**
* Коннект
*/

public static function getDb()
{
return Yii::$app->db;
}

/**
* Получение связи
*/

public function getRelation($name, $throwException = true)
{
$getter = 'get'. $name;

try {
$model = new Reader::$modelClass;
$relation = $model->$getter();
} catch (UnknownMethodException $e) {
if ($throwException) {
throw new InvalidArgumentException(get_class($this) .' has no relation named "'. $name .'".', 0, $e);
}
}


if (method_exists($model, $getter)) {
return $relation;
}

return null;
}

/**
* Инициализация
*/

public static function instance()
{
return new static(Reader::$modelClass);
}

/**
* Инициализация
*/

public static function instantiate()
{
return instance();
}

/**
* Заполнение атрибутов модели результатом запроса
*
*
@param object $record
*
@param array $row
*
*
@return void
*/

public static function populateRecord($record, $row)
{
foreach ($row as $name => $value) {
$record->attributes[$name] = $value;
}
}


/**
* Заглушка на afterFind()
*/

public function afterFind(){}
}



Так как ActiveRecord, это надстройка над AqtiveQuery исключительно для мутаций, то тут это запрещено автоматически.

Ну и прокси-декоратор:
<?php

namespace Core\domain\system;

use Core\domain\interfaces\ServiceInterface;
use Core\domain\system\yii2\readers\CommandReader;
use Core\domain\system\yii2\readers\QueryReader;
use Core\domain\system\yii2\readers\ActiveQueryReader;

/**
* Базовый ридер
*/

class Reader
{
const PRIMARY_KEY = 'id';

public static $modelClass;
public static $tableName;
protected static $readModel;

/**
* Эмуляция конструктора для статики
*/

public static function init()
{
self::$modelClass = get_called_class();
self::$readModel = new ActiveQueryReader(self::$modelClass);
self::$tableName = static::tableName();
}

/**
*
@return string
*/

public static function tableName()
{
return basename(self::$modelClass);
}

/**
*
@return string
*/

public static function primaryKey()
{
return self::PRIMARY_KEY;
}

/**
* __call
*/

public function __call($method, $params)
{
if ($this instanceof ServiceInterface) {
$reader = new QueryReader;
} else {
self::init();
$reader = self::$readModel;
}

return call_user_func_array([$reader, $method], $params);
}

/**
* __callStatic
*/

public static function __callStatic($method, $params)
{
self::init();
return forward_static_call_array([ActiveQueryReader::class, $method], $params);
}

/**
* Использование чистых запросов
*
*
@param string $query
*/

public function createCommand($query)
{
return (new CommandReader)->createCommand($query);
}

/**
* Эмуляция find()
*/

public static function find()
{
self::init();
$model = new ActiveQueryReader(get_called_class());
return $model->select('*')
->
from(self::$tableName)
->
asArray();
}

/**
* Эмуляция findOne()
*/

public static function findOne($condition)
{
$condition = self::prepareCondition($condition);
return self::find()->where($condition)
->
limit(1)
->
one();
}

/**
* Эмуляция findAll()
*/

public static function findAll($condition)
{
$condition = self::prepareCondition($condition);
return self::find()->where($condition)
->
all();
}

/**
* Запрет чистых запросов через ActiveQuery
*/

public static function findBySql($sql = NULL)
{
throw new \BadMethodCallException('The findBySql() method is not allowed. Use createCommand()');
}

/**
* Подготовка условий
*/

protected static function prepareCondition($condition)
{
if (is_array($condition)) {

if (key($condition) === 0) {
return [static::primaryKey() => array_values($condition)];
}

return $condition;
}

return [static::primaryKey() => $condition];
}
}



Если от прокси отнаследован сервис, значит будет работать Query. Если модель, то AqtiveQuery.

Так что можно юзать все три варианта без возможности что-то сунуть в базу. За это отвечают доменные модели части write. Правда выбирать лучше что то одно. Хотя можно подумать и о синглтонах.

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

Нужно уважать мнение оппонета. Ведь заблуждаться - его святое право.

Настаивал, настаиваю и буду настаивать на своем. На кедровых орешках.

user posted image
Быстрый ответ:

 Графические смайлики |  Показывать подпись
Здесь расположена полная версия этой страницы.
Invision Power Board © 2001-2024 Invision Power Services, Inc.