Свою первую апишку я написал лет 7-8 назад и это был первый блин. В целом этот блин прошел кучу испытаний и модернизаций и получилось, что то вполне вменяемое. Даже сейчас я понимаю, что это ядро актуально и его можно развивать дальше и оптимизировать к текущим реалиям (пока нет подходящего проекта).
Как можно догадаться это было RPC. Наверно стоит начать с того, что я выделил ряд слоев (какие то можно опустить, какие то добавить).
-
получение запроса в текст
-
конвертация текста в ассоциативный массив
-
конвертация ассоциативного массива в класс запроса (понять имя метода, данные метода, токен и другие доп. данные (язык, версия, …))
-
валидация наличия метода
-
права доступа (не доделал)
-
валидация (и преобразование данных в объект)
-
DI контейнер (не доделал на php)
-
логика метода (возвращает статус ответа и данные)
-
конвертируем статус объекта и данные в структуру ответа
-
преобразовываем структуру ответа в текст
Таким образом у нас получилось 10 слоев которые нам надо реализовать и которые нам кажутся очень сложными (наверно по этой причине RPC называют сложной и это очень сильно отталкивает). На самом же деле большая часть этих слоев уходит в ядро и вам не нужно их трогать. В моем случае остается зарегистрировать метод, описать схему валидации (иногда дописать метод валидации и преобразования) и написать саму логику. До схемы ответа, обычно руки доходят в последний момент…
Отдельным слоем можно добавить работу с файлами, их валидацию. У меня этот кейс встречал не так часто, по этому идем дальше.
Примеры написания методов
Простой пример метода на Go
Я не являюсь супер go разработчиком и тут было конвертировано RPC ядро с php на go за неделю. Народ на go очень специфичный и я не думаю поймать бурю аплодисментов, но пример думаю стоит вставить.
package methodGroup2 import ( "project-my-test/example/rpcApp/methodRequestShemaItem" "project-my-test/src/rpc" "project-my-test/src/rpc/rpcInterface" "project-my-test/src/rpc/rpcStruct" ) type MethodMyTest struct { rpc.RpcMethod Data struct { Name *string Email *string } Logger rpcInterface.Logger } func (r *MethodMyTest) GetRequestSchema() map[string]rpcStruct.ReformSchema { rs := make(map[string]rpcStruct.ReformSchema) rs["Name"] = methodRequestShemaItem.GROUP2_NAME() rs["Email"] = methodRequestShemaItem.GROUP2_EMAIL() return rs } func (r *MethodMyTest) Run() rpcInterface.Response { r.Logger.Info("test Info") r.Logger.Warning("test Warning") r.Logger.Error("test Error") r.Logger.Debug("test Debug") r.Response.SetData("test::string", "string") r.Response.SetData("test::int", 20) r.Response.SetData("test::Name", r.Data.Name) r.Response.SetData("test::Email", r.Data.Email) r.Response.GetError().SetCode("ERROR") return r.Response }
Пример авторизации на php
<?php namespace CustomRpc\Method\RpcUser; class RpcUserAuth extends \Oploshka\Rpc\Method { public static function description(){ return <<<DESCRIPTION Авторизация пользователя (пользователь должен подтвердить почту!) Пример объекта auth { "login": "test@mail.ru", "password": "12345678" } При успешном ответе вернется session. DESCRIPTION; } public static function validate(){ return [ 'auth' => ['type' => 'array', 'req' => true, 'validate' => [ 'login' => ['type' => 'string' , 'validate' => [], 'req' => true ], 'password' => ['type' => 'string', 'validate' => [], 'req' => true ], ] ], ]; } public function run(){ $RpcUser = \CustomRpc\EntityQuery\RpcUserQuery::auth($this->Data['auth']['login'], $this->Data['auth']['password']); if(!$RpcUser){ // TODO: add error auth count $this->Response->setError('ERROR_LOGIN_PASSWORD'); return; } $userSessionToken = \CustomRpc\EntityQuery\RpcUserSessionQuery::addNewUserSession($RpcUser); $this->Response->setData('session', $userSessionToken); $this->Response->setError('ERROR_NO'); } public static function return(){ return [ 'session' => ['type' => 'string' , 'validate' => [], 'req' => true ], ]; } }
Пример обновления данных пользователя
<?php namespace CustomRpc\Method\RpcUser; class RpcUserInfoUpdate extends \Oploshka\Rpc\Method { public static function description(){ return <<<DESCRIPTION Обновление своих данных gender = 'NULL' 'MALE' 'FEMALE' dateOfBirth = date format YYYY-MM-DD region - отправляй regionId image - отправляется посредством multipart в отдельном поле (от запроса), в бинарном виде. DESCRIPTION; } public static function validate(){ return [ 'session' => ['type' => 'rpcUserSession' , 'validate' => [], 'req' => true ], 'nickname' => ['type' => 'string' , 'validate' => [], 'req' => false ], 'region' => ['type' => 'region' , 'validate' => [], 'req' => false ], 'gender' => ['type' => 'string' , 'validate' => [], 'req' => false ], 'dateOfBirth' => ['type' => 'string' , 'validate' => [], 'req' => false ], 'aboutUs' => ['type' => 'string' , 'validate' => [], 'req' => false ], ]; } public function run(){ $RpcUser = $this->Data['session']; $updateField = []; if( $this->Data['nickname'] ){ // проверить зарегистрированность nickname if ( \CustomRpc\EntityQuery\RpcUserQuery::checkRegisteredNickname( $this->Data['nickname'] ) ) { $this->Response->setError('ERROR_NICKNAME_REGISTERED'); return; } $updateField['nickname'] = $this->Data['nickname']; } $this->Data['region'] && $updateField['region_id'] = $this->Data['region']->id; $this->Data['gender'] && $updateField['gender'] = $this->Data['gender']; $this->Data['dateOfBirth'] && $updateField['date_of_birth'] = $this->Data['dateOfBirth']; $this->Data['aboutUs'] && $updateField['about_us'] = $this->Data['aboutUs']; $file = \CustomRpc\Entity\RpcUserHelper::loadAndSaveImage('image'); $file && $updateField['image_id'] = $file->id(); \CustomRpc\EntityQuery\RpcUserQuery::update($RpcUser, $updateField); $this->Response->setError('ERROR_NO'); } public static function return(){ return []; } }
Весь процесс написания RPC метода сводиться к тому, что нам нужно написать класс (структуру в случае с go), описать схему валидации (можно описать структуру данных в отдельном классе или добавить свойства в текущий класс) и саму логику.
Были идеи, что RPC ядро должно запустить чистую логику, которая ничего не знает про api, но обычно, в этом не было большого смысла (это увеличивало количество кода, а переиспользования как такового не было)
По итогу мы получаем:
-
гибкость. Можно получать и отдавать запрос в разных форматах (на логику, это не влияет)
-
хорошую отказоустойчивость. Ошибки обрабатываются ядром RPC и вероятность падения сведена к миниму. В любом случае мы можем добавить дополнительную проверку/обработку, не меняя код методов.
-
простоту тестирования. Не нужно дергать сетевой слой. Есть возможность написать общие тесты для всех методов. Сам метод можно протестировать, передав сырые данные или же валидный объект.
-
документация RPC методов. В целом можно написать адаптер для swager’a или генерировать простенький html (я выбрал последнее).
Заключение
Я показывал пример RPC разным людям, кому то такой подход нравиться, кому то нет, кто то пытался сделать подобное. В любом случае есть куда расти и есть идеи для реализации. Примеры не тянут на оскар и демонстрируют как можно писать. RPC ядро на go можно посмотреть тут, на php тут (develop более свежее).
ссылка на оригинал статьи https://habr.com/ru/articles/594825/
Добавить комментарий