Сервисный слой, как правильно?

Обсуждаем, как правильно строить приложения
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Сервисный слой, как правильно?

Сообщение Melodic »

Разбираюсь с проектирование приложения на yii2. Решил отказаться от ActiveRecord (разве что для админки оставить) и разделить всё по слоям.

Интерфейс модели пользователя

Код: Выделить всё

<?php
namespace app\core\models\user;
use app\core\models\city\CityInterface;
use app\core\models\country\CountryInterface;

interface UserInterface
{
    /**
     * Возвращает ID пользователя
     * @return integer
     */

    public function getId();

    /**
     * Устанавливает ID пользователя
     * @param  $id
     */
    public function setId($id);

    /**
     * Возвращает имя
     * @return string
     */
    public function getFirstName();

    /**
     * Возвращает фамилию
     * @return string
     */
    public function getLastName();

    /**
     * Возвращает отчество
     * @return string
     */
    public function getPatronymic();

    /**
     * Возвращает город
     * @return CityInterface
     */
    public function getCity();

    /**
     * Возвращает страну
     * @return CountryInterface
     */
    public function getCountry();

    /**
     * Возвращает дату регистрации пользователя
     * @return \DateTime
     */
    public function getRegistrationDate();

    /**
     * Возвращает email
     * @return string
     */
    public function getEmail();

    /**
     * Возвращает установленный пароль
     * @return string
     */
    public function getPassword();

    /**
     * @param $firstName string Устанвалвивет имя пользователя
     */
    public function setFirstName($firstName);

    /**
     * @param $lastName string Устанавливает фамилию пользователя
     */
    public function setLastName($lastName);

    /**
     * @param $patronymic string Устанавливает отчество пользователя
     */
    public function setPatronymic($patronymic);

    /**
     * @param $email string Устанавливает email пользователя
     */
    public function setEmail($email);

    /**
     * @param $password string Устанаваливает пароль
     */
    public function setPassword($password);


}
Реализация интерфейса пользователя

Код: Выделить всё

<?php
namespace app\core\models\user;
use yii\base\Model;
use yii\base\Object;
use app\core\models\city\CityInterface;
use app\core\models\country\CountryInterface;

class User extends Object implements UserInterface
{

    
    /**
     * @var $_id integer
     */
    private $_id;

    /**
     * @var $_firstName string
     */
    private $_firstName;
    /**
     * @var $_lastName string
     */
    private $_lastName;
    /**
     * @var $_patronymic string
     */
    private $_patronymic;
    /**
     * @var $_city CityInterface
     */
    private $_city;
    /**
     * @var $_country CountryInterface
     */
    private $_country;
    /**
     * @var $_registrationDate \DateTime
     */
    private $_registrationDate;
    /**
     * @var $_email string
     */
    private $_email;
    /**
     * @var $_password string
     */
    private $_password;


    /**
     * Возвращает имя
     * @return string
     */
    public function getFirstName()
    {
        return $this->_firstName;
    }

    /**
     * Возвращает фамилию
     * @return string
     */
    public function getLastName()
    {
        return $this->_lastName;
    }

    /**
     * Возвращает отчество
     * @return string
     */
    public function getPatronymic()
    {
        return $this->_patronymic;
    }

    /**
     * Возвращает город
     * @return CityInterface
     */
    public function getCity()
    {
        return $this->_city;
    }

    /**
     * Возвращает страну
     * @return CountryInterface
     */
    public function getCountry()
    {
        return $this->_country;
    }

    /**
     * Возвращает дату регистрации пользователя
     * @return \DateTime
     */
    public function getRegistrationDate()
    {
        return $this->_registrationDate;
    }

    /**
     * Возвращает email
     * @return string
     */
    public function getEmail()
    {
        return $this->_email;
    }

    /**
     * @param $firstName string Устанвалвивет имя пользователя
     */
    public function setFirstName($firstName)
    {
        $this->_firstName = $firstName;
    }

