cat swift/traccia-library-authoring-versioning-docc.md

Lezione 69 60 min

Traccia library authoring: API, versioning e DocC

Progettare una libreria Swift pubblicabile: surface API, access control, SemVer, package layout, compatibilità e documentazione DocC.

Scrivere una libreria è diverso da scrivere codice per te stesso.

Una libreria ha utenti che non sono nella tua testa. Ogni tipo pubblico diventa una promessa.

Surface API

La surface API è tutto ciò che esponi pubblicamente:

  • tipi public;
  • initializer public;
  • metodi public;
  • protocolli;
  • enum e casi;
  • errori;
  • conformità;
  • documentazione.

Esempio:

public struct Money: Equatable, Hashable, Sendable {
  public let cents: Int
  public let currency: Currency

  public init(cents: Int, currency: Currency) throws {
    guard cents >= 0 else {
      throw MoneyError.negativeAmount
    }

    self.cents = cents
    self.currency = currency
  }
}

Se rendi pubblico cents, stai promettendo che quel dettaglio resterà comprensibile è utile.

Public solo quando serve

Access control è design.

public struct ProductClient {
  private let repository: ProductRepository

  public init(configuration: ProductClientConfiguration) {
    self.repository = HTTPProductRepository(configuration: configuration)
  }
}

Non tutto deve essere pubblico.

Regola pratica:

  • public per ciò che l’utente deve usare;
  • internal per implementazione del package;
  • private per dettagli locali;
  • open solo se vuoi permettere subclassing fuori modulo.

Initializer stabili

Un initializer pubblico è un contratto.

Fragile:

public init(baseURL: URL, timeout: TimeInterval, retryCount: Int, decoder: JSONDecoder)

Più evolvibile:

public struct ProductClientConfiguration: Sendable {
  public var baseURL: URL
  public var timeout: Duration
  public var retryPolicy: RetryPolicy

  public init(
    baseURL: URL,
    timeout: Duration = .seconds(15),
    retryPolicy: RetryPolicy = .default
  ) {
    self.baseURL = baseURL
    self.timeout = timeout
    self.retryPolicy = retryPolicy
  }
}

Una configuration struct permette di aggiungere opzioni con meno rotture.

SemVer

Semantic Versioning:

MAJOR.MINOR.PATCH

1.2.3

In pratica:

  • PATCH: bug fix compatibile;
  • MINOR: nuove API compatibili;
  • MAJOR: breaking change.

Esempi di breaking change:

  • rinominare un metodo pubblico;
  • rimuovere un enum case pubblico;
  • cambiare tipo di ritorno;
  • rendere throwing un metodo che prima non lo era;
  • cambiare comportamento documentato;
  • restringere un requisito di protocollo.

Package layout

Sources/
  TaskCore/
    TaskItem.swift
    TaskDraft.swift
    TaskError.swift
  TaskClient/
    TaskClient.swift
    TaskClientConfiguration.swift
    HTTPTaskRepository.swift

Tests/
  TaskCoreTests/
  TaskClientTests/

Sources/TaskClient/TaskClient.docc/
  TaskClient.md
  Tutorials/

Separare core e client può aiutare utenti che vogliono solo il dominio.

Package manifest

let package = Package(
  name: "TaskKit",
  platforms: [
    .iOS(.v17),
    .macOS(.v14)
  ],
  products: [
    .library(name: "TaskCore", targets: ["TaskCore"]),
    .library(name: "TaskClient", targets: ["TaskClient"])
  ],
  targets: [
    .target(name: "TaskCore"),
    .target(
      name: "TaskClient",
      dependencies: ["TaskCore"]
    ),
    .testTarget(
      name: "TaskCoreTests",
      dependencies: ["TaskCore"]
    ),
    .testTarget(
      name: "TaskClientTests",
      dependencies: ["TaskClient"]
    )
  ]
)

Le piattaforme minime sono parte della promessa.

API stability

Prima di pubblicare chiedi:

  • posso cambiare questo nome senza rompere utenti?
  • questo tipo deve davvero essere public?
  • questo enum dovrà crescere?
  • questo protocollo è troppo grande?
  • gli errori sono abbastanza espressivi?
  • sto esponendo DTO interni?
  • sto vincolando gli utenti a una implementazione?

Una libreria buona dice poco, ma bene.

Doc comments

/// Creates a task from validated user input.
///
/// Use `TaskDraft` when accepting data from UI, HTTP requests or CLI arguments.
/// The initializer trims whitespace and rejects empty titles.
public struct TaskDraft: Equatable, Sendable {
  ...
}

La documentazione deve spiegare intenzione, contratti e casi limite, non ripetere il nome.

DocC

DocC permette di costruire documentazione da commenti, articoli e tutorial.

Articolo introduttivo:

# TaskKit

Model tasks, validate input and build clients around a shared Swift domain.

## Overview

Use ``TaskDraft`` to validate input before creating a ``TaskItem``.

DocC è particolarmente utile per:

  • overview;
  • tutorial;
  • esempi progressivi;
  • collegamenti tra simboli;
  • documentazione pubblicabile.

Deprecation

Non rimuovere subito se puoi deprecare.

@available(*, deprecated, renamed: "execute(id:)")
public func load(id: TaskItem.ID) async throws -> TaskItem {
  try await execute(id: id)
}

Questo accompagna gli utenti nella migrazione.

Checklist

Una libreria Swift seria:

  • ha una surface API piccola;
  • usa access control con intenzione;
  • rispetta SemVer;
  • ha test sui contratti pubblici;
  • evita DTO pubblici inutili;
  • documenta errori e comportamento;
  • offre configuration evolvibili;
  • usa DocC per overview e tutorial;
  • depreca prima di rompere quando possibile.

Esercizio

Trasforma TaskCore in una libreria pubblicabile:

  1. scegli quali tipi sono public;
  2. rendi interni i dettagli;
  3. aggiungi una configuration struct se serve;
  4. scrivi commenti DocC per tre simboli;
  5. aggiungi un articolo TaskKit.md;
  6. definisci una policy SemVer per le prossime modifiche.

Obiettivo: un altro sviluppatore deve capire cosa usare senza leggere tutta l’implementazione.