itrostik.tech
250 subscribers
3 links
Это канал, в котором я делюсь техническими знаниями про IT и Backend: AI, Go, PostgreSQL, Kafka, Redis и многое другое!

Полезные ссылки:

@itrostik52 — основной канал
@itrostik_services — мои услуги
@itrostik_chat — бесплатный чат айтишников
Download Telegram
Channel created
SOLID, DRY, KISS, YAGNI: ЧТО ЭТО ЗА АРХИТЕКТУРНАЯ ШЛЯПА? 💻

Сегодня хочу рассказать, что это за буквы такие и всегда ли удаётся их применять на работе (спойлер: не всегда)

SOLID

Пять букв, которые спрашивают на собеседовании. Фактически — это 5 принципов, которых "нужно" придерживаться при написании кода, но это не значит, что ты обязан слепо следовать этим принципам, всегда есть исключения ради бизнес-логики проекта

1. S (Single Responsibility Principle) — принцип единственной ответственности, который означает, что ты должен писать код так, чтобы каждый модуль в твоём приложении отвечал за что-то конкретное, а каждая функция делала что-то одно. Логгер — логгирует, функция заказа — создаёт заказ. Я думаю, тут всё понятно

2. O (Open-Closed Principle) — принцип открытости-закрытости, при котором ты должен по возможности добавлять новое поведение, а не переписывать уже работающий код. То есть суть в том, чтобы проектировать код так, чтобы новое поведение можно было добавлять без постоянного переписывания уже работающей логики

Давай сразу на примере:

У нас есть Transfer с полем Balance, а также метод GetBalance, который просто возвращает этот Balance:

type Transfer struct {
Balance int
}

func (t *Transfer) GetBalance() int {
return t.Balance
}


И твоя задача — реализовать метод, который не просто вернёт баланс, но и проверит, что он больше нуля. И вот ты можешь залезть в метод GetBalance и изменить логику в нём, но намного правильнее создать новый метод CheckAndGetBalance и реализовать логику там следующим образом

func (t *Transfer) CheckAndGetBalance() (int, error) {
balance := t.GetBalance()
if balance <= 0 {
return 0, errors.New("check balance failed, balance <= 0")
}

return balance, nil
}


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

3. L (Liskov Substitution Principle) — принцип подстановки Барбары Лисков, который говорит о том, что если у тебя есть код, который работает с абстракцией (с интерфейсом, к примеру), то ты должен иметь возможность менять реализации и программа продолжит работать корректно

Опять же, пример:

Допустим, у нас есть интерфейс Animal, который имеет метод Eat()

type Animal interface {
Eat() string
}


Теперь мы создадим структуры Dog (собака) и Puppy (щенок)

type Dog struct {
Name string
}

type Puppy struct {
Name string
Age int
}


Теперь я могу реализовать метод Eat() у обеих структур, тем самым они будут реализовывать интерфейс Animal

func (d Dog) Eat() string {
return "I am dog, and I am eating"
}

func (p Puppy) Eat() string {
return "I am puppy, and I am eating"
}


А также я создам функцию EatAnimal, которая вообще ничего не будет знать о наших структурах и работать чисто с интерфейсом

func EatAnimal(a Animal) string {
return a.Eat()
}


И прикол в том, что если я теперь создам структуру Dog и Puppy, я смогу обе структуры передать в функцию EatAnimal и фактически логика никак не меняется от переданного аргумента, просто будет вызываться разная реализация

func main() {
puppy := Puppy{Name: "puppy", Age: 1}

dog := Dog{Name: "dog"}

EatAnimal(dog)
EatAnimal(puppy)
}


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

При этом недостаточно просто реализовать методы: реализация должна ещё и соблюдать ожидаемый контракт. Если Puppy.Eat() внезапно делает panic, формально интерфейс реализован, но принцип уже нарушается

4. I (Interface Segregation Principle) — принцип разделения интерфейсов, который говорит о том, чтобы вместо одного большого интерфейса делать несколько точечных. Для чего? Для того, чтобы клиент, который опирается на интерфейс, не зависел от ненужных ему методов

