design de apis

workshop bitcake 2022

lang:pt-BR

objetivo

refletir ativamente a respeito das decisões que tomamos ao arquitetar nossas soluções

*api* é apresentar *abstrações* através de uma *interface*

por que interfaces?

abstrações

é um modelo com o propósito de interpretar uma realidade rica em detalhes

alcançado por introduzir conceitos pertinentes ao domínio

abstrair = faca de dois legumes

esconder complexidade *adiciona* complexidade

abstrair != remover complexidade

esconder significa que ainda está lá

problemas complexos são complexos

a única maneira de simplificar um problema inerenentemente complexo é resolvendo outro problema (que seja mais simples)

escrever mais código que o necessário *por definição* apenas aumenta a complexidade da solução

complexidade inerente vs complexidade acidental

inerente

acidental

como saber qual complexidade estamos introduzindo?

erros e limitações serão encontrados independente da quantidade de design investido a priori. aceite-os e planeje-se!

exemplo

sistema de diálogo neko

dialog-before.png

começou super flexível porém laborioso

exemplo

sistema de diálogo neko

dialog-after.png

reconhecendo o caso comum, otimizamos o processo

exemplo

sistema de diálogo neko

dialog-implementation.png

inclusive, a forma simples é implementada por cima da flexível

código reutilizável vs código fácil de deletar

a única forma de criar código reutilizável é extraindo-o de código fácil de deletar

exemplo

bitstrap (umake)

umake.png

umake v2 vs v3

apesar de tudo, abstrações vazam

sem exceção

abstrações apenas têm valor quando levantam seu próprio peso

"programmers know the benefits of everything and the tradeoffs of nothing"

apis e abstrações *não* existem no vácuo

uma api é boa se ela é *profunda* e possui *interface enxuta*

granularidade

abstrações acontecem em vários níveis, mas o potencial é proporcional à escala

profunda

interface enxuta

profunda

ortogonal

orthogonal.png

conceitos são independentes e não se influenciam

profunda

ortogonal

altura do pulo ❌

jump-config.png gravity-config.png stick-config.png

profunda

composível

quão fácil é a interação com outras abstrações?

(mais prático quando há um canal uniforme que integra sistemas)

profunda

composível

narrow waist

exemplo: unix pipeline

ls -l | grep key | less

composível

callbacks

preferir api imediata

callbacks adicionam indireção ao fluxo de código

composível

padrão liga/desliga

// evitar
void Enable(); // this.enabled = true; ...
void Disable(); // this.enabled = false; ...
// eventualmente força um `if` em alguma camada

// preferir
void SetEnabled(bool enabled); // this.enabled = enabled; ...
// porém nem sempre possível!

profunda

composível

late binding x early binding

no início, queremos simplicidade

no final, queremos controle total

profunda

exemplo

late binding dialogo neko

class UNekoDialogueCharacterDataAsset : public UDataAsset {
    ENekoDialogueCharacterId Id = ENekoDialogueCharacterId::None;

    TArray<TSoftObjectPtr<UTexture2D>> Expressions;

    // implementado com for
    UTexture2D* GetExpression(ENekoDialogueCharacterExpression Expression);
};

profunda

exemplo

early binding dialogo neko

class UNekoDialogueCharacterDataAsset : public UDataAsset {
    ENekoDialogueCharacterId Id = ENekoDialogueCharacterId::None;

    TSoftObjectPtr<UTexture2D> ExpressionDefault;
    TSoftObjectPtr<UTexture2D> ExpressionAngry;
    TSoftObjectPtr<UTexture2D> ExpressionHappy;

    // implementado com switch
    UTexture2D* GetExpression(ENekoDialogueCharacterExpression Expression);
};

profunda

complexidade adicionada < complexidade abstraída

abstrações devem puxar mais que seu próprio peso

entender os tradeoffs feitos

profunda

complexidade adicionada < complexidade abstraída

desempenho

interface enxuta

padrões razoáveis

almejar criar um "pit of success" pra quem for usar

interface enxuta

padrões razoáveis

