Clean Architecture

  • kita sudah pernah mempelajari clean arcitecture

  • kali ini kita akan menggunakan konsep domain

  • structure folder sebagai berikut

---- [domain]
     ---- [ddrivers]
          ---- [handler]
               grpc.go
          ---- [repositories]
               repo.go
          ---- [usecase]
               usecase.go
               otherfile.go
          ---- [validation]
               validationfile.go
          repository_interface.go
          usecase_interface.go
          validation_interface.go
  • Kita akan memecah file domain/ddrivers/handler.go menjadi mengikuti structur folder di atas.

Helper

  • untuk fungsi-fungsi pendukung akan dikelompokkan ke dalam folder lib/helper

  • Buat file lib/helper/error_ctx.go

package helper

import (
    "context"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func ContextError(ctx context.Context) error {
    switch ctx.Err() {
    case context.Canceled:
        return status.Error(codes.Canceled, context.Canceled.Error())
    case context.DeadlineExceeded:
        return status.Error(codes.DeadlineExceeded, context.DeadlineExceeded.Error())
    default:
        return nil
    }
}

Repository

  • repository adalah kode-kode yang mengakses transaksi database

  • Buat file domain/ddrivers/repository_interface.go

package ddrivers

import (
    "context"
    "skeleton/pb/drivers"
    "skeleton/pb/generic"
)

type DriverRepoInterface interface {
    Find(ctx context.Context, id string) error
    FindAll(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error)
    Create(ctx context.Context) error
    Update(ctx context.Context) error
    Delete(ctx context.Context, in *generic.Id) error
    GetPb() *drivers.Driver
    SetPb(*drivers.Driver)
}
  • Buat folder domain/ddrivers/repositories. Semua file yang mengimplementasikan repository interface akan dibuat dalam filder ini.

  • Buat file domain/ddrivers/repositories/repo.go

package repositories

import (
    "database/sql"
    "log"
    "skeleton/domain/ddrivers"
    "skeleton/pb/drivers"
)

type repo struct {
    db  *sql.DB
    log *log.Logger
    pb  drivers.Driver
}

func NewDriverRepo(db *sql.DB, log *log.Logger) ddrivers.DriverRepoInterface {
    return &repo{
        db:  db,
        log: log,
    }
}

func (u *repo) GetPb() *drivers.Driver {
    return &u.pb
}

func (u *repo) SetPb(in *drivers.Driver) {
    if len(in.Id) > 0 {
        u.pb.Id = in.Id
    }
    if len(in.Name) > 0 {
        u.pb.Name = in.Name
    }
    if len(in.Phone) > 0 {
        u.pb.Phone = in.Phone
    }
    if len(in.LicenceNumber) > 0 {
        u.pb.LicenceNumber = in.LicenceNumber
    }
    if len(in.CompanyId) > 0 {
        u.pb.CompanyId = in.CompanyId
    }
    if len(in.CompanyName) > 0 {
        u.pb.CompanyName = in.CompanyName
    }
    u.pb.IsDelete = in.IsDelete
    if len(in.Created) > 0 {
        u.pb.Created = in.Created
    }
    if len(in.CreatedBy) > 0 {
        u.pb.CreatedBy = in.CreatedBy
    }
    if len(in.Updated) > 0 {
        u.pb.Updated = in.Updated
    }
    if len(in.UpdatedBy) > 0 {
        u.pb.UpdatedBy = in.UpdatedBy
    }
}
  • Buat file domain/ddrivers/repositories/find_all.go

package repositories

import (
    "context"
    "fmt"
    "skeleton/lib/helper"
    "skeleton/pb/drivers"
    "skeleton/pb/generic"
    "strconv"
    "strings"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (u *repo) FindAll(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) {
    select {
    case <-ctx.Done():
        return nil, helper.ContextError(ctx)
    default:
    }

    out := &drivers.Drivers{}
    query := `SELECT id, name, phone, licence_number, company_id, company_name FROM drivers`
    where := []string{"is_deleted = false"}
    paramQueries := []interface{}{}

    if len(in.Ids) > 0 {
        orWhere := []string{}
        for _, id := range in.Ids {
            paramQueries = append(paramQueries, id)
            orWhere = append(orWhere, fmt.Sprintf("id = %d", len(paramQueries)))
        }
        if len(orWhere) > 0 {
            where = append(where, "("+strings.Join(orWhere, " OR ")+")")
        }
    }

    if len(in.CompanyIds) > 0 {
        orWhere := []string{}
        for _, id := range in.CompanyIds {
            paramQueries = append(paramQueries, id)
            orWhere = append(orWhere, fmt.Sprintf("company_id = %d", len(paramQueries)))
        }
        if len(orWhere) > 0 {
            where = append(where, "("+strings.Join(orWhere, " OR ")+")")
        }
    }

    if len(in.LicenceNumbers) > 0 {
        orWhere := []string{}
        for _, licenceNumber := range in.LicenceNumbers {
            paramQueries = append(paramQueries, licenceNumber)
            orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries)))
        }
        if len(orWhere) > 0 {
            where = append(where, "("+strings.Join(orWhere, " OR ")+")")
        }
    }

    if len(in.Names) > 0 {
        orWhere := []string{}
        for _, name := range in.Names {
            paramQueries = append(paramQueries, name)
            orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries)))
        }
        if len(orWhere) > 0 {
            where = append(where, "("+strings.Join(orWhere, " OR ")+")")
        }
    }

    if len(in.Phones) > 0 {
        orWhere := []string{}
        for _, phone := range in.Phones {
            paramQueries = append(paramQueries, phone)
            orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries)))
        }
        if len(orWhere) > 0 {
            where = append(where, "("+strings.Join(orWhere, " OR ")+")")
        }
    }

    if in.Pagination == nil {
        in.Pagination = &generic.Pagination{}
    }

    if len(in.Pagination.Keyword) > 0 {
        orWhere := []string{}

        paramQueries = append(paramQueries, in.Pagination.Keyword)
        orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries)))

        paramQueries = append(paramQueries, in.Pagination.Keyword)
        orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries)))

        paramQueries = append(paramQueries, in.Pagination.Keyword)
        orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries)))

        paramQueries = append(paramQueries, in.Pagination.Keyword)
        orWhere = append(orWhere, fmt.Sprintf("company_name = %d", len(paramQueries)))

        if len(orWhere) > 0 {
            where = append(where, "("+strings.Join(orWhere, " OR ")+")")
        }
    }

    if len(in.Pagination.Sort) > 0 {
        in.Pagination.Sort = strings.ToLower(in.Pagination.Sort)
        if in.Pagination.Sort != "asc" {
            in.Pagination.Sort = "desc"
        }
    } else {
        in.Pagination.Sort = "desc"
    }

    if len(in.Pagination.Order) > 0 {
        in.Pagination.Order = strings.ToLower(in.Pagination.Order)
        if !(in.Pagination.Order == "id" ||
            in.Pagination.Order == "name" ||
            in.Pagination.Order == "phone" ||
            in.Pagination.Order == "licence_number" ||
            in.Pagination.Order == "company_id" ||
            in.Pagination.Order == "company_name") {
            in.Pagination.Order = "id"
        }
    } else {
        in.Pagination.Order = "id"
    }

    if in.Pagination.Limit <= 0 {
        in.Pagination.Limit = 10
    }

    if in.Pagination.Offset <= 0 {
        in.Pagination.Offset = 0
    }

    if len(where) > 0 {
        query += " WHERE " + strings.Join(where, " AND ")
    }

    query += " ORDER BY " + in.Pagination.Order + " " + in.Pagination.Sort
    query += " LIMIT " + strconv.Itoa(int(in.Pagination.Limit))
    query += " OFFSET " + strconv.Itoa(int(in.Pagination.Offset))

    rows, err := u.db.QueryContext(ctx, query, paramQueries...)
    if err != nil {
        u.log.Println(err.Error())
        return nil, status.Error(codes.Internal, err.Error())
    }
    defer rows.Close()

    for rows.Next() {
        var obj drivers.Driver
        err = rows.Scan(&obj.Id, &obj.Name, &obj.Phone, &obj.LicenceNumber, &obj.CompanyId, &obj.CompanyName)
        if err != nil {
            u.log.Println(err.Error())
            return nil, status.Error(codes.Internal, err.Error())
        }

        out.Driver = append(out.Driver, &obj)
    }

    if rows.Err() != nil {
        u.log.Println(rows.Err().Error())
        return nil, status.Error(codes.Internal, rows.Err().Error())
    }

    return out, nil
}
  • Buat file domain/ddrivers/repositories/find_driver_by_id.go