Фактически, вместо того, чтобы делать так:

type BaseService interface {
GetUser() User
GetOrder() Order
GetTransfer() Transfer
}

type UserHandler struct {
service BaseService // UserHandler не нуждается в GetOrder и GetTransfer
}


Ты делаешь следующим образом:

type UserService interface {
GetUser() User
}

type OrderService interface {
GetOrder() Order
}

type TransferService interface {
GetTransfer() Transfer
}

type UserHandler struct {
service UserService
}


Думаю тут понятно, если что, задавай вопросы! Едем дальше ⌨️

5. D (Dependency Inversion Principle) — принцип инверсии зависимостей, который говорит о том, что высокоуровневые и низкоуровневые модули должны зависеть от абстракций, а не друг от друга. Если непонятно, давай разбирать :)

Допустим, у нас есть сервис переводов:

type TransferService struct { 
pgRepository PostgresTransferRepository
}


И конкретная реализация репозитория, которая работает только с PostgreSQL:

type PostgresTransferRepository struct {
db *sql.DB
}

func (r PostgresTransferRepository) GetTransfer(id int) Transfer {
// идём в PostgreSQL
return Transfer{}
}


Это плохо, потому что мы зависим от конкретной реализации, и если нас завтра попросят поддержать MySQL, то нам придётся добавлять ещё и MySQLRepository в TransferService, а потом ещё и Redis, и MongoDB... короче, чтобы такого не было, мы можем положить в TransferService интерфейс, а не конкретную реализацию:

type TransferRepository interface { 
GetTransfer(id int) Transfer
}

type TransferService struct {
repository TransferRepository
}


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

DRY, KISS, YAGNI

DRY (Don't Repeat Yourself) — не повторяй сам себя. Короче, если код повторяется, посмотри, можешь ли ты вынести его в функцию. Также не дублируй одну и ту же логику в разных местах, выноси в общие хелперы/функции

KISS (Keep it simple, stupid) — не усложняй решение просто так, код должен быть максимально простым, насколько это возможно. И вообще, пиши код так, чтобы его было легко читать даже без комментариев (не говорю, что комментарии не нужны)

YAGNI (You Aren't Gonna Need It) — не реализуй то, что тебе сейчас не нужно. Ну, допустим, тебя попросили добавить ручку для создания заказа, а ты пошёл ещё и ручку удаления заказа написал. Зачем? Хрен знает, не делай так

А ГДЕ ЖЕ НАРУШАЮТСЯ ВСЕ ЭТИ ПРИНЦИПЫ НА ПРАКТИКЕ?

Да практически на каждом проекте есть нарушения того же SOLID, к примеру у меня на одном из проектов был некий TransferService, который одновременно вычислял среднюю цену, писал событие в брокер, создавал задачу в постгре и отправлял уведомление, и очевидно сервис слишком много делает (нарушение SRP), но когда у тебя маленький проект, то само соблюдение SOLID может только делать код сложнее и руинить сроки разработки

Стоит понимать, что, придя на работу, на проект, у тебя уже будет устоявшаяся кодовая база, и вряд ли она на 100% будет соответствовать всем этим принципам, а менять что-то слишком дорого по времени, да и кажется бессмысленным, потому что всё работает, рефакторинг займёт время, а реальной пользы бизнесу может вообще не принести

Как-то так. Это мой первый технический пост, который я написал руками, буду рад фидбэку, ну и спасибо, что прочитал(-а), это тебе 🏆

Кто я | Чат айтишников | Мои услуги
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10👍4🔥4
CONTEXT в GO или «ГАЛЯ, У НАС ОТМЕНА» ❌

Всем доброго дня, сегодня разберём, зачем нам вообще пакет «context» в Go и что интересного внутри там есть. Если тебя на собеседовании спрашивают про контекст в гошке и твой ответ звучит примерно так: «ну… это хуйня, которую я в функцию первым аргументом прокидываю», то этот пост для тебя

Что такое context

Это объект, который передаётся между функциями и хранит некую информацию, которая может пригодиться нескольким частям твоего приложения: допустим, к тебе пришёл запрос от клиента на покупку бургера, а у тебя приложение архитектурно построено так:

Handler -> Service -> Repository -> DB (1)

То есть в хэндлере ты принимаешь запрос, вызывая нужный сервис для обработки нового заказа, а сервис в свою очередь идёт в репозиторий, а репозиторий уже обращается к базе данных для создания заказа. Так вот, представим, что в моменте (например, когда исполнение запроса уже дошло до репозитория), пользователь отменил запрос, потому что случайно мискликнул и теперь нажал на крестик. Нужно ли теперь продолжать флоу и идти в базу, создавая заказ? Ну конечно же нет, в этом больше нет смысла

Но сам репозиторий не узнает о том, что запрос был отменён, кто-то ему должен об этом сказать. И вот этот «кто-то» — это и есть context. Сам контекст не несёт никакой бизнес-логики, его основная задача: дать информацию о том, что что-то изменилось, пока мы обрабатывали заказ, то есть дать некий сигнал, на который уже мы должны отреагировать

Как это работает

Схема (1), если накидать немного кода, выглядит примерно так:

//handler.go
func Handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
service.CreateOrder(ctx)
}

//service.go
func (s *Service) CreateOrder(ctx context.Context) error {
return s.repository.CreateOrder(ctx)
}

//repo.go
func (r *Repository) CreateOrder(ctx context.Context) error {
_, err := r.db.ExecContext(
ctx, "INSERT INTO orders (...) VALUES (...)",
)
return err
}


А если посмотреть в интерфейс Context, то выглядит он так

type Context interface {
Deadline() (deadline time.Time, ok bool) //возвращает дедлайн (при наличии), после которого контекст должен быть отменён. второй аргумент (ok) и говорит о том, есть ли deadline у данного контекста
Done() <-chan struct{} //возвращает канал, который закроется при отмене контекста (может быть nil)
Err() error //пока канал открыт, возвращает nil, после — возвращает ошибку context.Canceled или context.DeadlineExceeded
Value(key any) any //context может хранить значение по ключу, связанное с ним, а фактически с конкретным запросом или операцией
}


В комментариях есть описание каждого метода. Мы поняли, что метод Done() возвращает канал, который закроется, когда context будет отменён, а значит мы можем с помощью инструкции select дожидаться, когда закрытие канала разблокирует наш case и мы вернём ошибку контекста:

//service.go
func (s *Service) CreateOrder(ctx context.Context) error {
select {
case <-ctx.Done(): //этот кейс выберется только тогда, когда канал будет закрыт, в любом другом случае идем в default и в repository
return ctx.Err() //возвращаем причину, по которой контекст отменился
default:
}
return s.repository.CreateOrder(ctx)
}


В репозитории мы используем метод ExecContext, поэтому гошка на этом уровне разруливает сама, нам нужно лишь передать контекст, если он отменится — отменится и запрос (ну это если упростить, по факту там database/sql передает отмену драйверу, и запрос может быть прерван, если драйвер его поддерживает)

Важный момент, перед тем как пойти дальше: сам по себе context и его отмена не останавливает выполнение функции, он даёт лишь сигнал через канал, а нам нужно на него реагировать

Как нам самим объявлять context?

Пока у нас был только r.Context(), который мы вытащили из http.Request, но нам бы хотелось самим делать контекст, потому что таким образом отменять хочется не только запросы пользователей, но и другие, более внутренние операции

func main() {
ctx := context.Background()
service.Run(ctx)
}


context.Background() создаёт корневой контекст и у него пока нет ни дедлайна, ни функции отмены, ни каких-либо значений, пока мы просто создали оболочку, которую можем куда-либо прокинуть

Есть ещё context.TODO(), но он используется только в том случае, когда ты понимаешь, что он здесь должен быть, но пока не знаешь, какой именно context сюда нужно передать. Например, если ты рефакторишь старый код и ещё не успел протащить нормальный контекст сверху:

func CreateOrder() error { 
ctx := context.TODO()
return repository.CreateOrder(ctx)
}


Окей, у нас есть контекст. Как его отменять, то есть слать те самые сигналы?

context.WithCancel() — функция, которая возвращает нам новый контекст + функцию отмены этого контекста

func main() {
ctx := context.Background()
newCtx, cancel := context.WithCancel(ctx)
go worker(newCtx)
cancel()
}

func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("останавливаем работу:", ctx.Err())
return
default:
fmt.Println("работаем...")
time.Sleep(time.Second)
}
}
}


