Aspect Oriented Programming #

Dalam pengembangan aplikasi skala menengah hingga besar, ada satu pola yang hampir pasti kamu temui: logika yang berulang di banyak tempat bukan karena duplikasi yang ceroboh, melainkan karena ia memang dibutuhkan di mana-mana. Logging di setiap endpoint, pengecekan autentikasi di setiap handler, pengukuran latency di setiap method kritis, manajemen transaction di setiap operasi write. Logika ini bukan core business — tapi wajib ada. Jika ditaruh langsung di setiap method, kode bisnis tenggelam di bawah concern teknis. Jika dilupakan di satu tempat, konsistensi rusak. Aspect-Oriented Programming hadir untuk menyelesaikan masalah ini: memisahkan cross-cutting concern ke modul terpisah yang diterapkan secara deklaratif, tanpa mencemari kode bisnis. Artikel ini membahas AOP dari konsep dasarnya, lima istilah inti yang harus dipahami, implementasi di Java Spring dan pola middleware Go, empat use case utama, limitasi yang sering diremehkan, dan panduan kapan AOP adalah solusi yang tepat.

Apa Itu Cross-Cutting Concern? #

Sebelum membahas AOP, penting memahami masalah yang ia selesaikan. Cross-cutting concern adalah logika yang dibutuhkan di banyak bagian sistem tapi bukan bagian dari domain bisnis manapun secara spesifik.

Contoh yang paling mudah: logging. Setiap method di setiap layer memerlukan logging — tapi logging bukan bagian dari logika “buat order” atau “proses payment”. Ia adalah kebutuhan teknis yang memotong melintang semua domain.

// ANTI-PATTERN: cross-cutting concern tersebar di setiap method bisnis
func (s *OrderService) CreateOrder(req CreateOrderRequest) (*Order, error) {
    // Logging — bukan bagian dari logika order
    log.Info("start CreateOrder", "user_id", req.UserID)
    start := time.Now()
    
    // Auth check — bukan bagian dari logika order
    if !s.auth.HasPermission(req.UserID, "order:create") {
        log.Warn("unauthorized CreateOrder", "user_id", req.UserID)
        return nil, ErrUnauthorized
    }
    
    // Retry — bukan bagian dari logika order
    var order *Order
    var err error
    for attempt := 1; attempt <= 3; attempt++ {
        order, err = s.repo.Save(buildOrder(req))
        if err == nil { break }
        time.Sleep(time.Duration(attempt) * time.Second)
    }
    
    // Metrics — bukan bagian dari logika order
    metrics.Histogram("order.create.duration", time.Since(start))
    
    // Logging lagi — masih bukan bagian dari logika order
    log.Info("end CreateOrder", "order_id", order.ID, "duration", time.Since(start))
    return order, err
}
// Kode bisnis sebenarnya hanya 2 baris — tenggelam di bawah concern teknis
flowchart LR
    subgraph Without["❌ Tanpa AOP — Cross-cutting Tersebar"]
        O["OrderService\ncreateOrder\n+ logging\n+ auth\n+ metrics\n+ retry"]
        P["PaymentService\nprocessPayment\n+ logging\n+ auth\n+ metrics\n+ retry"]
        U["UserService\ncreateUser\n+ logging\n+ auth\n+ metrics\n+ retry"]
    end
    subgraph With["✅ Dengan AOP — Cross-cutting Terpusat"]
        LA[LoggingAspect] --> OS["OrderService\ncreateOrder"]
        AA[AuthAspect] --> OS
        MA[MetricsAspect] --> OS
        LA --> PS["PaymentService\nprocessPayment"]
        AA --> PS
        MA --> PS
        LA --> US["UserService\ncreateUser"]
        AA --> US
        MA --> US
    end

Concern yang paling umum memerlukan AOP:

Cross-Cutting ConcernContoh Konkret
LoggingLog method name, parameter, hasil, durasi
AuthenticationCek token JWT sebelum method dieksekusi
AuthorizationCek permission berdasarkan role
TransactionBuka transaction sebelum, commit/rollback sesudah
CachingReturn cache jika ada, simpan ke cache setelah method
RetryUlangi jika gagal dengan backoff
MetricsUkur latency, hitung error rate
Audit TrailCatat siapa melakukan apa kapan

Apa Itu Aspect-Oriented Programming? #