package repositories

import (
    "context"
    "skeleton/lib/helper"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (u *repo) Find(ctx context.Context, id string) error {
    select {
    case <-ctx.Done():
        return helper.ContextError(ctx)
    default:
    }

    query := `
        SELECT id, name, phone, licence_number, company_id, company_name 
        FROM drivers WHERE id = $1 AND is_deleted = false
    `

    err := u.db.QueryRowContext(ctx, query, id).Scan(
        &u.pb.Id, &u.pb.Name, &u.pb.LicenceNumber, &u.pb.CompanyId, &u.pb.CompanyName)

    if err != nil {
        u.log.Println(err.Error())
        return status.Error(codes.Internal, err.Error())
    }

    return nil
}
  • Buat file domain/ddrivers/repositories/create_driver.go

package repositories

import (
    "context"
    "skeleton/lib/helper"
    "time"

    "github.com/google/uuid"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (u *repo) Create(ctx context.Context) error {
    select {
    case <-ctx.Done():
        return helper.ContextError(ctx)
    default:
    }

    query := `
        INSERT INTO drivers (
            id, name, phone, licence_number, company_id, company_name, created, created_by, updated, updated_by)
        VALUES ($1, $2, $3 ,$4, $5, $6, $7, $8, $9, $10)
    `
    u.pb.Id = uuid.New().String()
    now := time.Now().Format("2006-01-02 15:04:05.000000")
    _, err := u.db.ExecContext(ctx, query,
        u.pb.Id, u.pb.Name, u.pb.Phone, u.pb.LicenceNumber, u.pb.CompanyId, u.pb.CompanyName,
        now, u.pb.CreatedBy, now, u.pb.UpdatedBy)

    if err != nil {
        u.log.Println(err.Error())
        return status.Error(codes.Internal, err.Error())
    }

    return nil
}
  • Buat file domain/ddrivers/repositories/update_driver_by_id.go

package repositories

import (
    "context"
    "skeleton/lib/helper"
    "time"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (u *repo) Update(ctx context.Context) error {
    select {
    case <-ctx.Done():
        return helper.ContextError(ctx)
    default:
    }

    query := `
        UPDATE drivers 
        SET name = $1, 
                phone = $2, 
                licence_number = $3, 
                updated = $4, 
                updated_by = $5
        WHERE id = $6
    `
    now := time.Now().Format("2006-01-02 15:04:05.000000")
    _, err := u.db.ExecContext(ctx, query,
        u.pb.Name, u.pb.Phone, u.pb.LicenceNumber, now, u.pb.UpdatedBy, u.pb.Id)

    if err != nil {
        u.log.Println(err.Error())
        return status.Error(codes.Internal, err.Error())
    }

    return nil
}
  • Buat file domain/ddrivers/repositories/delete_driver_by_id.go

package repositories

import (
    "context"
    "skeleton/lib/helper"
    "skeleton/pb/generic"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (u *repo) Delete(ctx context.Context, in *generic.Id) error {
    select {
    case <-ctx.Done():
        return helper.ContextError(ctx)
    default:
    }

    query := `
        UPDATE drivers 
        SET is_deleted = true
        WHERE id = $1
    `
    _, err := u.db.ExecContext(ctx, query, in.Id)

    if err != nil {
        u.log.Println(err.Error())
        return status.Error(codes.Internal, err.Error())
    }

    return nil
}

Validation

  • Setiap request perlu divalidasi

  • Buat file domain/ddrivers/validation_interface.go

package ddrivers

import (
    "context"
    "skeleton/pb/drivers"
)

type DriverValidationInterface interface {
    Create(ctx context.Context, id *drivers.Driver) error
    Update(ctx context.Context, id *drivers.Driver) error
    Delete(ctx context.Context, id string) error
}
  • Untuk kemudahan pengelolaan kode, semua implementasi validasi akan dimasukkan dalam folder domain/drivers/validation

  • Buat file domain/ddrivers/validation/driover_validation.go

package validation

import (
    "log"
    "skeleton/domain/ddrivers"
)

type driverValidation struct {
    log        *log.Logger
    driverRepo ddrivers.DriverRepoInterface
}

func NewValidation(log *log.Logger, driverRepo ddrivers.DriverRepoInterface) ddrivers.DriverValidationInterface {
    return &driverValidation{
        log:        log,
        driverRepo: driverRepo,
    }
}
  • Buat file domain/ddrivers/validation/create_driver_validation.go

package validation

import (
    "context"
    "skeleton/lib/helper"
    "skeleton/pb/drivers"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (u *driverValidation) Create(ctx context.Context, in *drivers.Driver) error {
    select {
    case <-ctx.Done():
        return helper.ContextError(ctx)
    default:
    }

    if len(in.Name) == 0 {
        u.log.Println("please supply valid name")
        return status.Error(codes.InvalidArgument, "please supply valid name")
    }

    if len(in.Phone) == 0 {
        u.log.Println("please supply valid phone")
        return status.Error(codes.InvalidArgument, "please supply valid phone")
    }

    if len(in.CompanyId) == 0 {
        u.log.Println("please supply valid company id")
        return status.Error(codes.InvalidArgument, "please supply valid company id")
    }

    if len(in.CompanyName) == 0 {
        u.log.Println("please supply valid company name")
        return status.Error(codes.InvalidArgument, "please supply valid company name")
    }

    if len(in.LicenceNumber) == 0 {
        u.log.Println("please supply valid licence number")
        return status.Error(codes.InvalidArgument, "please supply valid licence number")
    }

    return nil
}
  • Buat file domain/ddrivers/validation/update_driver_validation.go

package validation

import (
    "context"
    "skeleton/lib/helper"
    "skeleton/pb/drivers"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (u *driverValidation) Create(ctx context.Context, in *drivers.Driver) error {
    select {
    case <-ctx.Done():
        return helper.ContextError(ctx)
    default:
    }

    if len(in.Name) == 0 {
        u.log.Println("please supply valid name")
        return status.Error(codes.InvalidArgument, "please supply valid name")
    }

    if len(in.Phone) == 0 {
        u.log.Println("please supply valid phone")
        return status.Error(codes.InvalidArgument, "please supply valid phone")
    }

    if len(in.CompanyId) == 0 {
        u.log.Println("please supply valid company id")
        return status.Error(codes.InvalidArgument, "please supply valid company id")
    }

    if len(in.CompanyName) == 0 {
        u.log.Println("please supply valid company name")
        return status.Error(codes.InvalidArgument, "please supply valid company name")
    }

    if len(in.LicenceNumber) == 0 {
        u.log.Println("please supply valid licence number")
        return status.Error(codes.InvalidArgument, "please supply valid licence number")
    }

    return nil
}
  • Buat file domain/ddrivers/validation/delete_driver/validation.fo

package validation

import (
    "context"
    "skeleton/lib/helper"
)

func (u *driverValidation) Delete(ctx context.Context, id string) error {
    select {
    case <-ctx.Done():
        return helper.ContextError(ctx)
    default:
    }

    err := u.driverRepo.Find(ctx, id)
    if err != nil {
        return err
    }

    return nil
}

Usecase

  • usacase digunakan untuk menghandle logic. usecase yang akan memanggil validation maupun repository sekiranya logic membutuhkan hal tersebut.

  • Buat file usecase_interface.go

package ddrivers

import (
    "context"
    "skeleton/pb/drivers"
    "skeleton/pb/generic"
)

type DriverUsecaseInterface interface {
    List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error)
    Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error)
    Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error)
    Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error)
}
  • Buat folder usecase untuk menyimpan seluruh fiule implementasi usecase interface.

  • Buat file domain/ddrivers/usecase/usecase.go

package usecase

import (
    "log"
    "skeleton/domain/ddrivers"
)

type service struct {
    log        *log.Logger
    driverRepo ddrivers.DriverRepoInterface
}

func NewService(log *log.Logger, driverRepo ddrivers.DriverRepoInterface) ddrivers.DriverUsecaseInterface {
    return &service{
        log:        log,
        driverRepo: driverRepo,
    }
}
  • Buat file domain/ddrivers/usecase/create_driver_usecase.go

package usecase

import (
    "context"
    "skeleton/domain/ddrivers/validation"
    "skeleton/lib/helper"
    "skeleton/pb/drivers"
)

func (u *service) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) {
    select {
    case <-ctx.Done():
        return nil, helper.ContextError(ctx)
    default:
    }

    dValidation := validation.NewValidation(u.log, u.driverRepo)
    err := dValidation.Create(ctx, in)
    if err != nil {
        return nil, err
    }

    u.driverRepo.SetPb(in)

    err = u.driverRepo.Create(ctx)
    if err != nil {
        return nil, err
    }

    return u.driverRepo.GetPb(), nil
}
  • Buat file domain/ddrivers/usecase/list_driver_usecase.go

package usecase

import (
    "context"
    "skeleton/lib/helper"
    "skeleton/pb/drivers"
)

func (u *service) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) {
    select {
    case <-ctx.Done():
        return nil, helper.ContextError(ctx)
    default:
    }

    return u.driverRepo.FindAll(ctx, in)
}
  • Buat file domain/ddrivers/usecase/update_driver_usecase.go

