Interfaces Implícitas vs Explícitas en Go: Cómo la Magia se Convierte en Pesadilla

Interfaces Implícitas vs Explícitas en Go: Cómo la Magia se Convierte en Pesadilla

Una guía profunda sobre las interfaces implícitas en Go, por qué es peligroso en arquitectura hexagonal, cómo Go te permite escribir 'hexagonal sin pensarlo', y por qué eso rompe la arquitectura. Con ejemplos de desastre real y patrones para hacerlo bien.

Por Omar Flores

Existe una característica de Go que es simultáneamente su mayor fortaleza y su trampa más peligrosa: las interfaces implícitas.

La característica es simple: en Go, no necesitas declarar explícitamente que tu tipo implementa una interfaz. Si tienes los métodos correctos, automáticamente la implementas. Es hermoso. Es elegante. Y es exactamente lo que te permite escribir código que parece seguir arquitectura hexagonal cuando en realidad está roto.

He visto esta trampa una y otra vez. Equipos que creen tener una arquitectura hexagonal limpia, con puertos y adapters bien definidos, solo para descubrir meses después que sus “puertos” en realidad están completamente acoplados, que sus “adapters” dependen de otros adapters, y que el sistema es casi tan rígido como si hubieran escrito código monolítico desde el inicio.

La ironía es que Go te lo permitió sin quejarse. Compiló perfectamente. Los tests pasaron. Nadie en el equipo se dio cuenta de que habían caminado directamente a una trampa arquitectónica.

Este artículo es una exploración profunda de por qué las interfaces implícitas de Go son tan peligrosas en el contexto de arquitectura hexagonal. Explicaré qué son, por qué te permiten escribir código que parece hexagonal cuando no lo es, y cómo identificar y evitar esta trampa. Con ejemplos de desastre real, patrones para hacerlo bien, y un checklist que puedes usar ahora mismo en tu codebase.


Parte 1: Las Interfaces Implícitas y Su Magia

1.1 ¿Qué Son Las Interfaces Implícitas?

Una interfaz implícita en Go significa que no necesitas declarar que implementas una interfaz. Si tu tipo tiene los métodos correctos, automáticamente satisface la interfaz.

Ejemplo simple:

// La interfaz
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Tu tipo
type MyLogger struct{}

func (ml MyLogger) Write(p []byte) (int, error) {
    fmt.Println(string(p))
    return len(p), nil
}

// MyLogger automáticamente es un Writer
// No necesitaste escribir "implements Writer" en ningún lado

Puedes pasarle un MyLogger a cualquier función que espera un Writer:

func SaveToFile(w Writer, data []byte) {
    w.Write(data)
}

func main() {
    logger := MyLogger{}
    SaveToFile(logger, []byte("hello"))
    // Funciona perfectamente
}

Esto es diferente a Java o C#, donde debes declarar explícitamente:

// Java requiere declaración explícita
public class MyLogger implements Writer {
    // ...
}

1.2 Por Qué Esto Es Hermoso

La característica de interfaces implícitas es increíblemente poderosa. Permite:

1. Desacoplamiento natural:

// Tu código no depende de una interfaz específica
// Simplemente satisface lo que necesita

type MyService struct{}

func (ms MyService) Log(msg string) {
    fmt.Println(msg)
}

// Meses después, alguien crea una interfaz Logger
type Logger interface {
    Log(msg string)
}

// Y mágicamente, MyService ya la implementa
// Sin cambiar una línea de código

2. Pequeñas interfaces:

Go te anima a crear interfaces pequeñas (1-3 métodos), porque satisfacerlas es trivial. En Java, crearías una interfaz monolítica con 20 métodos.

3. Adopción retroactiva:

Si trabajas con librerías de terceros, puedes crear interfaces que sus tipos ya satisfacen:

type Reader interface {
    Read(p []byte) (int, error)
}

// La librería de terceros tiene un MyType con método Read
// Nunca planeó ser un Reader
// Pero automáticamente lo es

myType := thirdparty.NewMyType()
useAsReader(myType) // Funciona

1.3 Comparación: Go vs Java vs Python

Go (implícito):

