Страницы

Поиск по вопросам

Показаны сообщения с ярлыком repository. Показать все сообщения
Показаны сообщения с ярлыком repository. Показать все сообщения

четверг, 13 февраля 2020 г.

Как проще всего сымитировать репозиторий (данные)

#c_sharp #repository #mocking


У меня есть интерфейс репозитория, например:

public interface IUserRepository
{
    IEnumerable ListUsers();
    User GetUserById(int id);
    void AddUser(User user);
    void DeleteUser(User user);
    void EditUser(User user);
}


где User, например, выглядит так:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable { get; set; }
    public string Email { get; set; }
} 


на всякий случай, 

public class Role 
{
    public int Id { get; set; }
    public string Name { get; set; }
}


Как проще всего сделать имитацию данного репозитория с фейковыми данными?


Если просто сделать singleton внутри которого определить List, то это меня
смущает, потому что будут отличия в поведении от реального репозитория. Если в реальном
репозиторие два раза сделать GetUser(id) (с одним и тем же id), то получим два экземпляра
объекта. А если в singleton-е или статическом классе так же будем 2 раза доставать
из List<>, то получаем один экземпляр объекта.
Если делать Text File или XML, то вроде как долго. Хотелось бы максимально простой
вариант найти. Но если это самый простой вариант, то скажите, тогда сам отвечу, что
получилось. Что касается SQLite, то тем более городить долго, не стоит того. 
Если использовать Моки, то смущает, что у меня почти нет с ними опыта работы, и то,
что данные (например, имя, Email) будут выглядеть не красиво. Кажется, в моей ситуации
проще самому ввести как-то данные для 5-10 пользоватей. 


UPDATE: Сохранять состояние достаточно в рамках одного сеанса работы программы. После
ее выключения, можно сбрасывать добавленных или измененных User-ов
    


Ответы

Ответ 1



Статический класс, но отдавать и сохранять в нём всегда копии объектов, а не сами объекты. Естественно, имеются в виду глубокие копии.

Ответ 2



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

пятница, 27 декабря 2019 г.

Имплементация Repository паттерна в Laravel

#laravel #шаблоны_проектирования #php #repository


Всем привет!
С недавних пор изучаю Laravel 4 и его возможности. Стала задача имплементировать
паттерн Repository, чтобы вынести логику работы с бд туда. И вот тут столкнулся с рядом
неудобств или непониманием, как правильно все организовать. Общий вопрос у меня звучит
примерно так: возможна ли реализация и применение этого паттерна в Laravel без лишней
головной боли и стоит ли оно того?
Вопрос хотел бы разбить на несколько частей, которые вызвали у меня смятения.
1) В Laravel есть возможность на стадии определения роута биндить модель в качестве
параметра контроллера (например):
// routes.php
Route::bind('article', function($slug)
{
    return Article::where('slug', $slug)->first();
});

Route::get('articles/{article}', 'ArticlesController@getArticle');

// controllers/ArticlesController.php
class ArticlesController extends BaseController {

    public function getArticle(Article $article)
    {
        return View::make('article.show', compact('article'));
    }
}

Если я хочу использовать паттерн Repository, то я не могу использовать такой подход,
т.к. в этом случае контроллер явно будет знать о существовании модели Article? Правильно
ли будет переписать этот пример с использованием Repository таким образом:
// routes.php
Route::get('articles/{slug}', 'ArticlesController@getArticle');

// controllers/ArticlesController.php
class ArticlesController extends BaseController {

    private $article;

    public function __construct(ArticleRepository $article) {
        $this->article = $article;
    }

    public function getArticle($slug)
    {
        $article = $this->article->findBySlug($slug);

        return View::make('article.show', compact('article'));
    }
}

2) Допустим, мой вариант из предыдущего пункта с применением Repository оказался
удачным. Теперь я хочу, чтобы при просмотре статьи у нее увеличивался счетчик просмотров,
при этом я хочу вынести эту обработку в Event. То есть код будет следующим:
// routes.php
Route::get('articles/{slug}', 'ArticlesController@getArticle');

// controllers/ArticlesController.php
class ArticlesController extends BaseController {

    private $article;

    public function __construct(ArticleRepository $article) {
        $this->article = $article;
    }

    public function getArticle($slug)
    {
        $article = $this->article->findBySlug($slug);
        Events::fire('article.shown');

        return View::make('articles.single', compact('article'));
    }
}

// некий subscriber, подписанный на события
class ArticleSubscriber {

    public function onShown()
    {
        // почему нету реализации, описано ниже
    }

