Waarom het behandelen van uw tak als een zandbak... en spelen als legitiem werk... leidt tot betere software met minder stress.
Warme opname: De meeste ontwikkelaars proberen perfect te zijn in elke fase van ontwikkeling, en het doodt zowel hun creativiteit als hun productiviteit. Hier is een betere manier.
Ik heb software professioneel gebouwd voor... Nou, laten we zeggen dat ik me herinner toen "Agile" een nieuw idee was en niet een bedrijfsbuzzword die door projectmanagers werd bewapend om meer stand-ups te rechtvaardigen.
In de loop der jaren heb ik iets gemerkt: de beste code die ik ooit geschreven heb kwam uit projecten waar ik me voelde toestemming om af te spelen. De slechtste code kwam van projecten waar elke toetsaanslag voelde alsof het werd gecontroleerd, waar ik probeerde te schrijven "productie-klaar" code vanaf minuut één.
Dit is niet toevallig. Creativiteit en druk mengen zich niet goed.
Dus ik heb een aanpak ontwikkeld die de rommelige, creatieve exploratie scheidt van de gepolijste, productie-ready levering. Ik noem het "Laat het werken, maak het mooi, sluit het af." - maar eigenlijk gaat het erom dat je jezelf toestemming geeft om te experimenteren zonder het gewicht van perfectie boven je te hangen.
Laat me eerlijk zijn: dit gaat niet over slordig zijn of discipline vermijden. toepassing van de juiste discipline op het juiste moment.
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
De mantra: Speel eerst, bevestig je veronderstellingen.
Dit is de fase waarin uw tak is een zandbak. Messy code is prima. Dode uiteinden zijn prima. Beginnend over drie keer is boeteJe bouwt hier geen kathedraal, je schetst op een servet.
Wat je probeert uit te zoeken:
Het kritische inzicht: Totdat je een PR verhoogt, is je branch De jouwe.. Het is uw experimentele laboratorium. Niemand hoeft te zien uw valse starts, uw commentaar-out debugging code, uw "TODO: dit is verschrikkelijk, fix later" opmerkingen. Maar check in OFTEN alle keer dat je iets dat werkt, commit en push. Haak in uw CI-pijpleiding; een goede pijplijn zal u meer informatie (integratie testen, breken veranderingen elders, enz.). Het is fijn om in te checken lelijk, nauwelijks aan elkaar gestikt franken-code. Dat is het punt. Zelfs als een drie-decade veteraan, Ik doe dit nog steeds. Het bevrijdt mijn geest.
Bijwerkingen: Frequent commits zijn een hartslag. In remote/async teams, een gestaag spoor van "lelijke maar progressieve" commits verzekert iedereen die werk beweegt en zonder dat je productiviteit in Slack uit te voeren.
Deze psychologische vrijheid is essentieel. Als je weet dat je alles kunt weggooien en opnieuw kunt beginnen, maak je moedigere beslissingen. Je probeert die rare aanpak die waarschijnlijk niet zal werken. Soms werkt het briljant. Soms moet je een artikel lezen, een YouTube video bekijken, denken voor een tijdje. Onthoud: je taak is om goede software en goede beslissingen te leveren, niet om snel te typen. GEEN TYPIST.
// 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 };
}
Is dit productiecode? God nee. Is het nuttig? Absoluut. Het vertelt u of uw aanpak is zelfs levensvatbaar voordat je investeert uren polijsten iets dat niet zal werken.
Een opmerking over feedback: U kunt feedback krijgen op alle punt in fase 1: de sleutel is kiezen wie Het doel is om de beste functie te bouwen, niet om een rigide proces te volgen.
Het bouwen van een API die een ander team zal consumeren? Laat ze vroeg betrokken. Een snelle "heeft deze vorm zinvol?" conversatie kan dagen van herwerken besparen. Bouwen van iets user-facing? Misschien wachten met het tonen van de zakelijke stakeholder totdat het minder ruw een aantal mensen worden opgehangen over hoe iets blikken in plaats van of het werken. Dat is prima; ze geven feedback in fase 2.
Gebruik je oordeel. Soms heb je feedback nodig om verduidelijking, niet om goed te keuren de spec al deed dat. "De spec zegt 'afhandeling fouten sierlijk' betekent dat opnieuw proberen drie keer, of faal snel en melden?" Dat is een fase 1 gesprek.
Als de input van een stakeholder kritiek is, maar ze zouden worstelen met ruwe code, bouw een triviale demo. Een hard gecodeerde happy-path dat toont dat het concept waardevolle feedback kan ontgrendelen zonder de afleiding van ontbrekende rand gevallen.
Wees pragmatisch. Het doel is het beste kenmerk, niet proceszuiverheid.
De mantra: Nu je weet wat je aan het bouwen bent, maak het goed.
De verkennende fase heeft zijn werk gedaan. Je hebt bevestigd dat de aanpak werkt, je begrijpt de vorm van het probleem, en je hebt waarschijnlijk ontdekt een dozijn rand gevallen de oorspronkelijke ticket niet vermeld.
Nu ruim je het op:
// 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
};
}
Dit is waar je:
Kritisch gezien is dit het beste moment voor niet-technische feedback van belanghebbenden. De functie is nu zichtbaar en bruikbaar, maar je hebt nog niet geïnvesteerd uren schrijven testen voor het. Als ze zeggen "Eigenlijk, we wilden dat het te doen X in plaats," kunt u draaien zonder de gebroken hart van het verwijderen van een uitgebreide test suite.
De mantra: Verhard het, test het, maak het veerkrachtig.
Nu en alleen nu schrijf je je tests. Voeg beveiligingscontroles. Voer de juiste foutafhandeling voor de productie edge cases. Voeg monitoring en vangrails.
Waarom wachten tot nu? Je weet eindelijk wat je aan het testen bent..
Tests geschreven in Fase 1 zou verkeerd zijn geweest.Je begreep het probleem nog niet. Tests geschreven in Fase 2 zou hebben karn.Je was nog steeds het verfijnen van de API. Tests geschreven in Fase 3 zijn correct, omdat de uitvoering stabiel is.
[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);
}
Deze fase omvat ook:
Als je het mis hebt, verdrink je in wrijving.
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: Technische stemmen alleen senior devs, architecten. Ze kunnen voorbij de puinhoop zien naar de vorm van de oplossing. Niet-technische stakeholders zullen in paniek raken bij ruwe code en vragen naar knopkleuren wanneer je nog steeds het datamodel uitzoekt.
Fase 2: Niet-technische stemmen welkom. De functie is bruikbaar, knoppen doen dingen. Krijg producteigenaren en designers betrokken nu de veranderingen zijn nog steeds goedkoop, je hebt nog niet geschreven tests.
Fase 3: Ops en security stemmen. "Wat als dit krijgt 10x verkeer?" "Wat als iemand voorbij kwaadaardige input?" Deze zorgen zijn alleen belangrijk als de functie echt werkt.
Dit is het ding dat mijn ontwikkelingsleven veranderde: totdat u een PR verhoogt, is uw vestiging volledig privé.
Dat klinkt misschien voor de hand liggend, maar ik zie ontwikkelaars elke commit behandelen alsof het op hun permanente record staat. Ze zijn bang om te experimenteren. Bang om lelijke code te schrijven. Bang om dingen te breken.
Beschouw je functie als een zandbak, een speeltuin, een laboratorium waar explosies worden verwacht en niemand gewond raakt.
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 de praktijk betekent dit:
FIXME en TODO reacties overalDan, voordat je een PR oproept:
De PR beoordelaars zien schone, professionele code met een duidelijk verhaal. Ze zien niet de zes valse starts, de 3am "waarom niet dit bloederige werk" commits, of de "undo ongedaan maken" geschiedenis.
De zandbak blijft privé, het gepolijste werk gaat openbaar.
Traditioneel "wees professioneel te allen tijde" ontwikkeling is verpletterend. Elke lijn voelt permanent. Elke beslissing voelt als gevolg. Deze aanpak verwijdert die druk door het inperken van de chaos: spelen is legitiem werk in fase 1, feedback komt wanneer draaien goedkoop is in fase 2, en tests zijn eenmaal correct geschreven in fase 3, omdat je eindelijk weet wat je test.
Spelen is legitiem werk.
TDD werkt briljant als het domein goed wordt begrepen of als je een bekende bug vastpint. Maar als het probleem nog steeds wazig is, betekent het schrijven van tests eerst vaak het testen van het verkeerde ding drie keer achter elkaar. Deze benadering sidesteps die: verken eerst, test wanneer stabiel.
Dus wat doet deze drie-fase mindset met uw ontwerp principes? Kortom: het doodt dogma. Hier is hoe het verandert mijn denken over SOLID, DRY, en interfaces in C#.
Ik heb het gehad over de toepassing van de SOLID-beginselen "pragmatisch" in fase 2.
SOLID is een goede set van principes. Ik leer ze. Ik gebruik ze. Maar ik heb gezien teams verdwijnen abstractie konijnen gaten in de naam van Single Responsibility of Open/Closed, het creëren van systemen zo flexibel ze zijn onbegrijpelijk.
Hier is mijn heuristische:
SOLID toepassen als:
SOLID niet toepassen wanneer:
Drie gelijkaardige regels code zijn beter dan een premature abstractie. Je kunt altijd later extraheren als je het patroon duidelijk ziet. Je kunt niet gemakkelijk een abstractie ongedaan maken die in je architectuur wordt gebakken.
// 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
}
}
Als je later de flexibiliteit nodig hebt, refactoring is goedkoop. Over-engineering vooraf is duur omdat je abstractie lagen die niet verdienen hun houden.
En terwijl we heilige koeien doden, laten we praten over DRY Het is misschien wel het meest dogmatisch toegepaste principe in onze industrie, en wanneer toegepast zonder nadenken, veroorzaakt het meer schade dan goed.
Het probleem is dit: Niet alle duplicatie is hetzelfde soort duplicatie.
Als twee stukjes code op elkaar lijken maar verschillende doeleinden dienen, en ze extraheren in een gedeelde abstractie paren dingen die onafhankelijk moeten evolueren. Nu wanneer een use case moet veranderen, ben je ofwel:
// "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 */ }
Ja, er is wat "duplicatie" in de tweede benadering, maar elke methode is:
De DRY versie is een nachtmerrie te handhaven omdat het probeert om alle dingen te zijn voor alle bellers. Elke verandering vereist begrip van alle use cases. Elke bug repareert risico's breken van iets anders.
Mijn regel: Duplicatie tolereren tot je het hebt gezien. hetzelfde Drie keer en je bent er zeker van dat het de hetzelfde Tot die tijd is een beetje kopieerpasta prima. Het is geen morele mislukking; het is gepaste voorzichtigheid.
Duplicatie is veel goedkoper dan de verkeerde abstractie. Je kunt altijd later dedupliceren als het patroon duidelijk is. Je kunt niet gemakkelijk een slechte abstractie ongedaan maken die door je codebase is geweven.
Hier is nog een heilige koe die geslacht moet worden: het "elke klasse heeft een interface" patroon nodig.
In de vroege dagen van .NET afhankelijkheid injectie, iemand besloten dat om code testbaar te maken, je nodig hebt om interfaces overal te injecteren. De logica was: je kunt niet spotten met een betonnen klasse, dus elke dienst moet IService en ServiceImpl. Plotseling was elke C# codebase bezaaid met enkele-implementatie interfaces die uitsluitend bestond voor het testen.
// 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
Dit was cargo cult programmering. We betaalden een complexiteit belasting voor theoretisch testbaarheid en theoretisch de toekomstige flexibiliteit die zelden tot stand kwam.
Het zit zo: je hebt dit niet meer nodig.
Moderne bespotte kaders zoals N Plaatsvervanger en Moq kan bespot beton klassen (met virtuele methoden). Belangrijker, ASP.NET Core DI container werkt perfect goed met concrete types:
// 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
}
Geen interfaces. IOrderService. Precies de dienst die u nodig hebt, rechtstreeks geïnjecteerd.
"Maar hoe zit het met testen?" Ik hoor je huilen.
Voor unit testen, heb je opties:
virtual en gebruik maken van een bespottelijk kaderDat derde punt is van cruciaal belang: refactoring tools zijn buitengewoon goed in het extraheren van interfaces.
In Rider of Visual Studio, het is letterlijk Ctrl+. → "Extract Interface" en je bent klaar. Elke methode wordt eruit gehaald, de klasse wordt bijgewerkt om het te implementeren, en je kunt vinden / vervangen alle toepassingen indien nodig. Het duurt seconden.
// 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 { ... }
Mijn workflow nu: interfaces zijn te vinden in fase 3, niet in fase 1.
Tijdens "make it work" en "make it pretty," schrijf ik gewoon klassen. Geen premature abstractie. Geen interface belasting op elk type. De code is eenvoudiger, gemakkelijker te navigeren (geen springen tussen IFoo en Foo), en sneller te schrijven.
Als ik op "lock it down" druk en tests moet schrijven, dan Ik beslis welke diensten eigenlijk moeten spotten. Meestal is het minder dan je zou denken dat het vaak gewoon externe integraties zoals HTTP-clients, databases, en bericht wachtrijen.
Voor interne bedrijfslogica diensten? De helft van de tijd test ik ze direct met echte afhankelijkheden. De tests zijn toch waardevoller omdat ze het feitelijke gedrag testen, niet een spot van wat ik Denk na. het gedrag zou moeten zijn.
// 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
Het punt is: Stop met het schrijven van interfaces vooraf "voor het geval." Schrijf beton klassen. Als u een interface nodig hebt voor het testen of echte polymorfisme later, het extraheren van een duurt letterlijk seconden met moderne tooling.
Je codebase zal eenvoudiger zijn, bevaarbaarder, en je zult niet de onderhoudslast hebben om interfaces op één lijn te houden met implementaties zonder voordeel.
Ironisch genoeg is deze benadering meer afgestemd op de origineel Behendig manifest dan de meeste "Agile" processen die ik heb ondervonden. Werk software snel (fase 1). Reageren op verandering (fase 1-2). Klantsamenwerking wanneer de functie aantoonbaar is (fase 2). Geen sprintpunten, geen snelheid volgen, geen bloedige burndown grafieken.
Moderne "Agile" is gevangen genomen door proces liefhebbers die hebben toegevoegd zo veel ceremonies er is geen tijd meer voor exploratie. Deze aanpak brengt spel terug niet als een verwennerij, maar als een legitieme fase van creatief werk.
Ik moet eerlijk zijn: dit is niet altijd de juiste aanpak.
Bugfixes: Als je een bekende bug repareert, is TDD stijl logischer. Schrijf een test die de bug reproduceert, repareer het, klaar.
Begrepen problemen: Als je een standaard algoritme implementeert of een patroon dat je al honderd keer hebt gebruikt, heb je geen exploratiefase nodig.
Strakke deadlines met bekende eisen: Als je echt precies weet wat je aan het bouwen bent en wanneer het moet verschepen, kun je fase 1 verkenning overslaan.
Zeer gereguleerde omgevingen: Als elke commit afmelden nodig heeft, is sandboxen moeilijker. Hoewel je nog steeds de exploratie lokaal kunt doen voordat je duwt.
Maar voor de meeste feature development. Waar eisen zijn een beetje wazig, de technische aanpak is niet voor de hand liggend, en je hebt ruimte nodig om te denken deze aanpak werkt briljant.
Als je vroeg in je carrière, je zou kunnen voelen druk om te laten zien "perfecte" code de hele tijd. Je hoeft niet te. Gebruik deze drie-fase aanpak gewoon duidelijk over welke fase je in, en altijd het werk helemaal tot fase 3 af te maken. Niemand zal je de schuld voor rommelige exploratie code als het leidt tot gepolijst, getest, productie-klaar code aan het einde.
Laat het werken, maak het mooi, sluit het af.
Start eenvoudig. Ontwerp met vooruitziendheid, maar niet speculatie. Itereer snel. Voeg alleen complexiteit wanneer het loont.
"Duplicatie is veel goedkoper dan de verkeerde abstractie."
De exploratiefase is geen schuldig geheim.Het is een gedefinieerd onderdeel van het proces. Je vestiging is je sandbox totdat je een PR verhoogt. Technische feedback komt vroeg, niet-technische feedback komt midden, ops en security feedback komt te laat.
Dit houdt creativiteit in leven, vermindert stress, en produceert systemen die elegant schalen omdat u het probleem begreep voordat u zich inzette voor de oplossing.
Geef jezelf toestemming om te spelen.
Als dit resoneert, kunt u ook genieten van deze gerelateerde berichten:
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.