type Reader interface {
    Read(p []byte) (int, error)
}

type MyType struct{}
func (mt MyType) Read(p []byte) (int, error) { ... }

// MyType ya es un Reader

Java (explícito):

public interface Reader {
    int read(byte[] p) throws IOException;
}

public class MyType implements Reader {
    public int read(byte[] p) throws IOException { ... }
}

// MyType debe declarar que implementa Reader

Python (duck typing):

class Reader:
    def read(self, p: bytes) -> int:
        pass

class MyType:
    def read(self, p: bytes) -> int:
        pass

# Python no tiene interfaces, confía en que tengas los métodos
# Si no los tienes, falla en runtime

Go es el punto dulce: tiene la flexibilidad de Python (no necesitas declaración) con la seguridad de Java (el compilador verifica tipos en tiempo de compilación).

O eso es lo que parece…


Parte 2: El Peligro Oculto en Hexagonal

2.1 El Problema: Demasiada Libertad

Aquí viene el twist: la facilidad de crear interfaces implícitas en Go te permite escribir código que parece seguir arquitectura hexagonal cuando en realidad está completamente roto.

Y lo peor es que el compilador no te lo dirá. Tu código compilará perfectamente. Tus tests pasarán. Y solo cuando necesites cambiar algo, descubrirás que tu arquitectura es frágil.

2.2 Escenario de Desastre: Adapters Acoplados

Imagina que estás construyendo un servicio que maneja órdenes. Tomas las mejores prácticas de hexagonal y empiezas así:

// Dominio
type Order struct {
    ID    string
    Total float64
}

// Puerto: Persistencia
type OrderRepository interface {
    Save(order *Order) error
    GetByID(id string) (*Order, error)
}

// Adapter: PostgreSQL
type PostgresOrderRepository struct {
    db *sql.DB
}

func (p *PostgresOrderRepository) Save(order *Order) error {
    // Guardar en Postgres
    _, err := p.db.Exec("INSERT INTO orders...", order.ID, order.Total)
    return err
}

func (p *PostgresOrderRepository) GetByID(id string) (*Order, error) {
    // Obtener de Postgres
    row := p.db.QueryRow("SELECT * FROM orders WHERE id = $1", id)
    var order Order
    row.Scan(&order.ID, &order.Total)
    return &order, nil
}

Hasta aquí, perfecto. Ahora necesitas notificaciones por email cuando se crea una orden. Creas un adapter para email:

// Adapter: Email
type EmailService struct {
    smtp *mail.SMTP
}

func (e *EmailService) SendOrderConfirmation(order *Order) error {
    // Enviar email
    msg := fmt.Sprintf("Tu orden #%s por $%.2f ha sido creada", order.ID, order.Total)
    return e.smtp.Send("user@example.com", msg)
}

Perfecto. Ahora crean un caso de uso para crear órdenes:

// Caso de Uso
type CreateOrderUseCase struct {
    repo OrderRepository
    email *EmailService
}

func (c *CreateOrderUseCase) Execute(order *Order) error {
    if err := c.repo.Save(order); err != nil {
        return err
    }

    // Aquí está el problema
    if err := c.email.SendOrderConfirmation(order); err != nil {
        // ¿Qué hacemos si falla el email?
        // ¿Revertimos la orden?
    }

    return nil
}

¿Ves el problema?

El caso de uso está acoplado directamente a EmailService. No a una interfaz Notifier. A la implementación específica de email.

Peor aún, ¿qué pasa cuando necesitas SMS también? ¿Teléfono? ¿Notificación push?

// Esto es lo que termina pasando
type CreateOrderUseCase struct {
    repo OrderRepository
    email *EmailService
    sms *SMSService
    push *PushNotificationService
    slack *SlackNotificationService
    // ... 10 más
}

Ahora tienes un monolito disfrazado de arquitectura hexagonal.

¿Por qué pasó esto?

Porque fue demasiado fácil crear el acoplamiento. En Go, simplemente agregaste email *EmailService a la estructura, y compiló. No había interfaz, no había puerto. Solo lo olvidaste.

En Java, si intentas esto:

public class CreateOrderUseCase {
    private EmailService email; // Error: EmailService no es una interfaz
}

Java te fuerza a crear una interfaz. Y al crear la interfaz, empiezas a pensar: “¿Realmente quiero que el caso de uso dependa de una interfaz para notificaciones?”

Go no te fuerza. Y por eso es peligroso.

2.3 Patrón de Desastre: Interfaces Accidentales

Aquí hay un patrón que veo constantemente:

Paso 1: Crear adapters específicos

type PaymentProcessor struct { ... }
type NotificationService struct { ... }
type LoggingService struct { ... }

Paso 2: Importarlos en el caso de uso

type OrderService struct {
    payments *PaymentProcessor
    notifications *NotificationService
    logging *LoggingService
}

Paso 3: Meses después, alguien intenta testear

func TestCreateOrder(t *testing.T) {
    svc := OrderService{
        payments: &PaymentProcessor{
            gateway: realPaymentGateway, // Oops, necesitas acceso real
        },
        notifications: &NotificationService{
            smtp: realSMTPConnection, // Oops, necesitas servidor real
        },
        logging: &LoggingService{
            file: openFile(), // Oops, necesitas archivo real
        },
    }

    // Para testear, necesitas todas las dependencias reales
    // Los tests son lentos y frágiles
}

Paso 4: Alguien se da cuenta y agrega interfaces

type PaymentGateway interface { ... }
type Notifier interface { ... }
type Logger interface { ... }

type OrderService struct {
    payments PaymentGateway
    notifications Notifier
    logging Logger
}

Pero aquí está el problema: El daño ya está hecho. Ahora tienes todas las dependencias acopladas en el caso de uso. Las interfaces son solo un band-aid.


Parte 3: Interfaces Explícitas Como Defensa

3.1 El Antídoto: Ser Explícito Sobre Intenciones

La solución es pensar explícitamente sobre qué interfaces realmente necesitas, antes de crear un tipo.

En lugar de:

// ❌ Implícito: ¿Es esto un puerto?
type EmailService struct {
    smtp *mail.SMTP
}

func (e *EmailService) SendOrderConfirmation(order *Order) error {
    // ...
}

Haz esto:

// ✅ Explícito: Define el puerto primero
type OrderNotifier interface {
    NotifyOrderCreated(order *Order) error
}

// Ahora, la implementación por email
type EmailNotifier struct {
    smtp *mail.SMTP
}

func (e *EmailNotifier) NotifyOrderCreated(order *Order) error {
    msg := fmt.Sprintf("Tu orden #%s ha sido creada", order.ID)
    return e.smtp.Send("user@example.com", msg)
}

// Y puedes tener otras implementaciones
type SlackNotifier struct {
    webhook string
}

func (s *SlackNotifier) NotifyOrderCreated(order *Order) error {
    return s.postToSlack(fmt.Sprintf("Nueva orden: %s", order.ID))
}

// El caso de uso SOLO depende de la interfaz, no de implementaciones
type CreateOrderUseCase struct {
    repo OrderRepository
    notifier OrderNotifier
}

func (c *CreateOrderUseCase) Execute(order *Order) error {
    if err := c.repo.Save(order); err != nil {
        return err
    }

    // Usa la interfaz, no la implementación
    return c.notifier.NotifyOrderCreated(order)
}

¿Qué cambió?

  1. La interfaz existe primero: OrderNotifier es un puerto claro
  2. Las implementaciones la satisfacen: EmailNotifier, SlackNotifier
  3. El caso de uso depende de la interfaz: No está acoplado a email específicamente
  4. Puedes cambiar implementaciones fácilmente: Agregar SMS es solo crear SMSNotifier

3.2 Patrón: Definir Puertos Antes que Adapters

Aquí hay un checklist para saber si estás siendo explícito:

Antes de crear un adapter, pregúntate:

  1. ¿Cuál es el puerto (interfaz) que necesito?
  2. ¿Cuál es el contrato que esta interfaz establece?
  3. ¿Cómo sé que esta interfaz es realmente un puerto y no solo “una interfaz”?

El patrón correcto:

// PASO 1: Definir el puerto (interfaz)
// Este puerto vive en el dominio o en application
type UserRepository interface {
    GetByID(id string) (*User, error)
    Save(user *User) error
    Delete(id string) error
}

// PASO 2: Implementar el adapter
// Este adapter vive en adapter/repository
type PostgresUserRepository struct {
    db *sql.DB
}

func (p *PostgresUserRepository) GetByID(id string) (*User, error) {
    // Implementación específica de Postgres
    row := p.db.QueryRow("SELECT * FROM users WHERE id = $1", id)
    var user User
    row.Scan(&user.ID, &user.Name)
    return &user, nil
}

// PASO 3: El caso de uso usa la interfaz, no la implementación
type GetUserUseCase struct {
    repo UserRepository // ← Interfaz, no *PostgresUserRepository
}

func (u *GetUserUseCase) Execute(id string) (*User, error) {
    return u.repo.GetByID(id)
}

3.3 El Patrón del “Fake” para Verificar Puertos

Aquí hay un patrón potente para verificar que tu interfaz es realmente un puerto:

Crea una implementación “fake” sin dependencias externas:

// Fake implementation para testing
type FakeUserRepository struct {
    users map[string]*User
}

func (f *FakeUserRepository) GetByID(id string) (*User, error) {
    user, exists := f.users[id]
    if !exists {
        return nil, errors.New("not found")
    }
    return user, nil
}

func (f *FakeUserRepository) Save(user *User) error {
    f.users[user.ID] = user
    return nil
}

func (f *FakeUserRepository) Delete(id string) error {
    delete(f.users, id)
    return nil
}

// Ahora testea sin base de datos
func TestGetUser(t *testing.T) {
    repo := &FakeUserRepository{
        users: map[string]*User{
            "1": {ID: "1", Name: "John"},
        },
    }

    useCase := &GetUserUseCase{repo: repo}
    user, err := useCase.Execute("1")

    assert.NoError(t, err)
    assert.Equal(t, "John", user.Name)
}

Si puedes crear un Fake fácilmente, el puerto está bien diseñado.

Si es difícil hacer un Fake, significa la interfaz probablemente mezcla responsabilidades.


Parte 4: El Lado Oscuro - Cómo Go Te Engaña

4.1 La Ilusión de Satisfacción

Aquí hay algo sutil pero peligroso. Go te permite hacer esto:

// Defines una interfaz
type Reader interface {
    Read(p []byte) (int, error)
}

// Creas un tipo que no está intentando satisfacerla
type MyType struct {
    data []byte
}

func (m *MyType) Read(p []byte) (int, error) {
    copy(p, m.data)
    return len(m.data), nil
}

// Y luego, sin darte cuenta, usas MyType donde se espera Reader
func ProcessData(r Reader, data []byte) {
    r.Read(data)
}

func main() {
    mt := &MyType{data: []byte("hello")}
    ProcessData(mt, make([]byte, 10)) // Funciona
}

El peligro: Nunca tuviste la intención de que MyType fuera un Reader. Solo pasó que tenía ese método. Y ahora está acoplado.

4.2 Accidente Arquitectónico

Imagina este escenario real:

// Crear varias cosas que tienes métodos Log
type PaymentGateway struct{}
func (p *PaymentGateway) Log(msg string) { fmt.Println("[Payment]", msg) }

type EmailService struct{}
func (e *EmailService) Log(msg string) { fmt.Println("[Email]", msg) }

type OrderService struct{}
func (o *OrderService) Log(msg string) { fmt.Println("[Order]", msg) }

// Sin darte cuenta, todos satisfacen esta interfaz implícitamente
type Logger interface {
    Log(msg string)
}

// Y ahora podrías pasar cualquiera donde se espera Logger
// Lo cual es completamente incorrecto

Nadie planificó esto. Sucedió por accidente. Y es exactamente lo que hace que las interfaces implícitas sean peligrosas.

4.3 El Test Falso Negativo

Aquí hay un ejemplo de cómo Go te puede engañar:

// Tu código "parece" bien
type CreateOrderUseCase struct {
    repo OrderRepository      // ✅ Puerto
    notifier OrderNotifier    // ✅ Puerto
}