    public function subscribe($events)
    {
        $events->listen('article.shown', 'ArticleSubscriber@onShown');
    }

}

На данном этапе я снова был озадачен тем, как правильно реализовать обработку события.
Передать модель статьи $article в событие я не могу, т.к. это опять нарушает принципы
ООП и мой subscriber будет знать о существовании модели article. То есть сделать так
я не могу:
// controllers/ArticlesController.php
...
\Events::fire('article.shown', $article);
...

// некий subscriber, подписанный на события
...
public function onShown(Article $article)
{
    $article->increment('views');
}
...

С другой стороны, внедрять в subscriber репозиторий ArticleRepository я тоже не вижу
смысла, потому что мне снова придется сперва найти статью, а потом обновить ее счетчик,
в итоге получится лишний запрос к бд:
// controllers/ArticlesController.php
...
Events::fire('article.shown', $slug);
...

// некий subscriber, подписанный на события
...
private $article;

public function __construct(ArticleRepository $articleRepository) 
{
    $this->article = $articleRepository;
}

public function onShown($slug)
{
    $article = $this->articleRepository->findBySlug($slug);
    $article->increment('views');
}
...

Более того, после того, как Event отработал (т.е. увеличил счетчик просмотров), необходимо,
чтобы контроллер знал об обновленной модели, т.к. в представлении нужно вывести обновленный
счетчик просмотров. Получается, что каким-то образом мне еще и необходимо вернуть новую
модель из Event, но не хотелось бы, чтобы Event становился обычным методом для обработки
какого-то действия (для этого ведь есть репозиторий) и возвращал какое-то значение.
Вдобавок ко всему, вы можете заметить, что моя последняя реализация onShow() снова
противоречит правилам паттерна Repository, но я не понимаю, как вынести эту логику
в репозиторий:
public function onShown($slug)
{
    $article = $this->articleRepository->findBySlug($slug);
    // НЕВЕРНО! т.к. Event не должен знать о том, что умеет модель в реализации Eloquent
    // $article->increment('views');
}

Можно ли найденную модель передать обратно в репозиторий и уже там увеличить ей счетчик
(имеется ввиду, не противоречит ли такой подход паттерну?)? Примерно так:
public function onShown($slug)
{
    $article = $this->articleRepository->findBySlug($slug);
    $this->articleRepository->updateViews($article);
}

// ArticleRepository.php
...
public function updateViews(Article $article) {
    $article->increment('views');
}
...

В качестве итога попробую сформулировать все компактнее:


При использовании паттерна
    Repository мне придется отказаться
    от передачи модели в контроллер и подобных удобностей?


Возможно ли при использовании
    репозитория держать в нем состояние
    модели и передавать его между
    сущностями (например, из фильтра в
    контроллер, из контроллера в Event и
    обратно) во избежание непотребных
    повторных обращений к бд, и
    правильный ли это будет подход
    (сохранение состояния модели)?


Такие дела, такие вот у меня вопросы. Хотелось бы услышать ответы, мысли, комментарии.
Быть может, я не так пытаюсь применить паттерн? Сейчас он вызывает больше головной
боли, нежели решает проблему дата маппинга.    


Ответы

Ответ 1



Приветствую! Раз Вам нужно инкрементировать счётчик всегда, когда открывается статья, то не лучше ли будет вызывать increment() непосредственно из метода репозитория findBySlug($slug) и делать вызов не от модели $article, а сделать это методом самого репозитория? Что касается подхода с использованием событийной модели, то тут можно предложить использовать принцип Command-Query separation с использованием того же Commander от Джеффри (и/или Querier, что в данном случае больше подойдёт по концепции). При этом в контроллере у Вас будет вызов команды (тут и далее пишу так, если Вы будете пользовать Commander): // controllers/ArticlesController.php class ArticlesController extends BaseController { public function getArticle($slug) { $article = $this->execute(ArticleShowCommand::class, compact('slug')); return View::make('article.show', compact('article')); } } В команде у Вас будет получение из репозитория и вызов сопутствующих событий: // Site/Commands/Article/ArticleShowCommandHandler.php use Laracasts\Commander\CommandHandler; use Laracasts\Commander\Events\DispatchableTrait; class ArticleShowCommandHandler implements CommandHandler { use DispatchableTrait; private $article; public function __construct(ArticleRepository $articleRepository) { $this->article = $articleRepository; } public function handle($command) { $article = $this->articleRepository->findBySlug($command->slug); $this->dispatchEventsFor($article); return $article ; } } А в репозитории, при выдаче данных, заводите нужное событие: // Site/Repositories/ArticleRepository.php public static function findBySlug($slug) { $article = Article::where('article_alias', $slug); $article->raise(new ArticleWarViewed($article)); return $job; } Ну и уже создание листнеров нужных и реализация там нужной логики по инкрементации просмотров и прочего - смотрите README.md в репозитории...

