Lompat ke konten
dokumentasi.id
Commit 43aad26
v1
Star1,1 rb

7

Lapisan Usecase, Model, dan Converter

Tiga use case di package usecase memegang aturan main aplikasi: UserUseCase, ContactUseCase, dan AddressUseCase. Ketiganya dirakit di satu tempat bersama DB, Log, dan Validate yang sama, lalu diserahkan ke controller HTTP dan middleware. internal/config/app.go:45-48 Halaman ini membahas pola yang dipakai ketiganya: satu transaksi per method, validasi request, pemanggilan repository, pengiriman event, lalu pemetaan lewat converter.

Satu transaksi per method

Setiap method publik membuka transaksi sendiri lewat c.DB.WithContext(ctx).Begin(), menunda tx.Rollback() dengan defer, dan menutupnya lewat tx.Commit(). Contoh paling ringkas ada di ContactUseCase.Create: transaksi dibuka, request divalidasi, entity disusun dari field request, lalu ContactRepository.Create dipanggil. internal/usecase/contact_usecase.go:37-63

sequenceDiagram
    autonumber
    participant UC as ContactUseCase.Create
    participant DB as gorm.DB
    participant V as validator.Validate
    participant R as ContactRepository
    participant CV as converter
    participant P as ContactProducer
    UC->>DB: Begin
    UC->>V: Struct request
    V-->>UC: error -> fiber.ErrBadRequest
    UC->>R: Create tx dan contact
    R-->>UC: entity.Contact terisi ID
    UC->>DB: Commit
    UC->>CV: ContactToEvent
    UC->>P: Send event
    UC->>CV: ContactToResponse
    CV-->>UC: model.ContactResponse
    Note over UC,P: producer nil melewati Send
%% fig: Siklus satu method use case
%% desc: Method membuka transaksi, memvalidasi request, memanggil repository, commit, lalu memetakan entity ke event dan response.
Fig. 1: Siklus satu method use caseMethod membuka transaksi, memvalidasi request, memanggil repository, commit, lalu memetakan entity ke event dan response.

Urutan sesudah Commit patut dicatat. ContactUseCase.Create baru mengirim event setelah transaksi selesai, dan hanya jika ContactProducer tidak nil. internal/usecase/contact_usecase.go:60-74 UserUseCase.Create memakai urutan yang sama: commit dulu, lalu converter.UserToEvent dan UserProducer.Send. internal/usecase/user_usecase.go:100-116

⚠️ Event dikirim setelah Commit. Kalau Send gagal, method mengembalikan fiber.ErrInternalServerError padahal baris database sudah tersimpan, sehingga client melihat kegagalan untuk pekerjaan yang sebenarnya berhasil. internal/usecase/contact_usecase.go:65-74

Ketergantungan yang dipegang use case

Ketiga struct use case memegang DB, Log, dan Validate, ditambah satu repository dan satu producer. AddressUseCase memegang dua repository karena setiap operasi address lebih dulu memastikan contact milik user tersebut ada. internal/usecase/address_usecase.go:49-53

UseCaseField repositoryField producerRujukan
UserUseCaseUserRepositoryUserProducerinternal/usecase/user_usecase.go:19-25
ContactUseCaseContactRepositoryContactProducerinternal/usecase/contact_usecase.go:18-24
AddressUseCaseContactRepository, AddressRepositoryAddressProducerinternal/usecase/address_usecase.go:18-25

Constructor menerima producer sebagai pointer yang bisa kosong saat Kafka dimatikan. NewAddressUseCase menerima contactRepository lebih dulu, baru addressRepository, dan urutan itu harus diikuti pemanggilnya. internal/usecase/address_usecase.go:27-38

Method publik tiap use case

UseCaseMethodRequestHasil
UserUseCaseVerifymodel.VerifyUserRequestmodel.Auth
UserUseCaseCreatemodel.RegisterUserRequestmodel.UserResponse
UserUseCaseLoginmodel.LoginUserRequestmodel.UserResponse
UserUseCaseCurrentmodel.GetUserRequestmodel.UserResponse
UserUseCaseLogoutmodel.LogoutUserRequestbool
UserUseCaseUpdatemodel.UpdateUserRequestmodel.UserResponse
ContactUseCaseCreate, Update, Getmodel.CreateContactRequest, model.UpdateContactRequest, model.GetContactRequestmodel.ContactResponse
ContactUseCaseDeletemodel.DeleteContactRequesterror
ContactUseCaseSearchmodel.SearchContactRequest[]model.ContactResponse, int64
AddressUseCaseCreate, Update, Getmodel.CreateAddressRequest, model.UpdateAddressRequest, model.GetAddressRequestmodel.AddressResponse
AddressUseCaseDeletemodel.DeleteAddressRequesterror
AddressUseCaseListmodel.ListAddressRequest[]model.AddressResponse

Verify adalah satu-satunya method yang mengembalikan model.Auth, yaitu struct berisi ID user hasil pencarian token. internal/model/auth.go:3-6 Pencarian itu memakai UserRepository.FindByToken di dalam transaksi yang sama. internal/usecase/user_usecase.go:38-59

Login membandingkan password request dengan hash tersimpan lewat bcrypt.CompareHashAndPassword, mengisi user.Token dengan uuid.New().String(), menyimpannya lewat UserRepository.Update, commit, lalu mengembalikan converter.UserToTokenResponse yang hanya membawa token. internal/usecase/user_usecase.go:139-161

Model: request, response, dan event

Package model memisahkan tiga bentuk data: request masuk, response keluar, dan event Kafka. Response dibungkus WebResponse atau PageResponse, dengan metadata paging di PageMetadata. internal/model/model.go:3-19