    /**
     * @param $lastName string Устанавливает фамилию пользователя
     */
    public function setLastName($lastName)
    {
        $this->_lastName = $lastName;
    }

    /**
     * @param $patronymic string Устанавливает отчество пользователя
     */
    public function setPatronymic($patronymic)
    {
        $this->_patronymic = $patronymic;
    }

    /**
     * @param $email string Устанавливает email пользователя
     */
    public function setEmail($email)
    {
        $this->_email = $email;
    }

    /**
     * @param $password string Устанаваливает пароль
     */
    public function setPassword($password)
    {
        $this->_password = $password;
    }

    /**
     * Возвращает ID пользователя
     * @return integer
     */
    public function getId()
    {
        return $this->_id;
    }

    /**
     * Устанавливает ID пользователя
     * @param  $id
     */
    public function setId($id)
    {
        $this->_id = $id;
    }
}
 
И есть сервис для этой модели.
Интерфейс сервиса модели юзера

Код: Выделить всё

namespace app\core\models\user;
interface UserServiceInterface
{

    /**
     * Возвращает пользователя по его ID
     * @param $id integer ID пользователя
     * @return UserInterface
     */
    public function findById($id);

    /**
     * Возвращает пользователя по его E-mail
     * @param $email string Email пользователя
     * @return UserInterface
     */
    public function findByEmail($email);


    /**
     * Возвращает всех пользователей
     * @return UserInterface[]
     */
    public function findAll();

    /**
     * Сохраняет пользователя
     * @param UserInterface $user
     *
     */
    public function save(UserInterface $user);

    /**
     * Удаляет пользователя
     * @param UserInterface $user
     */
    public function delete(UserInterface $user);

}
и реализация этого интерфейса сервиса

Код: Выделить всё

<?php


namespace app\core\models\user;


use yii\db\Command;
use yii\db\Query;

class UserService implements UserServiceInterface
{

    /**
     * Возвращает пользователя по его ID
     * @param $id integer ID пользователя
     * @return UserInterface
     */
    public function findById($id)
    {
        return $this->findOneByAttribute('id', $id);

    }

    /**
     * Возвращает пользователя по его E-mail
     * @param $email string Email пользователя
     * @return UserInterface
     */
    public function findByEmail($email)
    {
        return $this->findOneByAttribute('email', $email);
    }

    /**
     * Возвращает всех пользователей
     * @return UserInterface[]
     */
    public function findAll()
    {

        $query = new Query();
        $rows = $query->all();
        $users = [];
        foreach ($rows as $row) {
            $users = $this->load($row);
        }
        return $users;

    }

    /**
     * Сохраняет пользователя
     * @param UserInterface $user
     *
     */
    public function save(UserInterface $user)
    {
        $command = new Command();
        $columns = [
            'first_name' => $user->getFirstName(),
            'last_name' => $user->getLastName(),
            'patronymic' => $user->getPatronymic(),
            'email' => $user->getEmail(),
        ];

        if ($user->getPassword() != null) {
            $columns['password_hash'] = \Yii::$app->security->generatePasswordHash($user->getPassword());
        }
        if ($user->getId() == null) {
            $command->insert(UserActiveRecord::tableName(), $columns);
            $user->setId(\Yii::$app->db->getLastInsertID());
        } else {
            $command->update(UserActiveRecord::tableName(), $columns, ['id' => $user->getId()]);
        }
    }

    /**
     * Удаляет пользователя
     * @param UserInterface $user
     */
    public function delete(UserInterface $user)
    {
        $command = new Command();
        $command->delete(UserActiveRecord::tableName(), ['id' => $user->getId()]);
    }

    private function load($result)
    {
        $user = new User();
        $user->setId($result['id']);
        $user->setEmail($result['email']);
        $user->setFirstName($result['first_name']);
        return $user;
    }

    private function findOneByAttribute($attribute, $value)
    {
        $query = new Query();
        $row = $query->andWhere([$attribute => $value])->one();
        if ($row) {
            return $this->load($row);
        } else {
            return null;
        }
    }
}
 