Aspect-Oriented Programming (AOP) adalah paradigma pemrograman yang memisahkan cross-cutting concern ke dalam modul terpisah yang disebut Aspect, lalu menerapkannya secara deklaratif ke titik-titik eksekusi tertentu tanpa mengubah kode bisnis.

Prinsip intinya: kode bisnis hanya berisi logika bisnis. Semua concern tambahan ditangani oleh aspect yang “membungkus” eksekusi dari luar.

// BENAR dengan AOP — kode bisnis murni (di Go: middleware/decorator)
type OrderService struct {
    repository OrderRepository
}

func (s *OrderService) CreateOrder(req CreateOrderRequest) (*Order, error) {
    // Hanya logika bisnis — tidak ada logging, auth, metrics
    order := buildOrder(req)
    return s.repository.Save(order)
}

// Logging, auth, metrics ditangani oleh middleware/decorator terpisah
// dan diterapkan secara otomatis tanpa menyentuh kode di atas

Lima Konsep Inti AOP #

Memahami lima istilah ini adalah kunci untuk bisa membaca dan menulis AOP dengan benar.

flowchart TD
    AS["Aspect\nModul berisi logic cross-cutting"] --> ADV["Advice\nKode yang dijalankan"]
    AS --> PC["Pointcut\nEkspresi pemilih join point"]
    PC --> JP["Join Point\nTitik eksekusi\nyang dipilih"]
    ADV --> JP
    WV["Weaving\nProses penggabungan\nAspect + Business Code"] --> JP

1. Aspect #

Modul yang berisi logika cross-cutting. Ini adalah unit yang mendefinisikan apa yang harus dilakukan dan kapan dilakukan.

// Aspect setara di Go: middleware — semua logika logging dikumpulkan di sini
func LoggingMiddleware() fiber.Handler {
    // Semua logika logging dikumpulkan di sini
    // Handler tidak perlu tahu middleware ini ada
    return func(c *fiber.Ctx) error { return c.Next() }
}

2. Join Point #

Titik eksekusi dalam program yang bisa di-intercept — biasanya sebuah method call. Dalam Spring AOP, join point selalu berupa method execution.

Join point yang mungkin:
  - Eksekusi method: orderService.createOrder(req)
  - Akses field: user.email (hanya di AspectJ, bukan Spring AOP)
  - Constructor call: new Order() (hanya di AspectJ)

3. Pointcut #

Ekspresi yang memilih join point mana yang akan di-intercept. Pointcut menentukan di mana advice akan diterapkan.

// Pointcut di Go: predicate pemilih method — dipakai oleh middleware/decorator
// Pointcut: semua method di package service
var serviceLayer = func(method string) bool {
    return strings.HasPrefix(method, "service.")
}

// Pointcut: method dengan annotation/tag tertentu
var adminOnly = func(hasTag bool) bool { return hasTag }

// Pointcut: method di struct/type tertentu
var paymentService = func(typeName string) bool {
    return typeName == "PaymentService"
}

4. Advice #

Kode yang dijalankan di sekitar join point. Ada lima jenis advice dengan timing eksekusi yang berbeda:

Tipe AdviceTimingCocok Untuk
@BeforeSebelum method dieksekusiAuth check, parameter logging, validation
@AfterSetelah method selesai (apapun hasilnya)Resource cleanup, audit trail
@AfterReturningSetelah method return suksesCache result, success logging
@AfterThrowingSetelah method throw exceptionError logging, alert, rollback
@AroundSebelum DAN sesudah, bisa intercept resultTiming, retry, caching, transaction
sequenceDiagram
    participant Caller
    participant AOP as AOP Proxy
    participant Method as Business Method

    Caller->>AOP: panggil method
    AOP->>AOP: @Before advice
    AOP->>Method: proceed()
    Method-->>AOP: return result
    AOP->>AOP: @AfterReturning advice
    AOP->>AOP: @After advice
    AOP-->>Caller: return result

    Note over AOP: Jika exception:
    Note over AOP: @AfterThrowing dipanggil
    Note over AOP: @After tetap dipanggil

5. Weaving #

Proses menggabungkan aspect dengan kode bisnis. Ada tiga strategi weaving:

StrategiKapanKelebihanKekurangan
Compile-timeSaat kompilasi (AspectJ)Performa maksimal, semua join pointButuh compiler khusus
Load-timeSaat class di-load ke JVMFleksibelKonfigurasi lebih kompleks
Runtime (Proxy)Saat aplikasi berjalan (Spring AOP)Paling mudah setupTerbatas pada method yang bisa di-proxy

