design de apis
workshop bitcake 2022
lang:pt-BR
lang:pt-BR
a única maneira de simplificar um problema inerenentemente complexo é resolvendo outro problema (que seja mais simples)
erros e limitações serão encontrados independente da quantidade de design investido a priori. aceite-os e planeje-se!
exemplo
começou super flexível porém laborioso
exemplo
reconhecendo o caso comum, otimizamos o processo
exemplo
inclusive, a forma simples é implementada por cima da flexível
exemplo
umake v2 vs v3
profunda
conceitos são independentes e não se influenciam
profunda
ortogonal
profunda
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
exemplo: unix pipeline
ls -l | grep key | less
composível
preferir api imediata
callbacks adicionam indireção ao fluxo de código
composível
// 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
no início, queremos simplicidade
no final, queremos controle total
profunda
exemplo
class UNekoDialogueCharacterDataAsset : public UDataAsset {
ENekoDialogueCharacterId Id = ENekoDialogueCharacterId::None;
TArray<TSoftObjectPtr<UTexture2D>> Expressions;
// implementado com for
UTexture2D* GetExpression(ENekoDialogueCharacterExpression Expression);
};
profunda
exemplo
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
abstrações devem puxar mais que seu próprio peso
entender os tradeoffs feitos
profunda
complexidade adicionada < complexidade abstraída
interface enxuta
almejar criar um "pit of success" pra quem for usar
interface enxuta
padrões razoáveis
extrair dados estáticos em arquivos separados
interface enxuta
padrões razoáveis
arrays muitoprovavelmente são suficientes
processadores adoram memória linear
*olha feio praquela lista encadeada* 😠
interface enxuta
sobre quais os dados a api opera?
onde eles são alterados?
interface enxuta
estado mínimo
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
quem é o responsável pelo recurso?
posse vs empréstimo
onde há alocação de memória? possível evitá-la?
interface enxuta
+ pré-condições
+ pós-condições
interface enxuta
invariantes explícitas
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)
Option<T>
interface enxuta
invariantes explícitas
dois tipos de erros:
exemplo
open "folder/some file.txt
ops, esqueci de fechar aspas ^
interface enxuta
invariantes explícitas
usar tipos para codificar valores válidos
// evitar
int GetPlayerCount();
// preferir
uint8 GetPlayerCount();
interface enxuta
a partir de uma transformação conhecida, é possível inferir aproximadamente suas entradas e saídas
interface enxuta
intui sua implementação
exemplo
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
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
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
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;
}
}
}
"everything should be made as simple as possible, but not simpler."