// Pero en realidad hiciste esto
type CreateOrderUseCase struct {
    repo OrderRepository           // ✅ Puerto
    notifier OrderNotifier         // Espera... es esto un puerto?
    stripe *stripe.Client          // ❌ Acoplamiento directo
    database *sql.DB               // ❌ Acoplamiento directo
}

Y el compilador no se queja porque OrderNotifier es una interfaz.


Parte 5: Cómo Identificar La Trampa en Tu Código

5.1 Checklist: ¿Es Esto Un Puerto Real?

Para cada interfaz en tu código de casos de uso, pregúntate:

1. ¿Definí esta interfaz explícitamente?

// ✅ Bien
type UserRepository interface { ... }

// ❌ Malo: Solo agregaste un campo de tipo que "parece" una interfaz
type UserService struct {
    repo *PostgresUserRepository
}

2. ¿El nombre de la interfaz describe QUÉ hace, no CÓMO lo hace?

// ✅ Bien: Describe qué hace
type UserRepository interface {
    GetByID(id string) (*User, error)
}

// ❌ Malo: Describe cómo lo hace
type PostgresUserRepository interface {
    QueryByID(id string) (*User, error)
}

3. ¿La interfaz tiene 1-3 métodos?

// ✅ Bien: Pequeña, coherente
type Reader interface {
    Read(p []byte) (int, error)
}

// ❌ Malo: Hace demasiadas cosas
type Database interface {
    Query(...)
    Exec(...)
    Create(...)
    Read(...)
    Update(...)
    Delete(...)
    // ... 20 métodos más
}

4. ¿Puedo crear una implementación “fake” sin dependencias?

// ✅ Bien: Fácil de hacer fake
type FakeRepository struct {
    data map[string]*User
}

// ❌ Malo: Necesita dependencias externas incluso para fake
type FakeRepository struct {
    db *sql.DB // Aún necesitas una BD
}

5. ¿Si cambio la implementación, el dominio se ve afectado?

// ✅ Bien: El dominio no se ve afectado
// Cambiar de PostgreSQL a MongoDB: solo cambias el adapter

// ❌ Malo: El dominio sabe de la implementación
type User struct {
    postgresID string // ← El dominio sabe de PostgreSQL
}

5.2 Análisis de Código: Encontrando Acoplamiento Oculto

Ejecuta este análisis en tu codebase:

Paso 1: Busca todos los struct en usecase

grep -r "type.*struct" internal/usecase/

Paso 2: Para cada struct, lista sus campos

type CreateOrderUseCase struct {
    repo OrderRepository        // Campo 1
    notifier OrderNotifier      // Campo 2
    stripe *stripe.Client       // ¿Es esto un puerto?
}

Paso 3: Para cada campo, pregunta: ¿Es una interfaz o una implementación?

// Si el tipo empieza con un nombre concreto: *stripe.Client
// Probablemente es acoplamiento directo

// Si el tipo es una interfaz: OrderNotifier
// Probablemente es un puerto

Paso 4: Busca campos que no son interfaces