Ответ 2



http://fideloper.com/hexagonal-architecture

вторник, 10 декабря 2019 г.

DDD repository/facade implementation + bounded context relationship

#c_sharp #шаблоны_проектирования #orm #repository #ddd


Вопрос в следующем, как лучше реализовать механизм Include?

Предположим, у нас есть Repository и нам требуется загрузить сущность со связанными
элементами, в сервисе мы можем использовать следующий код:

repo.Select().Include(sc => sc.Students).Include(sc => sc.Teachers);


или даже переопределить операцию select и передавать коллекцию Include Expression
в качестве параметра:

 repo.Select(sc => sc.Students, sc => sc.Teachers);


Cо временем наша школа становится платной, и мы создаем другой Bounded Context, в
котором реализована логика по оплате / начислениям.

Еще через время нам понадобилось подружить две модели и мы хотим в нашем классе Student
добавить NotMapped поле, которое бы определяло,например, имеет ли студент доступ к
библиотеке (логика определения находится в другой модели).

И нам, в момент получения данных, нужно сделать инъекцию для класса Student:

Student student = repo.Select(p => p.Id == id);
bool access = ChargeService.GetBalanceInfo(id);
student.Init(access);
return student;


Теперь о проблеме, когда ты используешь Include в коде самого сервиса,тебе придется
переписать кучу кода, чтобы сделать такую инъекцию.

Решение проблемы напрашивается - сделать отдельный метод в репозитории:

IEnumerable GetSchoolWithStudentsAndTeachers();


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

Можете подсказать, как бы вы поступили/поступаете в данной ситуации?  
    


Ответы

Ответ 1



В Вашем описании Student это класс предметной области, однако, он загружается непосредственно из базы с помощью Entity Framework. Entity Framework не может устанавливать значения свойств только для чтения, так что Вы должны сделать все свойства доступными для изменения. Но в предметной области некоторые поля неизменяемые, например, идентификатор сущности, дата и время создания сущности, и другие. Некоторые поля должны изменяться согласованно, например, новое состояние сущности и дата/время её последнего изменения. Именно такое согласованное изменение и называется инкапсуляцией. Student, у которого все поля открыты, можно назвать анемичной моделью, которую, например, Фаулер считает анти-паттерном (Anemic Domain Model). Было бы правильно спрятать состояние полностью внутрь, а снаружи оставить только методы, которые согласованно меняют состояние сущности. С чем же тогда будет работать Entity Framework? С Data Transfer Object'ом, который отражает состояние сущности: [Table("Student")] public class StudentDto { public int Id { get; set; } public int SchoolId { get; set; } public virtual SchoolDto School { get; set; } . . . } public class Student { private readonly StudentDto dto; internal Student(StudentDto dto) { this.dto = dto; School = new School(dto.School); } public int Id => dto.Id; public School School { get; } } В данном случае мы видим, что при загрузке студента из базы надо включить навигационное поле School. Это делает реализация класса-репозитория: public class EFStudentRepository: IStudentRepository { private readonly DbContextScope dbContextScope; public EFStudentRepository(DbContextScope dbContextScope) { this.dbContextScope = dbContextScope; } public Student ReadById(int id) { var dto = dbContextScope.Context .Students. .Include(x => x.School) .SingleOrDefault(x => x.Id == id); if (dto == null) throw new EntityNotFound(typeof(Student), id); return Student.From(dto); } } А вот как указывать репозиторию, какие навигационные свойства включать, а какие нет? Ответ DDD: это вопрос из другого уровня приложения. В предметной области нет понятия включенных классов, это термин уровня доступа к данным, причём термин конкретной библиотеки Entity Framework. В DDD есть понятие составных сущностей: агрегатов. Например, заказ и товарные позиции в этом заказе. В базе данных это две разных таблицы, а на уровне предметной области: класс Order со свойством коллекцией Products. И заказ, и товарная позиция суть сущности, но заказ это корневая сущность агрегата, а товарная позиция просто в него входит. Значит, при загрузке из базы надо загружать корень агрегата и все данные, которые должны быть в агрегате в рамках данного Bounded Context. Но считается, что одни агрегаты не должны содержать другие агрегаты, на них можно ссылаться только по идентификатору. Поскольку School — это, вероятно, отдельный агрегат, у Student должно быть свойство SchoolId вместо School. Такое ограничение позволяет упростить слой доступа к данным. С другой стороны, из-за этого приходится чаще посылать к базе отдельные запросы для загрузки связанных агрегатов. Это цена, которая в DDD платится за то, чтобы сделать код понятнее и чище. Таким образом, ответ из DDD звучит так: опишите агрегаты предметной области, достаточные для реализации её сценариев. Загрузку независимых агрегатов опишите в репозиториях в явном виде. Например, сценарий требует получения всех учителей студента, а учитель и студент — независимые агрегаты. В этом случае в репозитории учителей надо реализовать метод загрузки всех учителей по идентификатору студента: public interface ITeacherRepository { IReadOnlyCollection ReadAllByStudentId(int studentId); } Сервисы загружают все необходимые данные и выполняют групповую операцию. На уровне сервисов также задаются транзакции, если это нужно.