Spring AOP menggunakan runtime proxy — ia membuat proxy object yang membungkus bean asli. Ini kenapa Spring AOP tidak bisa intercept method private atau final.


Implementasi AOP di Java Spring #

Logging Aspect #

// ANTI-PATTERN: logging manual di setiap method service
func (s *OrderService) createOrder(req CreateOrderRequest) (*Order, error) {
    log.Infof("start createOrder userId=%v", req.UserID) // duplikat di setiap method
    order, err := s.doCreate(req)
    log.Infof("end createOrder orderId=%v", order.ID)    // duplikat di setiap method
    return order, err
}

// BENAR: logging terpusat di middleware — semua handler otomatis ter-log
func LoggingMiddleware() fiber.Handler {
    return func(c *fiber.Ctx) error {
        method := c.Method()
        log.Infof("[START] %s", method)
        start := time.Now()

        err := c.Next() // ← jalankan handler asli
        if err != nil {
            log.Errorf("[ERROR] %s failed after %v: %v", method, time.Since(start), err)
            return err
        }
        log.Infof("[END] %s took %v", method, time.Since(start))
        return nil
    }
}

Authorization Aspect dengan Custom Annotation #

// Definisi "annotation" di Go: struct marker sederhana
type RequireRole struct {
    Role string // role yang dibutuhkan
}

// Aspect yang menerapkan check — middleware/auth wrapper
func RequireRoleMiddleware(requiredRole string, getCurrentRole func() string, next fiber.Handler) fiber.Handler {
    return func(c *fiber.Ctx) error {
        currentRole := getCurrentRole()

        if currentRole != requiredRole {
            return c.Status(403).JSON(fiber.Map{
                "error": "Role '" + requiredRole + "' diperlukan, tapi user punya '" + currentRole + "'",
            })
        }
        return next(c)
    }
}

// Penggunaan — deklaratif, bersih
func (s *AdminService) DeleteUser(userID string) {
    // Auth check ditangani middleware — method ini hanya berisi logika bisnis
    s.userRepository.Delete(userID)
}

func (s *AdminService) ResetSystem() {
    s.systemRepository.ResetAll()
}

Performance Monitoring Aspect #

// BENAR: ukur latency setiap method handler secara otomatis
type MetricsMiddleware struct {
    registry *MetricsRegistry
}

func NewMetricsMiddleware(registry *MetricsRegistry) *MetricsMiddleware {
    return &MetricsMiddleware{registry: registry}
}

func (m *MetricsMiddleware) Wrap(handler fiber.Handler) fiber.Handler {
    return func(c *fiber.Ctx) error {
        methodName := c.Route().Name
        className := "handler"

        sample := m.registry.StartTimer("method.latency")
        err := handler(c)

        status := "success"
        if err != nil {
            status = "error"
        }
        sample.Stop(map[string]string{
            "class":  className,
            "method": methodName,
            "status": status,
        })
        return err
    }
}

Pola AOP di Go: Middleware dan Decorator #

Go tidak punya framework AOP seperti Spring, tapi pola yang sama bisa dicapai dengan middleware pattern dan decorator pattern — yang justru lebih eksplisit dan lebih mudah dipahami.

HTTP Middleware (Fiber/Echo/Gin) #

// BENAR: cross-cutting concern sebagai middleware — tidak masuk ke handler
func LoggingMiddleware() fiber.Handler {
    return func(c *fiber.Ctx) error {
        start := time.Now()
        method := c.Method()
        path := c.Path()

        // Before: catat request masuk
        log.Infof("[START] %s %s", method, path)

        err := c.Next() // ← jalankan handler asli

        // After: catat hasil dan durasi
        log.Infof("[END] %s %s status=%d duration=%v",
            method, path, c.Response().StatusCode(), time.Since(start))

        return err
    }
}

func AuthMiddleware(jwtSecret string) fiber.Handler {
    return func(c *fiber.Ctx) error {
        token := c.Get("Authorization")
        claims, err := validateJWT(token, jwtSecret)
        if err != nil {
            return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
        }
        c.Locals("user_id", claims.UserID)
        return c.Next()
    }
}

