Perché trattare il tuo ramo come una sandbox e giocare come un lavoro legittimo produce software migliore con meno stress.
Hot take: La maggior parte degli sviluppatori stanno cercando di essere perfetti in ogni fase dello sviluppo, e sta uccidendo sia la loro creatività che la loro produttività. Ecco un modo migliore.
Ho costruito software professionalmente per... beh, diciamo solo che ricordo quando "Agile" era una nuova idea e non una parola d'ordine aziendale armata dai project manager per giustificare più standup.
Nel corso degli anni, ho notato qualcosa: il miglior codice che abbia mai scritto proveniva da progetti in cui sentivo permesso di giocare. Il peggior codice proveniva da progetti in cui ogni battuta di tasti sembrava essere esaminata, in cui stavo cercando di scrivere codice "pronto alla produzione" dal primo minuto.
Non e' un caso. Creatività e pressione non si mescolano bene.
Così ho sviluppato un approccio che separa l'esplorazione disordinata e creativa dalla consegna levigata e pronta alla produzione. Lo chiamo "Fallo funzionare, fallo bello, bloccalo." - ma in realtà, si tratta di darti il permesso di sperimentare senza il peso della perfezione che incombe su di te.
Voglio essere chiaro: non si tratta di essere sciatti o evitare la disciplina. Si tratta di applicare la disciplina giusta al momento giusto.
graph LR
subgraph "Phase 1: Make It Work"
A[New Requirement] --> B[Explore & Experiment]
B --> C{Does It Work?}
C -->|No| D[Try Different Approach]
D --> B
C -->|Yes| E[Basic Solution]
end
subgraph "Phase 2: Make It Pretty"
E --> F[Refactor & Clean]
F --> G[Apply SOLID Pragmatically]
G --> H[Get Stakeholder Feedback]
H --> I[Polished Solution]
end
subgraph "Phase 3: Lock It Down"
I --> J[Add Tests]
J --> K[Security Review]
K --> L[Add Monitoring]
L --> M[Production Ready]
end
style A stroke:#f59e0b,stroke-width:3px
style E stroke:#0ea5e9,stroke-width:3px
style I stroke:#8b5cf6,stroke-width:3px
style M stroke:#10b981,stroke-width:4px
Il mantra: Prima suona, conferma le tue supposizioni, fai scappare qualcosa.
Questa è la fase in cui il tuo ramo è una sandbox. Il codice Messy va bene. Le estremità morte vanno bene. Iniziare più di tre volte è beneNon stai costruendo una cattedrale qui, stai disegnando su un tovagliolo.
Quello che stai cercando di capire:
L'intuizione critica: Finche' non sollevi un PR, il tuo ramo e' tuo. E 'il tuo laboratorio sperimentale. Nessuno ha bisogno di vedere il tuo falso inizia, il tuo codice di debug commentato, il tuo "TODO: questo è terribile, risolvere più tardi" commenti. Ma check-in OFTEN / ogni volta che si dispone di qualcosa che funziona, commettere e spingere. Aggancia nella tua pipeline CI; una buona pipeline vi darà più informazioni (test di integrazione, rompere i cambiamenti altrove, ecc.). E 'bene controllare brutto, appena cucito insieme franken-code. Questo è il punto. Anche come un veterano a tre decadi, lo faccio ancora. Mi libera la mente.
Vantaggio collaterale: I commit frequenti sono un battito cardiaco. Nei team remoti/async, una scia costante di "brutti ma progressi" si impegna rassicurando tutti che il lavoro si sta muovendo senza dover eseguire la produttività in Slack.
Questa libertà psicologica è essenziale. Quando sai che puoi buttare via tutto e ricominciare da zero, prendi decisioni più audaci. Prova quello strano approccio che probabilmente non funzionerà. A volte funziona brillantemente. A volte hai bisogno di leggere un articolo, guardare un video di YouTube, pensare per un po '. Ricorda: il tuo lavoro è quello di fornire un buon software e buone decisioni, non digitare veloce. NON UN TIPISTA.
// Phase 1 code looks like this - and THAT'S OKAY
public async Task<Result> ProcessThing(Request req)
{
// TODO: what if this is null?
var data = await _api.GetSomething(req.Id);
// This probably isn't right but let's see what happens
var transformed = data.Items
.Where(x => x.Status != "deleted") // Is this the right filter?
.Select(x => new Thing { Name = x.Name }); // Missing loads of fields
// HACK: hardcoded for now
return new Result { Items = transformed.ToList(), Total = 42 };
}
È questo codice di produzione? Dio no. È utile? Assolutamente. Ti dice se il vostro approccio è anche fattibile prima di investire ore lucidando qualcosa che non funzionerà.
Una nota sul feedback: È possibile ottenere feedback a qualsiasi punto nella fase 1 [49] la chiave è la scelta chi lo dà. L'obiettivo è quello di costruire la migliore caratteristica, non di seguire un processo rigido.
Costruire un'API che un altro team consumerà? Farli coinvolgere presto. Una rapida "fa questa forma ha senso?" conversazione può salvare giorni di lavoro. Costruire qualcosa di rivolto all'utente? Forse tenere fuori mostrando il business stakeholder fino a quando è meno ruvida gente viene appeso su come qualcosa sguardi piuttosto che se lavori. Va bene; danno feedback in fase 2.
Usa il tuo giudizio. A volte hai bisogno di feedback per chiarire, non approvare la specifica già fatto. "La specifica dice 'gestire graziosamente gli errori' che significa riprovare tre volte, o fallire velocemente e notificare?" Quella è una conversazione di fase 1.
Se l'input di un stakeholder è critico, ma avrebbe lottato con il codice approssimativo, costruire una demo banale. Un hardcoded happy-percorso che mostra il concetto può sbloccare feedback preziosi senza la distrazione di casi di bordo mancanti.
Siate pragmatici. L'obiettivo è la migliore caratteristica, non la purezza del processo.
Il mantra: Ora che sai quello che stai costruendo, fallo buono.
La fase esplorativa ha fatto il suo lavoro. Hai confermato che l'approccio funziona, capisci la forma del problema, e probabilmente hai scoperto una dozzina di casi di bordo il biglietto originale non ha menzionato.
Ora lo ripulisci:
// Phase 2: Same logic, but actually maintainable
public async Task<ProcessingResult> ProcessItemsAsync(
ProcessingRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var items = await _itemRepository.GetActiveItemsAsync(
request.AccountId,
cancellationToken);
var processedItems = items
.Where(item => item.Status != ItemStatus.Deleted)
.Select(item => _mapper.ToProcessedItem(item))
.ToList();
return new ProcessingResult
{
Items = processedItems,
TotalCount = processedItems.Count,
ProcessedAt = _timeProvider.UtcNow
};
}
Qui e' dove tu:
Criticamente, questo è il momento migliore per il feedback degli stakeholder non tecnici. La funzione è ora visibile e utilizzabile, ma non hai ancora investito ore di test di scrittura per esso. Se dicono "in realtà, volevamo che fare X invece," è possibile perno senza la rottura di cuore di eliminare una suite di test completa.
Il mantra: L'harden, l'ha testato, l'ha reso resiliente.
Ora e solo ora Scrivi i tuoi test. Aggiungi controlli di sicurezza. Implementa la corretta gestione degli errori per i casi bordo produzione. Aggiungi monitoraggio e guardrails.
Perche' aspettare fino ad ora? Finalmente sai cosa stai testando..
Le prove scritte in fase 1 sarebbero state sbagliate, non hai ancora capito il problema. Le prove scritte in fase 2 avrebbero dovuto rifinire l'API. Le prove scritte in fase 3 sono corretto, perché l'attuazione è stabile.
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(100)]
public async Task ProcessItemsAsync_WithValidRequest_ReturnsCorrectCount(
int itemCount)
{
// Arrange
var items = _fixture.CreateMany<Item>(itemCount).ToList();
_mockRepository
.Setup(r => r.GetActiveItemsAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(items);
// Act
var result = await _sut.ProcessItemsAsync(
new ProcessingRequest { AccountId = Guid.NewGuid() });
// Assert
result.TotalCount.Should().Be(itemCount);
result.Items.Should().HaveCount(itemCount);
}
[Fact]
public async Task ProcessItemsAsync_WithNullRequest_ThrowsArgumentNullException()
{
// Act & Assert
await _sut.Invoking(s => s.ProcessItemsAsync(null!))
.Should().ThrowAsync<ArgumentNullException>();
}
[Fact]
public async Task ProcessItemsAsync_ExcludesDeletedItems()
{
// Arrange
var items = new[]
{
new Item { Status = ItemStatus.Active },
new Item { Status = ItemStatus.Deleted },
new Item { Status = ItemStatus.Active }
};
_mockRepository
.Setup(r => r.GetActiveItemsAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(items);
// Act
var result = await _sut.ProcessItemsAsync(
new ProcessingRequest { AccountId = Guid.NewGuid() });
// Assert
result.Items.Should().HaveCount(2);
}
Questa fase comprende anche:
Persone diverse dovrebbero essere coinvolte in diverse fasi, sbagliate e affogherete nell'attrito.
graph TB
subgraph "Phase 1: Technical Voices"
P1[Make It Work] --> T1[Senior Dev Review]
T1 --> T2["Does the approach<br/>make sense?"]
T2 --> T3["Any obvious<br/>architectural issues?"]
end
subgraph "Phase 2: Non-Technical Voices"
P2[Make It Pretty] --> N1[Product Owner Demo]
N1 --> N2["Does it meet<br/>the requirement?"]
N2 --> N3["Is the UX<br/>intuitive?"]
end
subgraph "Phase 3: Ops/Security Voices"
P3[Lock It Down] --> O1[Security Review]
O1 --> O2[Operations Review]
O2 --> O3["Will it hold up<br/>in production?"]
end
T3 --> P2
N3 --> P3
style T1 stroke:#3b82f6,stroke-width:2px
style N1 stroke:#8b5cf6,stroke-width:2px
style O1 stroke:#ef4444,stroke-width:2px
Fase 1: Voci tecniche solo gli sviluppatori, architetti. Possono vedere oltre il pasticcio alla forma della soluzione. Stakeholder non tecnici si farà prendere dal panico a codice approssimativo e chiedere circa i colori dei bottoni quando si sta ancora scoprendo il modello di dati.
Fase 2: Voci non tecniche benvenuti. La funzione è utilizzabile, pulsanti fare cose. Ottenere proprietari di prodotti e designer coinvolti ora i cambiamenti sono ancora a buon mercato, non hai ancora scritto test.
Fase 3: Ops e voci di sicurezza. "E se questo ottiene 10x traffico?" "E se qualcuno passa ingresso dannoso?" Queste preoccupazioni importa solo una volta che la funzione funziona effettivamente.
Ecco cosa ha cambiato la mia vita di sviluppo: fino a quando non sollevi un PR, il tuo ramo è completamente privato.
Potrebbe sembrare ovvio, ma vedo gli sviluppatori trattare ogni commit come se stesse andando sul loro record permanente. Hanno paura di sperimentare. Paura di scrivere codice brutto. Paura di rompere le cose.
Voglio che pensiate al vostro ramo come una sandbox, un parco giochi, un laboratorio dove ci si aspettano esplosioni e nessuno si fara' male.
graph TD
A[Feature Branch Created] --> B[Experiment Freely]
B --> C{Did it work?}
C -->|No| D[Try Something Else]
D --> B
C -->|Yes| E[Clean Up the Mess]
E --> F[Interactive Rebase]
F --> G[Squash to Clean Commits]
G --> H[Raise PR]
H --> I[Public Code Review]
style B stroke:#f59e0b,stroke-width:3px
style D stroke:#f59e0b,stroke-width:2px
style H stroke:#10b981,stroke-width:3px
subgraph "Private - Go Mental"
B
C
D
E
end
subgraph "Public - Be Professional"
H
I
end
In pratica, ciò significa:
FIXME e TODO commenti ovunquePoi, prima di sollevare un PR:
I recensori delle pubbliche relazioni vedono un codice pulito e professionale con una narrazione chiara. Non vedono i sei falsi inizi, le 3 del mattino "perché questo maledetto lavoro" commette, o la storia "undo annullare Annulla."
La sandbox rimane privata. Il lavoro lucido diventa pubblico.
Lo sviluppo tradizionale "sii professionale in ogni momento" è schiacciante. Ogni linea si sente permanente. Ogni decisione si sente consequenziale. Questo approccio rimuove questa pressione contenendo il caos: il gioco è lavoro legittimo Nella Fase 1, il feedback arriva quando la rotazione è a buon mercato nella Fase 2, e i test sono scritti una volta correttamente nella Fase 3 perché finalmente sai cosa stai testando.
"Il gioco è un lavoro legittimo."
TDD funziona brillantemente quando il dominio è ben compreso o si sta bloccando un bug noto. Ma quando il problema è ancora confuso, scrivere test prima spesso significa testare la cosa sbagliata tre volte di fila. Questo approccio passi laterali che: esplorare prima, testare quando stabile.
Quindi cosa fa questa mentalità trifase ai tuoi principi di progettazione? In breve: uccide il dogma. Ecco come cambia il mio pensiero su SOLID, DRY e interfacce in C#.
Ho menzionato l'applicazione "praticamente" dei principi SOLID nella fase 2.
SOLID è una buona serie di principi. Li insegno. Li uso. Ma ho visto team scomparire giù astrazione fori di coniglio in nome di One Responsibility o Open/Chiuso, creando sistemi così flessibili sono incomprensibili.
Ecco la mia euristica:
Applicare SOLID quando:
Non applicare SOLID quando:
Tre linee di codice simili sono meglio di un'astrazione prematura. Puoi sempre estrarre più tardi quando vedi il modello chiaramente. Non puoi facilmente annullare un'astrazione che è cotta nella tua architettura.
// Over-engineered SOLID worship
public interface IThingProcessor { }
public interface IThingProcessorFactory { }
public class ThingProcessorFactory : IThingProcessorFactory { }
public class ThingProcessor : IThingProcessor { }
public class ThingProcessorDecorator : IThingProcessor { }
public class ThingProcessorValidationDecorator : IThingProcessor { }
// What you probably actually need
public class ThingProcessor
{
public async Task<Result> ProcessAsync(Thing thing)
{
// Just do the thing
}
}
Se avete bisogno di flessibilità in seguito, refactoring è a buon mercato. Over-engineering upfront è costoso perché si sta mantenendo livelli di astrazione che non stanno guadagnando il loro mantenimento.
E mentre stiamo uccidendo mucche sacre, parliamo di DRYCity name (optional, probably does not need a translation) Forse è il principio più dogmaticamente applicato nel nostro settore, e se applicato senza pensiero, causa più danno che bene.
Il problema è questo: non tutta la duplicazione è lo stesso tipo di duplicazione.
Quando due pezzi di codice sembrano simili ma servono scopi diversi, estraendoli in una coppia di astrazioni condivise, le cose che dovrebbero evolversi indipendentemente. Ora, quando un caso d'uso deve cambiare, tu sei:
// "DRY" solution - looks clever, causes pain
public async Task<Result> ProcessEntity<T>(
T entity,
bool validateFirst = true,
bool sendNotification = false,
Func<T, bool>? customFilter = null,
Action<T>? preProcess = null,
Action<T>? postProcess = null) where T : IEntity
{
// 50 lines of conditional spaghetti trying to handle
// "processing" for Users, Orders, and Products
// because they all had 3 similar lines once
}
// What you probably actually need
public async Task<Result> ProcessUser(User user) { /* 15 clear lines */ }
public async Task<Result> ProcessOrder(Order order) { /* 15 clear lines */ }
public async Task<Result> ProcessProduct(Product product) { /* 15 clear lines */ }
Sì, c'è qualche "duplicazione" nel secondo approccio, ma ogni metodo è:
La versione DRY è un incubo da mantenere perché sta cercando di essere tutto per tutti i chiamanti. Ogni cambiamento richiede la comprensione di tutti i casi di utilizzo. Ogni bug fix rischia di rompere qualcos'altro.
La mia regola: Tollerare la duplicazione fino a quando non hai visto il stesso cosa tre volte e sei sicuro che rappresenta il stesso Fino ad allora, un po' di copia-incolla va bene. Non è un fallimento morale; è un'adeguata cautela.
La duplicazione è molto più economica dell'astrazione sbagliata. Puoi sempre deduplicare più tardi quando il modello è chiaro. Non puoi facilmente annullare una cattiva astrazione che è intrecciata attraverso la tua base di codice.
Ecco un'altra mucca sacra che ha bisogno di macellare: il modello "ogni classe ha bisogno di un'interfaccia".
Nei primi giorni dell'iniezione di dipendenza .NET, qualcuno ha deciso che per rendere il codice testabile, era necessario iniettare interfacce ovunque. La logica era: non si può deridere una classe concreta, quindi ogni servizio ha bisogno IService e ServiceImpl. Improvvisamente ogni base di codice C# era disseminata di interfacce mono-implementazione che esistevano puramente per i test.
// The interface tax - early 2010s style
public interface IUserService { }
public interface IOrderService { }
public interface IEmailService { }
public interface INotificationService { }
public interface IPaymentProcessor { }
public interface IInventoryManager { }
// Each with exactly ONE implementation
public class UserService : IUserService { }
public class OrderService : IOrderService { }
// ... you get the idea
Questa era la programmazione della setta cargo, stavamo pagando una tassa sulla complessità per teorico provabilità e teorico la flessibilità futura che raramente si è concretizzata.
Il fatto e' che non ne hai piu' bisogno.
Moderni quadri di derisione come NSubstitute e MoqCity name (optional, probably does not need a translation) Inoltre, il contenitore DI di ASP.NET Core funziona perfettamente con i tipi di calcestruzzo:
// Modern approach - just register the concrete type
services.AddScoped<UserService>();
services.AddScoped<OrderService>();
services.AddScoped<EmailService>();
// In your controller or service
public class OrderController(OrderService orderService, UserService userService)
{
// Primary constructor injection - clean and simple
}
Nessuna interfaccia. IOrderServiceSolo il servizio di cui hai bisogno, iniettato direttamente.
"Ma che mi dici dei test?" Ti sento piangere.
Per i test unitari, avete opzioni:
virtual e utilizzare un quadro di giocoIl terzo punto è fondamentale: Gli strumenti di refactoring sono straordinariamente bravi nell'estrazione delle interfacce.
In Rider o Visual Studio, è letteralmente Ctrl+. → "Extract Interface" e hai finito. Ogni metodo viene estratto, la classe viene aggiornata per implementarlo, e puoi trovare/sostituire tutti gli usi se necessario. Ci vogliono secondi.
// Phase 1 & 2: Just write the class
public class OrderService
{
public async Task<Order> CreateOrderAsync(CreateOrderRequest request) { ... }
public async Task<Order?> GetOrderAsync(int orderId) { ... }
public async Task CancelOrderAsync(int orderId) { ... }
}
// Phase 3: Need to mock it for testing? Extract interface in 2 seconds
public interface IOrderService
{
Task<Order> CreateOrderAsync(CreateOrderRequest request);
Task<Order?> GetOrderAsync(int orderId);
Task CancelOrderAsync(int orderId);
}
public class OrderService : IOrderService { ... }
Il mio flusso di lavoro ora: interfacce sono disponibili in fase 3, non fase 1.
Durante "fatelo funzionare" e "fatelo bello," scrivo solo lezioni. Nessuna astrazione prematura. Nessuna tassa di interfaccia su ogni tipo. Il codice è più semplice, più facile da navigare (nessun salto tra IFoo e Foo), e più veloce da scrivere.
Quando premero' "bloccalo" e dovro' scrivere dei test, poi Io decido quali servizi effettivamente bisogno di deridere. Di solito è meno di quanto si potrebbe pensare solo integrazioni esterne come client HTTP, database, e code di messaggi.
Per i servizi di logica aziendale interna? Metà del tempo li testo direttamente con dipendenze reali. I test sono comunque più preziosi perché testano il comportamento reale, non un gioco di quello che ho pensa il comportamento dovrebbe essere.
// Services I typically DO extract interfaces for (external boundaries)
public interface IPaymentGateway { } // Third-party API
public interface IEmailSender { } // External service
public interface IBlobStorage { } // Cloud storage
// Services I typically DON'T need interfaces for (internal logic)
public class OrderValidator { } // Pure logic, test directly
public class PriceCalculator { } // Pure logic, test directly
public class OrderService { } // Test with real repo + in-memory DB
Il punto è: Scrivi le classi di cemento. Se hai bisogno di un'interfaccia per testare o un vero polimorfismo in seguito, estraendone uno prende letteralmente secondi con strumenti moderni.
La vostra base di codice sarà più semplice, più navigabile, e non avrete l'onere di mantenere le interfacce in sincronia con le implementazioni senza alcun beneficio.
Ironicamente, questo approccio è più allineato con il originale Manifesto Agile rispetto alla maggior parte dei processi "Agile" che ho incontrato. Software di lavoro veloce (fase 1). Rispondere al cambiamento (fase 1-2). Collaborazione del cliente quando la funzione è dimostrabile (fase 2). Nessun punto di sprint, nessun monitoraggio della velocità, nessun grafico di bruciatura sanguinoso.
Moderno "Agile" è stato catturato da appassionati di processo che hanno aggiunto così tante cerimonie non c'è più tempo per l'esplorazione. Questo approccio porta il gioco indietro non come indulgenza, ma come una fase legittima di lavoro creativo.
Dovrei essere onesto: questo non è sempre l'approccio giusto.
Correzioni bug: Quando si sta riparando un bug noto, lo stile TDD ha più senso. Scrivere un test che riproduce il bug, risolverlo, fatto.
Problemi ben compresi: Se stai implementando un algoritmo standard o uno schema che hai usato centinaia di volte, non hai bisogno di una fase di esplorazione.
Termini stretti con requisiti noti: Se davvero sai esattamente cosa stai costruendo e quando ha bisogno di spedire, potresti saltare l'esplorazione di Fase 1.
Ambienti altamente regolamentati: Se ogni commit ha bisogno di un segnale, sandboxing è più difficile. Anche se si può ancora fare l'esplorazione a livello locale prima di spingere.
Ma per maggior parte sviluppo di funzionalità in cui i requisiti sono un po 'fuzzy, l'approccio tecnico non è ovvio, e avete bisogno di spazio per pensare questo approccio funziona brillantemente.
Se siete all'inizio della vostra carriera, potreste sentire la pressione per mostrare il codice "perfetto" tutto il tempo. Non dovete. Usi questo approccio di trifase appena sia chiaro circa che fase siete dentro e finisca sempre il lavoro fino alla fase 3. Nessuno vi farà difetto per codice di esplorazione disordinato se conduce al codice lucido, testato, production-ready alla fine.
Fallo funzionare, fallo bello, bloccalo.
Iniziare semplice. Progettare con lungimiranza ma non speculazione. Iterare rapidamente. Aggiungere solo la complessità quando si ripaga.
"La duplicae'ione e' molto piu' economica dell'astrazione sbagliata."
La fase di esplorazione non è un segreto colpevole è una parte definita del processo. Il tuo ramo è la tua sandbox fino a quando non si alza un PR. Il feedback tecnico arriva presto, il feedback non tecnico arriva al centro, le operazioni e il feedback di sicurezza arriva in ritardo.
Questo mantiene viva la creatività, riduce lo stress e produce sistemi che scalano elegantemente perché avete capito il problema prima di impegnarsi per la soluzione.
Smettila di cercare di essere perfetto dal primo minuto.
Se questo risuona, potresti anche godere di questi post correlati:
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.