Подскажите, в правильном ли я направлении двигаюсь? Не покидает чувство, что что-то не так :)
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

неплохо. в чем сомнения?
замечания:
- не надо использовать $_ в приватных переменных - ноль смысла
- интерфейс для сущности - оверхед. обычно все строится над сущностью, но ошибки тут нет
- назовите сервис UserYiiDaoService/UserMysqlService, поскольку он основан на йиишной реализации - это для понимания для чего интерфейсы. Но правильнее вынести бд-слой, инжектя его в сервисы.
-

Код: Выделить всё

$user->getId() == null
будет true при 0, а 0 валидный id. Надо === (и вообще приучайте себя к === везде, где сами пишете код)
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):неплохо. в чем сомнения?
замечания:
- не надо использовать $_ в приватных переменных - ноль смысла
- интерфейс для сущности - оверхед. обычно все строится над сущностью, но ошибки тут нет
- назовите сервис UserYiiDaoService/UserMysqlService, поскольку он основан на йиишной реализации - это для понимания для чего интерфейсы. Но правильнее вынести бд-слой, инжектя его в сервисы.
-

Код: Выделить всё

$user->getId() == null
будет true при 0, а 0 валидный id. Надо === (и вообще приучайте себя к === везде, где сами пишете код)
- $_ - взял из https://github.com/yiisoft/yii2/blob/ma ... e-style.md
- Т.е. по сути в интерфейсе сущности смысла нет вообще?
- Есть ли смысл выносить в отдельный слой БД, если я точно знаю,что всегда будет MySQL?
- Ок, учту.

Правильно ли, что я в методе save() сервиса хеширую пароль?
Как быть в случае, если добавится новый модуль(который по сути будет зависеть от этого "core") и этому новому модулю потребуется дополнительный метод выборки в сервисе? Создать новый сервис и унаследовать его от UserService?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а): - $_ - взял из https://github.com/yiisoft/yii2/blob/ma ... e-style.md
там дилетанты сидят. пср почитайте и больше ничего не придумывайте
Melodic писал(а): - Т.е. по сути в интерфейсе сущности смысла нет вообще?
есть, если это модуль типа yii2-user-module, но в своем проекте сущности можно делать без интерфейсов.
Melodic писал(а): - Есть ли смысл выносить в отдельный слой БД, если я точно знаю,что всегда будет MySQL?
тогда нет смысла вообще в интерфейсе - зачем он вам если у вас всегда будет одна реализация? на самом деле приучите себя забыть о слове "всегда", тогда в нужный момент вы будете готовы к неожиданному рефакторингу проекта) я сам недавно три базы сменил, пока не понял что мне нужно - благодаря интерфейсам репозиториев мигрировал на каждую в течении пяти минут.
Melodic писал(а):Правильно ли, что я в методе save() сервиса хеширую пароль?
в такой реализации сервисного слоя сойдет (но само хэширование я бы вынес в отдельный сервис PasswordHasher с инджектом в UserService) - см. ниже предложение по рефакторингу
Melodic писал(а):Как быть в случае, если добавится новый модуль(который по сути будет зависеть от этого "core") и этому новому модулю потребуется дополнительный метод выборки в сервисе? Создать новый сервис и унаследовать его от UserService?
выбираем композицию вместо наследования. Наследование используем только для детей абстрактного класса.
Почему нельзя добавить метод в этот же сервис?

Вообще рекомендую работу с сущностями вынести в репозиторий (у вас по сути ваш сервис и есть репозиторий), который непосредственно только с БД и будет работать, а в сервисы оборачивайте общие таски типа UserService::create, где будет хэширование пароля, отсылка писем, создание событий, сущности и передача ее в UserRepository::save(User $user);
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):выбираем композицию вместо наследования. Наследование используем только для детей абстрактного класса.
Почему нельзя добавить метод в этот же сервис?
Получается так, что когда добавляем новый модуль, которому нужен специфический метод выборки пользователей и такого метода нет в UserService, придётся редактировать напрямую UserService? Т.е. добавляя новый модуль, нужно будет редактировать ядро(модуль core, в котором содержится UserService),грубо говоря? Или при разработке core нужно учесть все возможные варианты выборки?