// Daftarkan sekali, berlaku ke semua route di bawahnya
app := fiber.New()
app.Use(LoggingMiddleware())  // apply ke semua route
app.Use(AuthMiddleware(secret)) // apply ke semua route

app.Get("/orders", getOrders)        // otomatis ter-log dan ter-auth
app.Post("/orders", createOrder)     // otomatis ter-log dan ter-auth
app.Delete("/orders/:id", deleteOrder)

Service Decorator Pattern #

// Interface bisnis
type OrderRepository interface {
    Save(ctx context.Context, order *Order) error
    FindByID(ctx context.Context, id string) (*Order, error)
}

// ANTI-PATTERN: logging di dalam implementasi — mencemari repository
type MySQLOrderRepository struct{ db *sql.DB }

func (r *MySQLOrderRepository) Save(ctx context.Context, order *Order) error {
    log.Infof("saving order %s", order.ID) // ← tidak seharusnya di sini
    _, err := r.db.ExecContext(ctx, "INSERT INTO orders ...", order)
    log.Infof("saved order %s err=%v", order.ID, err)
    return err
}

// BENAR: logging sebagai decorator — wraps repository tanpa mengubahnya
type LoggingOrderRepository struct {
    inner  OrderRepository // ← bungkus implementasi asli
    logger Logger
}

func NewLoggingOrderRepository(inner OrderRepository, logger Logger) OrderRepository {
    return &LoggingOrderRepository{inner: inner, logger: logger}
}

func (r *LoggingOrderRepository) Save(ctx context.Context, order *Order) error {
    r.logger.Infof("[REPO] Save order %s", order.ID)
    start := time.Now()
    
    err := r.inner.Save(ctx, order) // delegasi ke implementasi asli
    
    r.logger.Infof("[REPO] Save order %s done in %v err=%v",
        order.ID, time.Since(start), err)
    return err
}

func (r *LoggingOrderRepository) FindByID(ctx context.Context, id string) (*Order, error) {
    r.logger.Infof("[REPO] FindByID %s", id)
    order, err := r.inner.FindByID(ctx, id)
    r.logger.Infof("[REPO] FindByID %s found=%v", id, order != nil)
    return order, err
}

// Wiring di main() — transparansi penuh
func main() {
    mysqlRepo    := repository.NewMySQLOrderRepository(db)
    loggingRepo  := NewLoggingOrderRepository(mysqlRepo, logger)  // decorator
    cachingRepo  := NewCachingOrderRepository(loggingRepo, redis) // decorator di atas decorator
    
    service := service.NewOrderService(cachingRepo) // service tidak tahu ada logging/caching
}

Empat Use Case Utama AOP #

flowchart TD
    AOP["Aspect / Middleware"] --> L["Logging\nLog semua method\nscara konsisten"]
    AOP --> A["Auth & AuthZ\nCek token dan role\nsebelum method dieksekusi"]
    AOP --> M["Metrics\nUkur latency dan\nhitung error rate"]
    AOP --> T["Transaction\nBuka dan tutup\ntransaksi otomatis"]
    L --> B["Business Logic\nBersih dan fokus\npada domain"]
    A --> B
    M --> B
    T --> B
Use CaseApproachAlasan AOP Tepat
Logging@Around atau middlewareBerlaku di semua endpoint/method secara konsisten
Auth/AuthZ@Before atau middlewareHarus dilakukan sebelum logika bisnis, tidak boleh lupa
Transaction@AroundBuka sebelum, commit sesudah, rollback jika exception
Metrics/Tracing@Around atau middlewarePerlu akses ke start time, method name, dan hasil

Limitasi AOP yang Harus Dipahami #

Limitasi Spring AOP #

@Service
public class OrderService {
    
    @Transactional // ← Aspect yang mengelola transaction
    public void createOrder(CreateOrderRequest req) {
        Order order = repository.save(req);
        
        // ANTI-PATTERN: memanggil method @Transactional dari dalam class yang sama
        // Spring AOP menggunakan proxy — this.processOrder() tidak melalui proxy!
        this.processOrder(order); // ← @Transactional di sini TIDAK akan bekerja
    }
    
    @Transactional(propagation = REQUIRES_NEW)
    public void processOrder(Order order) {
        // Kira-kira dalam transaction baru — tapi tidak, karena dipanggil via this
    }
}

// BENAR: pisahkan ke bean terpisah agar melalui proxy
@Service
public class OrderService {
    private final OrderProcessor processor; // ← bean terpisah
    