Что тут произошло?

1. Мы создали корневой контекст через context.Background()
2. С помощью context.WithCancel(ctx) создали дочерний контекст, который мы можем отменить
3. Запустили в отдельной горутине worker, в котором итерируемся в цикле for пока наш контекст, переданный в функцию, не отменился
4. Отменяем этот контекст в функции main
5. Получаем срабатывание кейса с <-ctx.Done() и выход из функции worker


В этом кейсе мы отменяем контекст вручную, когда захотим этого сами, но если же мы хотим отменить контекст через какое-то время автоматически, то есть ограничить выполнение функции по времени, то лучше сделать это через context.WithTimeout():

func (s *Service) CreateOrder(ctx context.Context) error { 
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return s.repository.CreateOrder(ctx)
}


Фактически, WithTimeout от WithCancel отличается тем, что принимает время (time.Duration) вторым аргументом и собственно через это время автоматически отменяется контекст

Внимательный читатель спросит: «а нахрена нам здесь defer cancel(), если ты говоришь, что контекст сам отменится», а я отвечу:

Суть в том, что в примере выше контекст отменяется автоматически только через 2 секунды, но поход в репозиторий (а дальше и в бд) может закончиться сильно раньше, например через 150 мс, и тогда нам этот контекст уже не нужен и его можно отменить раньше, чем запланировано по таймауту

Немного про наследование контекстов

Как вы поняли, во все функции по типу WithCancel и WithTimeout мы передаем уже существующий контекст и на его основе создаём новый, и поэтому тут работает следующее правило: если отменяется контекст, то и отменяются все контексты, которые были созданы от него

Например:

func main() {
parentCtx, parentCancel := context.WithCancel(context.Background())
childCtx, childCancel := context.WithTimeout(parentCtx, 10*time.Second)
defer parentCancel()
defer childCancel()
}


Несмотря на то, что в childCtx есть таймаут в 10 секунд, он отменится вместе с вызовом parentCancel(), причем контекст отменяется сверху вниз, если отменяется childCtx, то parentCtx будет жить

Помимо функции WithTimeout существует ещё и WithDeadline — функция, которая работает так же, как и WithTimeout, но задаёшь ты не время работы контекста, а время, до которого он работает

Вернёмся к Err() у интерфейса Context

Какая конкретно ошибка вернётся при отмене контекста зависит от того, как мы его отменили:

context.Canceled — контекст был отменён через cancel
context.DeadlineExceeded — контекст был отменён через timeout/deadline

Теперь про Value() у интерфейса Context

Мы можем в наш контекст положить значение по ключу, например для логов (про это отдельно), с помощью context.WithValue

//main.go
ctx := context.WithValue(ctx, "requestID", "abc-123")


А потом, в другом месте приложения, можем вытащить значение по ключу

//service.go
requestID := ctx.Value("requestID")


В качестве домашки почитайте про WithoutCancel, у меня лимиты :)

Кто я | Чат айтишников | Мои услуги
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12🔥5👍2
Всем привет, выкладываю первый урок из моего курса по Golang 🤓

https://www.youtube.com/watch?v=NzKkQz4w7DU

Ролики планирую выкладывать раз в неделю, ждите :)
Буду рад вашей поддержке, всё по базе, ну и посты в этом канале постараюсь писать параллельно, чтобы закрыть теоретическую базу 🙏🏻❤️
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7🔥2👍1