Можно по подробнее про композицию в данном случае?
Спасибо за ответы :)
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а): - Есть ли смысл выносить в отдельный слой БД, если я точно знаю,что всегда будет MySQL?
тогда нет смысла вообще в интерфейсе - зачем он вам если у вас всегда будет одна реализация? на самом деле приучите себя забыть о слове "всегда", тогда в нужный момент вы будете готовы к неожиданному рефакторингу проекта) я сам недавно три базы сменил, пока не понял что мне нужно - благодаря интерфейсам репозиториев мигрировал на каждую в течении пяти минут.
Я имел ввиду, что БД всегда будет MySQL, но источник данных может смениться на файл или API
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):
Melodic писал(а): - Есть ли смысл выносить в отдельный слой БД, если я точно знаю,что всегда будет MySQL?
тогда нет смысла вообще в интерфейсе - зачем он вам если у вас всегда будет одна реализация? на самом деле приучите себя забыть о слове "всегда", тогда в нужный момент вы будете готовы к неожиданному рефакторингу проекта) я сам недавно три базы сменил, пока не понял что мне нужно - благодаря интерфейсам репозиториев мигрировал на каждую в течении пяти минут.
Я имел ввиду, что БД всегда будет MySQL, но источник данных может смениться на файл или API
репозитории вводите - так удобнее.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):выбираем композицию вместо наследования. Наследование используем только для детей абстрактного класса.
Почему нельзя добавить метод в этот же сервис?
Получается так, что когда добавляем новый модуль, которому нужен специфический метод выборки пользователей и такого метода нет в UserService, придётся редактировать напрямую UserService? Т.е. добавляя новый модуль, нужно будет редактировать ядро(модуль core, в котором содержится UserService),грубо говоря? Или при разработке core нужно учесть все возможные варианты выборки?

Можно по подробнее про композицию в данном случае?
Спасибо за ответы :)
Создаем новый UserService, реализующий интерфейс из модуля, инжектим туда старый сервис из модуля - вуаля. Мы в нашем новом сервисе имеем доступ ко всем старым методам и можем добавлять новые.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):
zelenin писал(а):выбираем композицию вместо наследования. Наследование используем только для детей абстрактного класса.
Почему нельзя добавить метод в этот же сервис?
Получается так, что когда добавляем новый модуль, которому нужен специфический метод выборки пользователей и такого метода нет в UserService, придётся редактировать напрямую UserService? Т.е. добавляя новый модуль, нужно будет редактировать ядро(модуль core, в котором содержится UserService),грубо говоря? Или при разработке core нужно учесть все возможные варианты выборки?

Можно по подробнее про композицию в данном случае?
Спасибо за ответы :)
Создаем новый UserService, реализующий интерфейс из модуля, инжектим туда старый сервис из модуля - вуаля. Мы в нашем новом сервисе имеем доступ ко всем старым методам и можем добавлять новые.
Т.е. в новом сервисе делаем что то типа такого?

Код: Выделить всё

class NewUserService implements UserServiceInterface{
  private $userService;
  public __counstruct(UserServiceInterface $service){
    $this->userService = $service;
  }
  ..........
  public function methodNameFromUserInterface($id){
     return $this->userService->methodNameFromUserInterface($id); 
  }
  ..........
}
 
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

верно. декоратор
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

Правильно ли будет, если в модели User убрать все get/set методы и сделать все поля класса публичными и пользоваться механизмом getters/setters от Yii, что бы не городить кучи методов get/set?