    @Transactional
    public void createOrder(CreateOrderRequest req) {
        Order order = repository.save(req);
        processor.processOrder(order); // ← melalui proxy, @Transactional bekerja
    }
}
flowchart LR
    subgraph Direct["this.method() — Tidak Melalui Proxy"]
        C1[Caller] --> P1[Spring Proxy]
        P1 -->|"apply aspect"| B1["Bean\ncreateOrder"]
        B1 -->|"this.processOrder()\nLEWATI proxy"| B1
        B1 --> B2["processOrder\naspect TIDAK berlaku"]
    end
    subgraph External["bean.method() — Melalui Proxy"]
        C2[Caller] --> P2["Proxy A\ncreateOrder"]
        P2 -->|"apply aspect"| B3[createOrder]
        B3 --> P3["Proxy B\nprocessOrder"]
        P3 -->|"apply aspect"| B4["processOrder\naspect BERLAKU"]
    end

Hal-hal yang tidak bisa di-intercept oleh Spring AOP (karena berbasis proxy):

Yang Tidak BisaSolusi
Method privatePindahkan ke method public atau gunakan AspectJ
Method finalHapus final atau gunakan AspectJ
this.method() internal callPisahkan ke bean terpisah atau inject self
ConstructorGunakan AspectJ compile-time weaving
Field accessGunakan AspectJ
Spring AOP bekerja lewat proxy object. Ini berarti jika sebuah method memanggil method lain dalam class yang sama (this.method()), proxy tidak ikut campur dan semua advice (@Transactional, @Cacheable, custom aspect) di method yang dipanggil tidak akan bekerja. Ini adalah penyebab bug yang sangat sering terjadi dan sangat sulit dilacak.

Best Practice Menggunakan AOP #

Jangan Taruh Business Logic di Aspect #

// ANTI-PATTERN: business logic di dalam middleware — salah tempat
func handleOrder(next fiber.Handler) fiber.Handler {
    return func(c *fiber.Ctx) error {
        // ← ini seharusnya di OrderService, bukan di middleware!
        req := c.Locals("req").(CreateOrderRequest)
        if req.Amount <= 0 {
            return fiber.NewError(400, "Amount must be positive")
        }
        return next(c)
    }
}

// BENAR: business logic di service, aspect hanya concern teknis
func logAndMeasure(next fiber.Handler) fiber.Handler {
    return func(c *fiber.Ctx) error {
        start := time.Now()
        err := next(c) // ← tidak modifikasi args atau result
        log.Infof("%v took %v", c.Route().Path, time.Since(start))
        return err
    }
}

Gunakan Pointcut yang Spesifik #

// ANTI-PATTERN: pointcut terlalu luas — intercept semua method di seluruh aplikasi
var logAll = func(method string) bool { return true } // ← semua method!
// Intercept method internal framework, library pihak ketiga, dsb.

// BENAR: spesifik ke package dan layer yang memang butuh
var logServiceLayer = func(method string) bool {
    return strings.HasPrefix(method, "com.example.service.")
}

// Atau gunakan marker-based pointcut — lebih eksplisit
var auditMethod = func(hasTag bool) bool { return hasTag }

Anti-Pattern yang Harus Dihindari #

// ✗ Business logic di Aspect — aspect menjadi "controller" terselubung
func handle(next fiber.Handler) fiber.Handler {
    return func(c *fiber.Ctx) error {
        if !hasEnoughStock() {
            return fiber.NewError(400, "out of stock") // ← logika bisnis!
        }
        return next(c)
    }
}
// ✓ Validasi stok di dalam service, bukan di aspect

// ✗ Pointcut terlalu luas — performa terdampak, debugging sulit
var intercept = func(next fiber.Handler) fiber.Handler { return next } // ← semua method
// ✓ Batasi ke layer spesifik: execution(* com.example.service..*(..))

// ✗ Terlalu banyak aspect bertumpuk — alur eksekusi tidak jelas
// LoggingMiddleware → MetricsMiddleware → RetryMiddleware → CachingMiddleware → AuthMiddleware
// Tidak ada yang tahu aspek mana yang jalan duluan
// ✓ Gunakan urutan pendaftaran eksplisit (app.Use urut)
app.Use(AuthMiddleware())     // paling awal
app.Use(LoggingMiddleware())
app.Use(MetricsMiddleware())  // paling akhir

