Примеры кода на PHP, но вопрос не зависит от языка.
Я пытаюсь найти лучший способ разделить сервисный слой на несколько четко определенных слоев.
В приведенном ниже примере я загружаю аватар пользователя в кодировке base64 и показываю, как он будет проходить через слои. Я использую шаблон декоратора для имитации слоев.
Важный:
Данные, передаваемые на каждый слой, обычно перед тем, как переходить на следующий уровень, обычно каким-то образом изменяются, и это именно то, что я ищу. Единственное, что мне не нравится в этом, это то, что для обновления аватара вы должны сначала поговорить с ValidatedProfile
объект вместо сказать Profile
объект. Что-то в этом кажется странным, но я всегда мог иметь Profile
объект, который делегирует вызовы ValidatedProfile
,
class ValidatedProfile
{
private $verifiedProfile;
/**
* @param string $avatar Example: data:image/png;base64,AAAFBfj42Pj4
*/
public function updateAvatar($userId, $avatar)
{
$pattern = '/^data:image\/(png|jpeg|gif);base64,([a-zA-Z0-9=\+\/]+)$/';
if (!preg_match($pattern, $avatar, $matches)) {
// error
}
$type = $matches[1]; // Type of image
$data = $matches[2]; // Base64 encoded image data
$image = imagecreatefromstring(base64_decode($data));
// Check if the image is valid etc...
// Everything went okay
$this->verifiedProfile->updateAvatar($userId, $image);
}
}
class VerifiedProfile
{
private $profileCommander;
public function updateAvatar($userId, $image)
{
$user = // get user from persistence
if ($user === null) {
// error
}
// User does exist
$this->profileCommander->updateAvatar($user, $image);
}
}
class ProfileCommander
{
private $userService;
public function updateAvatar($user, $image)
{
$this->userService->updateAvatar($user, $image);
// If any processes need to be run after an avatar is updated
// you can invoke them here.
}
class UserService
{
private $persistence;
public function updateAvatar($user, $image)
{
$fileName = // generate file name
// Save the image to disk.
$user->setAvatar($fileName);
$this->persistence->persist($user);
$this->persistence->flush($user);
}
}
Вы могли бы тогда иметь Profile
класс как следующий:
class Profile
{
private $validatedProfile;
public function updateAvatar($userId, $avatar)
{
return $this->validatedProfile->updateAvatar($userId, $avatar);
}
}
Таким образом, вы просто говорите с экземпляром Profile
скорее, чем ValidatedProfile
что имеет больше смысла, я думаю.
Есть ли лучшие и более общепринятые способы достижения того, что я пытаюсь сделать здесь?
Я думаю, что у вас слишком много слоев. Для такой операции должно хватить двух основных объектов. Вам нужно проверить ввод аватара и как-то сохранить его.
Поскольку вам необходимо проверить идентификатор пользователя и он каким-то образом связан с постоянством, вы можете делегировать его объекту UserService.
interface UserService {
/**
* @param string $userId
* @param resource $imageResource
*/
public function updateAvatar($userId, $imageResource);
/**
* @param string $userId
* @return bool
*/
public function isValidId($userId);
}
Эта проверка на действительный идентификатор пользователя должна быть частью проверки запроса. Я бы не стал делать отдельный шаг вроде проверки. Таким образом, UserAvatarInput может справиться с этим (реализация валидации является лишь примером) и небольшим методом-оберткой, чтобы сохранить все это.
class UserAvatarInput {
/**
* @var UserService
*/
private $userService;
/**
* @var string
*/
private $userId;
/**
* @var resource
*/
private $imageResource;
public function __construct(array $data, UserService $service) {
$this->userService = $service; //we need it for save method
$errorMessages = [];
if (!array_key_exists('image', $data)) {
$errorMessages['image'] = 'Mandatory field.';
} else {
//validate and create image and set error if not good
$this->imageResource = imagecreatefromstring($base64);
}
if (!array_key_exists('userId', $data)) {
$errorMessages['userId'] = 'Mandatory field.';
} else {
if ($this->userService->isValidId($data['userId'])) {
$this->userId = $data['userId'];
} else {
$errorMessages['userId'] = 'Invalid user id.';
}
}
if (!empty($errorMessages)) {
throw new InputException('Input Error', 0, null, $errorMessages);
}
}
public function save() {
$this->userService->updateAvatar($this->userId, $this->imageResource);
}
}
Я использовал объект исключения для передачи сообщений проверки.
class InputException extends Exception {
private $inputErrors;
public function __construct($message, $code, $previous, $inputErrors) {
parent::__construct($message, $code, $previous);
$this->inputErrors = $inputErrors;
}
public function getErrors() {
return $this->inputErrors;
}
}
Вот как клиент будет использовать это, например:
class UserCtrl {
public function postAvatar() {
try {
$input = new AvatarInput($this->requestData(), new DbUserService());
$input->save();
} catch (InputException $exc) {
return new JsonResponse($exc->getErrors(), 403);
}
}
}
Других решений пока нет …