package usecase

import (
    "context"
    "skeleton/domain/ddrivers/validation"
    "skeleton/lib/helper"
    "skeleton/pb/drivers"
)

func (u *service) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) {
    select {
    case <-ctx.Done():
        return nil, helper.ContextError(ctx)
    default:
    }

    dValidation := validation.NewValidation(u.log, u.driverRepo)
    err := dValidation.Update(ctx, in)
    if err != nil {
        return nil, err
    }

    u.driverRepo.SetPb(in)

    err = u.driverRepo.Update(ctx)
    if err != nil {
        return nil, err
    }

    return u.driverRepo.GetPb(), nil
}
  • Buat file domain.ddrivers/usecase/delete_driver_usecase.go

package usecase

import (
    "context"
    "skeleton/domain/ddrivers/validation"
    "skeleton/lib/helper"
    "skeleton/pb/generic"
)

func (u *service) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) {
    select {
    case <-ctx.Done():
        return nil, helper.ContextError(ctx)
    default:
    }

    dValidation := validation.NewValidation(u.log, u.driverRepo)
    err := dValidation.Delete(ctx, in.Id)
    if err != nil {
        return nil, err
    }

    err = u.driverRepo.Delete(ctx, in)
    if err != nil {
        return nil, err
    }

    return &generic.BoolMessage{IsTrue: true}, nil
}

Perbarui Handler

  • handler merupakan endpoint service.

  • handler mengimpolementasikan seluruh seluruh funhgsi dari interface DomainServiceServer

  • Buat file domain/ddrivers/handler/grpc.go

package handler

import (
    "context"
    "skeleton/domain/ddrivers"
    "skeleton/pb/drivers"
    "skeleton/pb/generic"
)

type DriverHandler struct {
    usecase ddrivers.DriverUsecaseInterface
}

func NewDriverHandler(usecase ddrivers.DriverUsecaseInterface) *DriverHandler {
    handler := new(DriverHandler)
    handler.usecase = usecase
    return handler
}

func (u *DriverHandler) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) {
    return u.usecase.List(ctx, in)
}

func (u *DriverHandler) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) {
    return u.usecase.Create(ctx, in)
}

func (u *DriverHandler) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) {
    return u.usecase.Update(ctx, in)
}

func (u *DriverHandler) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) {
    return u.usecase.Delete(ctx, in)
}

Perbarui Routing

  • Ubah file route/route.go

package route

import (
    "database/sql"
    "log"

    driverHandler "skeleton/domain/ddrivers/handler"
    driverRepo "skeleton/domain/ddrivers/repositories"
    driverUsecase "skeleton/domain/ddrivers/usecase"
    "skeleton/pb/drivers"

    "google.golang.org/grpc"
)

func GrpcRoute(grpcServer *grpc.Server, log *log.Logger, db *sql.DB) {
    driverServer := driverHandler.NewDriverHandler(
        driverUsecase.NewService(log, driverRepo.NewDriverRepo(db, log)),
    )

    drivers.RegisterDriversServiceServer(grpcServer, driverServer)
}

Last updated