// ✗ Tidak mendokumentasikan aspect — engineer baru tidak tahu kenapa method tiba-tiba ter-log
// ✓ Tambahkan komentar di middleware dan di method yang di-intercept
// ✓ Catat di README atau internal wiki: "semua handler otomatis di-log oleh LoggingMiddleware"

Kapan Menggunakan dan Tidak Menggunakan AOP #

Gunakan AOP Jika: #

KondisiContoh
Concern berulang di 10+ tempatLogging di semua handler, auth di semua endpoint
Concern harus konsistenSemua transaction harus dibuka/ditutup dengan cara yang sama
Perubahan concern tidak boleh menyentuh kode bisnisGanti logging library tidak boleh ubah service
Concern bersifat deklaratif@RequireAdmin, @Cacheable, @Transactional

Jangan Gunakan AOP Jika: #

❌ Concern hanya dibutuhkan di satu atau dua tempat
   → Duplikasi kecil lebih baik dari kompleksitas AOP yang tidak perlu

❌ Business logic perlu masuk ke aspect
   → Tanda desain salah — refactor service, bukan tambah aspect

❌ Tim belum familiar dengan AOP
   → Debugging aspect yang salah sangat membingungkan tanpa pemahaman dasar

❌ Aplikasi sangat kecil
   → Overhead setup tidak sebanding dengan manfaat

❌ Butuh intercept method private atau self-call di Java
   → Spring AOP tidak bisa; butuh AspectJ atau refactor arsitektur

Checklist Implementasi AOP #

DESAIN ASPECT:
  □ Aspect hanya berisi logika cross-cutting, bukan logika bisnis
  □ Setiap aspect punya satu tanggung jawab (SRP)
  □ Aspect didokumentasikan: apa yang ia lakukan, ke method mana ia berlaku

POINTCUT:
  □ Pointcut spesifik ke layer atau annotation tertentu
  □ Tidak ada pointcut yang terlalu luas (execution(* *.*(..)))
  □ Pointcut diuji dengan unit test atau integration test

ORDERING:
  □ Urutan eksekusi aspect ditentukan eksplisit dengan @Order
  □ Auth aspect dijalankan sebelum logging aspect

LIMITASI (Java Spring AOP):
  □ Tidak ada method private atau final yang diharapkan ter-intercept
  □ Tidak ada internal self-call (this.method()) yang diharapkan ter-intercept
  □ Tim mengetahui proxy mechanism dan limitasinya

TESTING:
  □ Ada test yang membuktikan aspect bekerja saat dipanggil dari luar
  □ Ada test yang membuktikan business logic tetap berjalan benar dengan aspect
  □ Error dari aspect tidak menyembunyikan error bisnis yang sebenarnya

Ringkasan #

  • AOP memisahkan cross-cutting concern — logika seperti logging, auth, metrics, dan transaction dipisahkan ke Aspect sehingga kode bisnis tetap bersih dan fokus.
  • Lima konsep inti: Aspect (modul cross-cutting), Join Point (titik eksekusi yang bisa di-intercept), Pointcut (ekspresi pemilih join point), Advice (kode yang dijalankan), Weaving (proses penggabungan).
  • Lima tipe Advice: @Before, @After, @AfterReturning, @AfterThrowing, @Around — pilih berdasarkan kapan concern perlu dijalankan.
  • Spring AOP berbasis proxy — hanya bisa intercept method public yang dipanggil dari luar bean; this.method() internal call tidak melalui proxy dan tidak ter-intercept.
  • Go menggunakan middleware dan decorator — pola yang lebih eksplisit dan lebih mudah di-debug dibanding annotation-based AOP.
  • Jangan taruh business logic di Aspect — Aspect hanya untuk concern teknis; keputusan bisnis tetap di service.
  • Gunakan Pointcut yang spesifikexecution(* com.example.service..*(..)) jauh lebih aman dari execution(* *(..)).
  • Dokumentasikan semua Aspect — aspect yang tidak terdokumentasi adalah sumber kebingungan bagi engineer baru dan debugging yang menyulitkan.
  • AOP tepat untuk concern yang berulang di banyak tempat dan harus konsisten — untuk concern yang hanya dibutuhkan di satu dua tempat, cukup tulis langsung.

← Sebelumnya: Dependency Injection   Berikutnya: Retry Strategy →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact