Número 74
6 de decembro de 2023

Outras maneiras de restruturar código

Co gallo do sistema de comentarios, teño que facer alteracións no código do meu xerador de sitios web. O problema que teño é que o escribín para aprender a linguaxe Go, e iso nótase moitísimo. Xa non é que nalgunhas partes pareza que non sabía Go; o que semella nelas é que non sabía programar!

Un home cun capirote ten varios accidentes no traballo.

Por ese motivo, levo uns días a reestruturar (ou “refactorizar”, como dicimos os que temos os miolos podres polo inglés) todo ese código. E, xa que estou, tamén engado tests unitarios, que descubrín uns poucos sitios nos que había erros nos que eu non reparara.

Por exemplo, teño un pequeno depurador (ou “sanitizador”, como dicimos os que etc. etc.) para deixar o HTML ben limpiño e relucente e libre de ataques. Ou así o faría se non tivese un erro que o impedía.

// A ver se atopades o erro.
func Render(w io.Writer, p *page.Page) error {
  buf := &bytes.Buffer{}
  err := markdown.Renderer().Render(buf, p.Source, p.Root)
  if err != nil {
    return err
  }
  buf.WriteTo(w)
  _, err = sanitizer.SanitizeReader(buf).WriteTo(w)
  return err
}

Visto isto, igual xa intuídes que non era coincidencia ningunha que eu estivera a falar nestes días da inxección de dependencias: estou a escribir moitos tests e para iso teño que usar moito esa técnica, aínda que ás veces teño que facer adaptacións.

Por exemplo, quedei moi pancho despois de vos dicir: “non abrades ficheiros nas vosas funcións; no seu canto, que as funcións os reciban abertos”, pero non dixen nada do que facer cando o cometido da función é percorrer o sistema de ficheiros e ir abrindo os que atope.

// Percorre o sistema de ficheiros comezando por s.InputPath
err := filepath.Walk(s.InputPath, func(path string, info os.FileInfo, err error) error {
  // Ignora os directorios.
  if info.IsDir {
    return nil
  }
  // Abre os ficheiros.
  file, err := os.Open(path)
  if err != nil {
    return err
  }
  // … e agora fai cousas con eles …
}

O meu test tería que crear unha árbore de directorios e executar a función nela, e despois borrar toda esa árbore de directorios. Évos moito traballo, e eu son un preguiceiro, así que, para non ter que facelo, inventei unha nova interface con funcións para percorrer o sistema de ficheiros, abrir ficheiros e ler o seu contido, e despois modifiquei o meu código para que usara esta nova interface.

type File interface {
  Name() string
  GoTo(name string) File
  Read() (Input, error)
  ForAllFiles(fn ForAllFilesFunc) error
}

// Percorre o sistema de ficheiros de s.GetInputBase()
err := s.GetInputBase().ForAllFiles(func(file io.File, err error) error {
  // Le os ficheiros
  input, err := file.Read()
  if err != nil {
    return err
  }
  // … e agora fai cousas con eles …

Finalmente, fixen dúas implementacións desta interface: unha delas accede ao sistema de ficheiros —é a implementación que uso no programa— e a outra actúa sobre ficheiros falsos que gardo na memoria —é a implementación que uso nos tests.

// A implementación de ficheiros reais
func (f *osFile) Read() (Input, error) {
  return os.Open(f.FullPath())
}

// A implementación de ficheiros na memoria
func (f *memoryFile) Read() (Input, error) {
  b, ok := f.fs.files[f.rel]
  if !ok {
    return nil, fmt.Errorf("file does not exist: %s", f.rel)
  }
  return &memoryInput{Reader: *bytes.NewReader(b.content), file: f}, nil
}

Tamén é moi habitual usar esta técnica cando hai código que utiliza o reloxo do sistema. Por exemplo: nun antigo proxecto eu tiña unha clase que producía un rexistro de operacións, e cada entrada do rexistro incluía o “timestamp” no que se producira esa entrada.

class Rexistros {
  public Rexistro NovoRexistro(TipoEvento tipo, DatosEvento datos) {
    Rexistro r = new Rexistro();
    r.timestamp = System.currentTimeMillis();
    r.tipo = tipo;
    r.datos = datos;
    return r;
  }
}

O meu problema viña cando quería facer un test unitario, porque o valor de r.timestamp era impredicible:

Rexistros rexistros = new Rexistros();
Rexistro actual = rexistros.NovoRexistro(tipo, datos);
// Sempre falla
assertEquals(actual.timestamp, System.currentTimeMillis());

A solución foi usar a interface Clock cun reloxo falso nos tests:

class Rexistros {
  public Rexistros(Clock clock) { this.clock = clock; }
  public Rexistro NovoRexistro(TipoEvento tipo, DatosEvento datos) {
    Rexistro r = new Rexistro();
    r.timestamp = this.clock.millis();
    r.tipo = tipo;
    r.datos = datos;
    return r;
  }
  private Clock clock;
}

// e despois nos tests:
Clock fakeClock = new FakeClock(12345678);
// O FakeClock sempre di que o timestamp é 12345678.
Rexistros rexistros = new Rexistros(fakeClock);
Rexistro actual = rexistros.NovoRexistro(tipo, datos);
assertEquals(actual.timestamp, 12345678);

Daquí a uns días hei publicar o código do meu xerador de sitios web, que así podedes ver as burradas que fixen (a ver se axuda á vosa autoestima) e como as vou arranxando.

A ilustración desta Folla é unha adaptación dunha serie de cartaces sobre seguridade no traballo durante a Segunda Guerra Mundial.