Autonomía en cada escala#
La autonomía no aparece únicamente en el módulo más grande. Puede fomentarse de forma granular cada vez que la perspectiva de API-DD se aplica recursivamente.
| Escala | Qué significa autonomía | |---|---| | Módulo | Reúne la capacidad y protege sus invariantes | | API | Ofrece una conversación suficiente sin revelar cómo se coordina el interior | | Mensaje de resultado | Tiene significado propio y no comparte estado mutable con quien lo produjo |
Autonomía no significa aislamiento. Un módulo puede consumir otras APIs y un resultado puede contener varios valores. La diferencia es que cada elemento conserva un límite claro: ningún consumidor necesita reparar su estado, completar su significado ni conocer una representación ajena.
Aplicar el fundamento a un resultado no lo convierte en otro módulo. El resultado sigue siendo vocabulario de la API, pero también necesita propiedad y validez propias.
API autónoma#
Una API es autónoma cuando el consumidor puede expresar una intención y comprender la respuesta dentro de la misma conversación. No necesita abrir la implementación, consultar una estructura interna ni coordinar colaboradores que pertenecen al proveedor.
Una secuencia pública puede formar parte de un protocolo real. Pierde autonomía cuando sus pasos solo existen para terminar de montar el módulo:
value := Interval{}
value.SetStart(2)
value.SetEnd(8)
value.Validate()La intención completa puede expresarse en un solo mensaje:
Interval.New(2, 8) → Interval | InvalidIntervalLa autonomía tampoco exige que cada respuesta incluya todos los datos imaginables. Incluye lo necesario para que el consumidor actúe ante las diferencias que el contrato reconoce.
Módulo completo#
Un módulo es completo cuando reúne lo necesario para cumplir el contrato que ofrece. El consumidor usa su capacidad mediante la API sin completar pasos internos, corregir su estado ni decidir cómo deben coordinarse sus dependencias.
Completo no significa autosuficiente. El módulo puede leer, calcular o producir efectos mediante otras APIs. Su responsabilidad consiste en gobernar esas colaboraciones y traducirlas al contrato que ofrece.
Una señal de incompletitud aparece cuando varios consumidores repiten la misma coordinación para obtener una capacidad que debería pertenecer al módulo. La solución no es esconder cualquier secuencia: es asignar la responsabilidad al límite que puede garantizarla.
Módulo válido#
Un módulo válido conserva sus invariantes en todos los estados observables. Puede rechazar una entrada o devolver un error; evita continuar con un estado incoherente que otro consumidor tenga que descubrir después.
La validez se protege en cada entrada de información: construcción, cambios de estado, datos externos, persistencia y resultados de otras APIs.
Un estado inicial vacío puede ser válido. También puede ser inválido pero seguro. La API debe distinguirlo antes de producir un efecto incorrecto.
Resultados autónomos#
Un resultado autónomo pertenece a la conversación que lo recibe. Su significado está completo y su contenido no cambia porque el proveedor continúe trabajando ni porque otro consumidor lo utilice.
Esto requiere evitar referencias compartidas a estado mutable. Devolver directamente un slice, un mapa o un puntero interno permite que el consumidor modifique al proveedor y que el proveedor altere un resultado ya entregado. Ambos dejan de ser autónomos.
Puede conservarse la autonomía mediante valores copiados, campos privados y operaciones de lectura, o una transferencia explícita de propiedad. Si copiar resulta demasiado costoso, el módulo puede ofrecer una API de recorrido sin exponer su colección interna.
Go no tiene una declaración general de inmutabilidad. Aquí, inmutable significa que la API pública no permite cambiar el resultado y que el proveedor no puede alterarlo después de entregarlo. Un struct copiado puede contener slices, mapas o punteros que aún comparten memoria; la autonomía debe llegar hasta cada referencia mutable, no detenerse en el tipo exterior.
Ejemplo en Go#
Collection conserva su estado y entrega un Snapshot autónomo. El snapshot copia los datos y solo ofrece operaciones de lectura.
package collection
import "errors"
var (
ErrInvalidLimit = errors.New("invalid limit")
ErrInvalid = errors.New("invalid collection")
ErrFull = errors.New("collection full")
)
type Snapshot struct {
values []string
}
func newSnapshot(values []string) Snapshot {
return Snapshot{values: clone(values)}
}
func (s Snapshot) Len() int {
return len(s.values)
}
func (s Snapshot) At(index int) (string, bool) {
if index < 0 || index >= len(s.values) {
return "", false
}
return s.values[index], true
}
type Collection struct {
limit int
values []string
}
func New(limit int) (*Collection, error) {
if limit <= 0 {
return nil, ErrInvalidLimit
}
return &Collection{limit: limit}, nil
}
func (c *Collection) Add(value string) error {
if c == nil || c.limit <= 0 {
return ErrInvalid
}
if len(c.values) == c.limit {
return ErrFull
}
c.values = append(c.values, value)
return nil
}
func (c *Collection) Snapshot() Snapshot {
if c == nil {
return Snapshot{}
}
return newSnapshot(c.values)
}
func clone(values []string) []string {
return append([]string(nil), values...)
}New produce un módulo válido o un error. Add responde de forma segura incluso ante el valor cero de Go. Snapshot no conserva el slice de Collection ni lo expone: Len y At permiten leerlo sin ofrecer una operación de mutación.
Valores iniciales y ausencia#
En Go, todo tipo concreto tiene un valor cero. Publicar New no lo elimina. El contrato puede tratarlo como útil, como ausencia o como inválido pero seguro. La especificación de Go define el mecanismo; la API define su significado.
Ausente y presente con valor cero tampoco son siempre equivalentes. Si la diferencia cambia el comportamiento, puede representarse mediante (T, bool), un puntero o un resultado nombrado. Los formatos de transporte se traducen en el adaptador para que el módulo reciba significado, no detalles de serialización.
Dependencias autónomas#
Una dependencia forma parte de la construcción cuando el módulo no puede cumplir su contrato sin ella. Recibirla mediante una API hace visible qué necesita, pero no traslada su coordinación al consumidor.
No hace falta introducir una interfaz para cada helper. Una dependencia merece contrato propio cuando tiene otros consumidores o proveedores, cruza un límite relevante o evoluciona de forma autónoma. En ese punto los mismos fundamentos pueden aplicarse de nuevo.
Revisión práctica#
La API debe expresar una intención completa, y el módulo debe gobernar sus dependencias e invariantes. La creación produce un valor válido o un fallo explícito. Cada resultado contiene lo necesario para actuar y deja clara la propiedad de cualquier slice, mapa, puntero o elemento mutable.
La autonomía se conserva desde el módulo hasta el resultado que cruza su API.