Ответ 2



Прошу прощения, если не в тему. Я из мира PHP и сталкивался с похожей проблемой, если я правильно ее понял. Насколько я понял, проблема в том, как изменять запросы в результате естественного развития проекта так, чтобы ничего не потерять и репозитории не разрастались. Один из способов решения этой задачи - это шаблон проектирования Спецификация (пример в вики возможно не самый удачный). Вот еще пример на .NET и на PHP. А вот пример реализации библиотеки на PHP (под C# тоже должно быть что-то такое). Возможно поможет понять общую идею. Вкратце. Мы создаем простые классы спецификаций и компонуя их получаем требуемый результат. Очень грубо по вашему промеру: repo.match(new AndSpec( new SchoolSpec(), new JoinStudents(), new JoinTeachers() )) три разных спецификации группируются в одну. Можно вызывать такую группировку из контроллера или из Query в контексте CQRS как предложил @Fynivx. Можно 3 спецификации объединить в одну большую SchoolWithStudentsAndTeachersSpec. Надеюсь это то что вас интересует. PS: Говоря о Bounded Context и DDD, я все больше прихожу к мысли, что в каждом контексте должны быть свои сущности и они ни как не должны взаимодействовать с сущностями в других контекстах. Статья на хабре о микросервисах натолкнула меня на мысль что Bounded Context нужно рассматривать как потенциальный микросервис. И соответственно связи между контекстами должны быть минимизированы. А если нам нужно в одном контексте использовать сущности из другого контекста, то мы должны использовать адапторы или маппить БД на свои собственные сущности которые отвечают требованиям нашего контекста. Суперглобальные агрегаты это плохо. Вопросы практического использования DDD хорошо описаны в книге Вон Вернона.

вторник, 23 апреля 2019 г.

Как проще всего сымитировать репозиторий (данные)

У меня есть интерфейс репозитория, например:
public interface IUserRepository { IEnumerable ListUsers(); User GetUserById(int id); void AddUser(User user); void DeleteUser(User user); void EditUser(User user); }
где User, например, выглядит так:
public class User { public int Id { get; set; } public string Name { get; set; } public IEnumerable { get; set; } public string Email { get; set; } }
на всякий случай,
public class Role { public int Id { get; set; } public string Name { get; set; } }
Как проще всего сделать имитацию данного репозитория с фейковыми данными?
Если просто сделать singleton внутри которого определить List, то это меня смущает, потому что будут отличия в поведении от реального репозитория. Если в реальном репозиторие два раза сделать GetUser(id) (с одним и тем же id), то получим два экземпляра объекта. А если в singleton-е или статическом классе так же будем 2 раза доставать из List<>, то получаем один экземпляр объекта. Если делать Text File или XML, то вроде как долго. Хотелось бы максимально простой вариант найти. Но если это самый простой вариант, то скажите, тогда сам отвечу, что получилось. Что касается SQLite, то тем более городить долго, не стоит того. Если использовать Моки, то смущает, что у меня почти нет с ними опыта работы, и то, что данные (например, имя, Email) будут выглядеть не красиво. Кажется, в моей ситуации проще самому ввести как-то данные для 5-10 пользоватей.
UPDATE: Сохранять состояние достаточно в рамках одного сеанса работы программы. После ее выключения, можно сбрасывать добавленных или измененных User-ов


Ответ

Статический класс, но отдавать и сохранять в нём всегда копии объектов, а не сами объекты. Естественно, имеются в виду глубокие копии.