UserResponse membawa ID, Name, Token, CreatedAt, dan UpdatedAt, semuanya bertanda omitempty sehingga field kosong hilang dari JSON. internal/model/user_model.go:3-9 ContactResponse dan AddressResponse sebaliknya tidak memakai omitempty pada field utama, dan ContactResponse bisa menyertakan Addresses. internal/model/contact_model.go:3-12 internal/model/address_model.go:3-12

Event dipisahkan dari response. UserEvent hanya membawa ID, Name, CreatedAt, dan UpdatedAt, tanpa token atau password. internal/model/user_event.go:3-12 Ketiga event memenuhi interface model.Event yang hanya menuntut satu method, GetId(), dan method itulah yang dipakai producer sebagai key pesan. internal/model/event.go:3-5 internal/model/contact_event.go:3-16

Producer generik memakai constraint itu: Send melakukan json.Marshal pada event, menyusun sarama.ProducerMessage dengan Key: sarama.StringEncoder(event.GetId()), lalu mengirimnya secara sinkron. internal/gateway/messaging/producer.go:21-41 Topik ditentukan di constructor masing-masing producer; UserProducer mengunci topik users. internal/gateway/messaging/user_producer.go:10-22

Converter memetakan entity ke dua arah keluar

Converter adalah fungsi bebas di package converter yang menerima pointer entity dan mengembalikan pointer model. Tidak ada state dan tidak ada akses database di dalamnya.

graph LR
    E["entity.User"] --> UR["model.UserResponse"]
    E --> UE["model.UserEvent"]
    EC["entity.Contact"] --> CR["model.ContactResponse"]
    EC --> CE["model.ContactEvent"]
    EA["entity.Address"] --> AR["model.AddressResponse"]
    EA --> AE["model.AddressEvent"]
    style E fill:#336791,color:#fff
    style EC fill:#336791,color:#fff
    style EA fill:#336791,color:#fff
    style UR fill:#2d7d46,color:#fff
    style CR fill:#2d7d46,color:#fff
    style AR fill:#2d7d46,color:#fff
    style UE fill:#8b5cf6,color:#fff
    style CE fill:#8b5cf6,color:#fff
    style AE fill:#8b5cf6,color:#fff
%% fig: Pemetaan entity ke response dan event
%% desc: Setiap entity punya satu fungsi converter ke response HTTP dan satu ke event Kafka, dan keduanya membaca entity yang sama.
Fig. 2: Pemetaan entity ke response dan eventSetiap entity punya satu fungsi converter ke response HTTP dan satu ke event Kafka, dan keduanya membaca entity yang sama.

UserToResponse menyalin ID, Name, CreatedAt, dan UpdatedAt; UserToTokenResponse hanya menyalin Token; UserToEvent menyalin empat field yang sama seperti response tanpa token. internal/model/converter/user_converter.go:8-30 ContactToResponse dan ContactToEvent berbagi field yang sama, hanya ContactEvent menambahkan UserID. internal/model/converter/contact_converter.go:8-31 AddressToEvent mempertahankan ContactId sedangkan AddressToResponse tidak membawanya. internal/model/converter/address_converter.go:8-33

Untuk hasil berbentuk daftar, use case mengalokasikan slice response sepanjang hasil repository lalu mengisi tiap elemen dengan converter. ContactUseCase.Search melakukannya atas []entity.Contact. internal/usecase/contact_usecase.go:194-199 AddressUseCase.List melakukannya atas []entity.Address. internal/usecase/address_usecase.go:214-217

Validasi request

Validasi dijalankan dengan c.Validate.Struct(request) dan kegagalannya selalu dipetakan ke fiber.ErrBadRequest. Aturan ditegakkan lewat tag validate pada struct request, bukan di dalam method.

ModelFieldTag
RegisterUserRequestID, Password, Namerequired,max=100
UpdateUserRequestPassword, Namemax=100
SearchContactRequestPage dan Sizemin=1 dan min=1,max=100
CreateAddressRequestPostalCodemax=10

Tag required pada UpdateUserRequest sengaja tidak ada supaya field kosong berarti jangan diubah. internal/model/user_model.go:15-25 SearchContactRequest membatasi Size sampai 100. internal/model/contact_model.go:31-38 Alamat membatasi PostalCode sepuluh karakter dan Country seratus karakter. internal/model/address_model.go:19-27

Sebagian field request bertag json:"-". UserId pada request contact diisi oleh delivery layer dari sesi terautentikasi, bukan dari body. internal/model/contact_model.go:14-20

Urutan validasi tidak seragam

ContactUseCase.Create memvalidasi request sebelum menyentuh database. internal/usecase/contact_usecase.go:41-44 ContactUseCase.Update justru melakukannya setelah FindByIdAndUserId, sehingga contact yang tidak ada mengembalikan fiber.ErrNotFound lebih dulu daripada fiber.ErrBadRequest. internal/usecase/contact_usecase.go:83-92

AddressUseCase.Create memvalidasi request sebelum menyentuh repository. internal/usecase/address_usecase.go:44-47 AddressUseCase.Get sama sekali tidak memanggil validator: method itu langsung mencari contact dari request.ContactId dan request.UserId, lalu mencari address lewat AddressRepository.FindByIdAndContactId. internal/usecase/address_usecase.go:140-154

List melakukan pencarian contact yang sama lalu memanggil AddressRepository.FindAllByContactId, juga tanpa Validate.Struct. internal/usecase/address_usecase.go:193-201 Kedua method baca itu bergantung penuh pada filter kepemilikan data, bukan pada aturan tag validate.