"data driven"

extrair dados estáticos em arquivos separados

interface enxuta

padrões razoáveis

estruturas de dados

arrays muitoprovavelmente são suficientes

processadores adoram memória linear

*olha feio praquela lista encadeada* 😠

interface enxuta

estado mínimo

sobre quais os dados a api opera?

onde eles são alterados?

interface enxuta

estado mínimo

const correctness

preferir estilo funcional sempre que possível

facilita compreensão. menos dependente de ordem de execução

// evitar
obj.CheckSomething();
obj.MaybeDoSomethingInResponse();

// preferir
var result = obj.CheckSomething();
if (result) {
    obj.DoSomethingInResponse(result);
}

interface enxuta

estado mínimo

recursos e posse

quem é o responsável pelo recurso?

posse vs empréstimo

onde há alocação de memória? possível evitá-la?

interface enxuta

invariantes explícitas

+ pré-condições

+ pós-condições

interface enxuta

invariantes explícitas

null

pré-condição: quais funções podem recebernull?

pós-condição: quais funções podem retornarnull?

decidir e ser explícito!

linguagens mais recentes têm esse conceito embutido (rust, zig, kotlin, c#8)

interface enxuta

invariantes explícitas

minimizar erros

dois tipos de erros:

exemplo

pepper CommandTokenizer

open "folder/some file.txt
 ops, esqueci de fechar aspas ^

interface enxuta

invariantes explícitas

intervalo de valores

usar tipos para codificar valores válidos

// evitar
int GetPlayerCount();

// preferir
uint8 GetPlayerCount();

interface enxuta

intui sua implementação

a partir de uma transformação conhecida, é possível inferir aproximadamente suas entradas e saídas

interface enxuta

intui sua implementação

debugabilidade

às vezes não precisa de api ou abstração

código vai, design fica

exemplo

classe AssetLoader

pub trait Asset: Sync + Send + 'static {
    type Id: fmt::Debug + Hash + Eq + Clone + Sync + Send;
}
pub trait AssetLoader<'a, A: Asset> {
    type Storage;
    fn load(&'a self, id: &A::Id, storage: &mut Self::Storage) -> Result<A, AssetLoadError>;
}
pub fn try_load<'a, S>(
    &mut self,
    id: &A::Id,
    loader: &'a AssetLoader<'a, A, Storage = S>,
    storage: &mut S,
) -> Result<AssetHandle<A>, AssetLoadError> {
    match self.cache_map.get(id).cloned() {
        Some(handle) => Ok(handle),
        None => {
            let asset = loader.load(id, storage)?;
            let handle = self.add(asset);
            self.cache_map.insert(id.clone(), handle);
            Ok(handle)
        }
    }
}

exemplo

classe timer v1

public class Timer { // holodrive
    [SerializeField] private float length = 1.0f; // serializado
    private float counter = 0.0f;
    public System.Action OnTimer { get; set; } // callback

    public void OnUpdate() {
        if( counter < 0.0f ) {
            // Already triggered callback.
        } else if( counter < length ) {
            counter += Time.deltaTime;
        } else {
            counter = -1.0f;
            if( OnTimer != null )
                OnTimer();
        }
    }
}

exemplo

classe timer v2

public sealed class Timer { // demagnete
    public float length = 1.0f; // dinamico
    private float elapsedTime = -1.0f;

    public bool OnUpdate() { // sem callback
        if( elapsedTime >= 0.0f )
            elapsedTime += Time.deltaTime;
        if( elapsedTime < length ) {
            elapsedTime = -1.0f;
            return true;
        }
        return false;
    }
}

exemplo

classe timer v3

float Timer = -1.0f; // neko
// ...
void AMyActor::Tick(float DeltaSeconds) {
    if (Timer >= 0.0f) {
        Timer += DeltaSeconds;
        if (Timer >= Config->TimerDuration) {
            // do the thing!
            Timer = -1.0f;
        }
    }
}

3 big lies

design de api = ponderar custo-benefícios

"everything should be made as simple as possible, but not simpler."

!links