grep -A 20 "type.*UseCase struct" internal/usecase/*.go | grep -v "interface"

Si encuentras campos como:

  • *postgres.DB
  • *stripe.Client
  • *smtp.Connection
  • *firebase.App

Tienes acoplamiento.


Parte 6: Patrones Para Hacerlo Bien

6.1 El Patrón “Puerto Primero”

Siempre define el puerto antes de la implementación:

// 📁 internal/domain/repository.go
// O en internal/usecase si no es crítico de negocio

// PASO 1: Define el puerto
type UserRepository interface {
    GetByID(ctx context.Context, id string) (*User, error)
    Save(ctx context.Context, user *User) error
    Delete(ctx context.Context, id string) error
}

// 📁 internal/adapter/repository/postgres.go

// PASO 2: Implementa el adapter
type PostgresUserRepository struct {
    db *sql.DB
}

func (p *PostgresUserRepository) GetByID(ctx context.Context, id string) (*User, error) {
    row := p.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    var user User
    if err := row.Scan(&user.ID, &user.Name); err != nil {
        return nil, err
    }
    return &user, nil
}

// 📁 internal/usecase/create_user.go

// PASO 3: El caso de uso usa la interfaz
type CreateUserUseCase struct {
    repo UserRepository
}

func (c *CreateUserUseCase) Execute(ctx context.Context, user *User) error {
    return c.repo.Save(ctx, user)
}

Estructura de carpetas:

internal/
├── domain/
│   ├── user.go              # Entidades del dominio
│   └── repository.go        # Puertos (interfaces)
├── usecase/
│   └── create_user.go       # Casos de uso
├── adapter/
│   ├── repository/
│   │   └── postgres.go      # Implementación del puerto
│   └── http/
│       └── user_handler.go  # Entrada HTTP

6.2 El Patrón de “Mockeo Explícito”

En lugar de confiar en satisfacción implícita, crea mocks explícitos:

// ✅ Bien: Mock explícito para testing
type MockUserRepository struct {
    GetByIDFunc func(ctx context.Context, id string) (*User, error)
    SaveFunc func(ctx context.Context, user *User) error
}

func (m *MockUserRepository) GetByID(ctx context.Context, id string) (*User, error) {
    if m.GetByIDFunc != nil {
        return m.GetByIDFunc(ctx, id)
    }
    return nil, errors.New("not implemented")
}

func (m *MockUserRepository) Save(ctx context.Context, user *User) error {
    if m.SaveFunc != nil {
        return m.SaveFunc(ctx, user)
    }
    return errors.New("not implemented")
}

// Test explícito
func TestCreateUser(t *testing.T) {
    mock := &MockUserRepository{
        SaveFunc: func(ctx context.Context, user *User) error {
            return nil // Comportamiento esperado
        },
    }

    useCase := &CreateUserUseCase{repo: mock}
    err := useCase.Execute(context.Background(), &User{})

    assert.NoError(t, err)
}

6.3 El Patrón de “Constructor Explícito”

En lugar de pasar tipos directamente, usa constructores que dejan claro qué inyectas:

// ❌ Confuso: ¿Qué parámetros son puertos?
type OrderService struct {
    a *TypeA
    b *TypeB
    c *TypeC
}

// ✅ Claro: Constructor explícito
func NewOrderService(
    repo OrderRepository,        // ← Puerto
    notifier OrderNotifier,      // ← Puerto
    logger Logger,               // ← Puerto
) *OrderService {
    return &OrderService{
        repo: repo,
        notifier: notifier,
        logger: logger,
    }
}

// Ahora es obvio qué son puertos
service := NewOrderService(postgresRepo, emailNotifier, zeroLogger)

Parte 7: Ejemplos de Desastre vs Corrección

7.1 Ejemplo 1: Payment Processing

❌ MALO: Acoplamiento implícito

type ProcessPaymentUseCase struct {
    stripe *stripe.Client      // ❌ Acoplado a Stripe
    db *sql.DB                 // ❌ Acoplado a SQL
    email *mail.SMTP           // ❌ Acoplado a SMTP
}

func (p *ProcessPaymentUseCase) Execute(payment *Payment) error {
    // ¿Qué pasa si quiero cambiar a Paypal?
    // ¿Qué pasa si quiero cambiar a MongoDB?
    // El caso de uso está lleno de detalles técnicos
    return p.stripe.Charge(payment.Amount)
}

✅ BIEN: Puertos explícitos

// Puertos
type PaymentGateway interface {
    Charge(ctx context.Context, amount float64) (transactionID string, err error)
}

type PaymentRepository interface {
    Save(ctx context.Context, payment *Payment) error
}

type PaymentNotifier interface {
    NotifyPaymentProcessed(ctx context.Context, payment *Payment) error
}

// Caso de Uso
type ProcessPaymentUseCase struct {
    gateway PaymentGateway
    repo PaymentRepository
    notifier PaymentNotifier
}

func (p *ProcessPaymentUseCase) Execute(ctx context.Context, payment *Payment) error {
    // Solo depende de interfaces
    transactionID, err := p.gateway.Charge(ctx, payment.Amount)
    if err != nil {
        return err
    }

    payment.TransactionID = transactionID
    if err := p.repo.Save(ctx, payment); err != nil {
        return err
    }

    return p.notifier.NotifyPaymentProcessed(ctx, payment)
}

// Implementaciones (en adapter/)
type StripePaymentGateway struct { ... }
type PaypalPaymentGateway struct { ... }
type PostgresPaymentRepository struct { ... }
type EmailPaymentNotifier struct { ... }

Ahora, cambiar de Stripe a Paypal es trivial:

// Hoy
gateway := &StripePaymentGateway{client: stripeClient}

// Mañana
gateway := &PaypalPaymentGateway{client: paypalClient}

// El resto del código sin cambios

7.2 Ejemplo 2: User Management

❌ MALO:

type UserService struct {
    db *sql.DB
    cache *redis.Client
    validator *govalidator.StringValidator
    hasher *bcrypt.Hasher
}

func (u *UserService) Register(email string, password string) error {
    // Todo acoplado a implementaciones específicas
    if err := u.validator.ValidateEmail(email); err != nil {
        return err
    }

    hashed, err := u.hasher.Hash(password)
    if err != nil {
        return err
    }

    // ...
}

✅ BIEN:

// Puertos de negocio
type EmailValidator interface {
    IsValid(email string) bool
}

type PasswordHasher interface {
    Hash(password string) (string, error)
    Verify(password, hash string) bool
}

type UserRepository interface {
    Save(ctx context.Context, user *User) error
    GetByEmail(ctx context.Context, email string) (*User, error)
}

// Caso de Uso
type RegisterUserUseCase struct {
    emailValidator EmailValidator
    passwordHasher PasswordHasher
    userRepo UserRepository
}

func (r *RegisterUserUseCase) Execute(ctx context.Context, email, password string) error {
    if !r.emailValidator.IsValid(email) {
        return errors.New("invalid email")
    }

    hash, err := r.passwordHasher.Hash(password)
    if err != nil {
        return err
    }

    user := &User{Email: email, PasswordHash: hash}
    return r.userRepo.Save(ctx, user)
}

Parte 8: Checklist Final - ¿Tu Arquitectura Es Realmente Hexagonal?

Usa este checklist para verificar que tus puertos son realmente explícitos:

🔍 Examen de Puertos:

  • ¿Definí explícitamente cada interfaz que mi caso de uso necesita?
  • ¿Cada interfaz tiene un nombre que describe QUÉ hace, no CÓMO?
  • ¿Cada interfaz tiene 1-3 métodos (no más)?
  • ¿Puedo crear un Fake implementation sin dependencias externas?
  • ¿Las implementaciones viven en adapter/ o carpeta específica?
  • ¿Los casos de uso reciben interfaces, no tipos concretos?
  • ¿Mi dominio no importa nada de adapter/?
  • ¿Si cambio de PostgreSQL a MongoDB, cambio solo el adapter?

🔴 Red Flags (Señales de Peligro):

  • ¿Un caso de uso recibe *stripe.Client directamente?
  • ¿Un caso de uso recibe *sql.DB directamente?
  • ¿Un caso de uso recibe *redis.Client directamente?
  • ¿Tengo métodos Log/Debug/Info que aceptan tipos específicos?
  • ¿Tengo campos que son “adapters” pero no en la carpeta adapter/?

Si contestas “sí” a cualquier red flag, tienes acoplamiento.


Conclusión: La Responsabilidad Es Tuya

Go te da libertad. Las interfaces implícitas significan que puedes escribir código hermoso, desacoplado, y flexible. O puedes escribir código que parece desacoplado pero que en realidad está monolíticamente acoplado.

La diferencia está en ser consciente de lo que estás haciendo.

No confíes en la satisfacción implícita. Define tus puertos explícitamente. Nómbralos claramente. Colócalos donde pertenecen. Y crea implementaciones que realmente satisfacen esos puertos.

La facilidad de Go no es una excusa para ser perezoso con la arquitectura. Es una oportunidad para ser excelente.

Tags

#golang #interfaces #hexagonal #architecture #design-patterns #pitfalls