Также, где проводить валидацию входящих данных?
К примеру, в самом UserService::registration($login,$password) или в сервис уже валидные данные должны приходить?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):Правильно ли будет, если в модели User убрать все get/set методы и сделать все поля класса публичными и пользоваться механизмом getters/setters от Yii, что бы не городить кучи методов get/set?
неправильно. сущность обладает поведением, поэтому публичных атрибутов иметь обычно не должна. Публичными атрибутами могут обладать только объекты для переноса данных (Data-transfer object) без поведения. И вообще чем меньше будете yii-шных штучек использовать тем лучше. Используйте только реализации каких-то штук типа кэша, БД, валидации если хотите итд.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):Также, где проводить валидацию входящих данных?
К примеру, в самом UserService::registration($login,$password) или в сервис уже валидные данные должны приходить?
например UserRegistrationValidator::validate(), внутри которого необходимая вам валидация. Валидатор инжектится в сервис.
Круто завязать метод registration (лучше registrate - глагол, общайтесь императивами) и validate на UserRegistrationDto с публичными полями login и password.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):Также, где проводить валидацию входящих данных?
К примеру, в самом UserService::registration($login,$password) или в сервис уже валидные данные должны приходить?
например UserRegistrationValidator::validate(), внутри которого необходимая вам валидация. Валидатор инжектится в сервис.
Круто завязать метод registration (лучше registrate - глагол, общайтесь императивами) и validate на UserRegistrationDto с публичными полями login и password.
Как то так?

Код: Выделить всё

<?php
interface UserRegistrationValidatorInterface{
    public function validate(UserRegistrateDTO $dto);
}

class UserService implements UserServiceInterface{
    private $userRegistrationValidator;
    public function __counstruct(UserRegistrationValidatorInterface $validator){ 
        $this->userRegistrateValidator = $validator;
    }
    
    public function registrate(UserRegistrateDTO $dto){
        if($this->userRegistrationValidator->validate($dto)){
            .......
        }
    }
}
 
Не избыточным будет интерфейс для валидатора?
Как мне в таком случае получить ошибки валидации что бы отобразить где то?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

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

Код: Выделить всё

if($this->userRegistrationValidator->validate($dto)){
            .......
        } else {
        $errors = $this->userRegistrationValidator->getErrors(); // возвращается итерируемый объект ValidationErrors
        }
         
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):Не избыточным будет интерфейс для валидатора?
ну если подумать, что можно будет написать расширенный валидатор и переопределить его, то интерфейс имеет смысл.
Melodic писал(а):Как мне в таком случае получить ошибки валидации что бы отобразить где то?

Код: Выделить всё

if($this->userRegistrationValidator->validate($dto)){
            .......
        } else {
        $errors = $this->userRegistrationValidator->getErrors(); // возвращается итерируемый объект ValidationErrors
        }
А как в таком случае "на верх" эти ошибки вернуть?
К примеру, метод UserService::registrate() вызывается из контроллера, и эти ошибки нужно отобразить в View.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

во вьюшку спускать UserRegistrationResponse с dto и errors
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):во вьюшку спускать UserRegistrationResponse с dto и errors
UserRegistrationResponse это DTO и его будет возвращать UserService::registrate(), верно?

Код: Выделить всё

<?php
class UserRegistrationResponse{
    public $success;
    public $errors;
}
 
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):во вьюшку спускать UserRegistrationResponse с dto и errors
UserRegistrationResponse это DTO и его будет возвращать UserService::registrate(), верно?

Код: Выделить всё

<?php
class UserRegistrationResponse{
    public $success;
    public $errors;
}
не очень нравится. надо подумать
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

предлагаю так:

Код: Выделить всё

public function actionTest()
{
$dto = UserRegistrationDto::createFromRequest(Yii::$app->request);

if($this->userRegistrationValidator->validate($dto)) {
    $errors = new ValidationErrors();
    $this->userRegistrationService->registrate($dto);
} else {
    $errors = $this->userRegistrationValidator->getErrors();
}

return $this->render('test', ['response' => new UserRegistrationResponse($dto, $errors)]);
}
 
Закрыто