Rozšiřitelný kód není lepší než jednoduchý kód
TLDR: Vyhněte se rozšiřitelnému kódu, pokud je to možné – je méně čitelný a složitější na úpravu.
Co je rozšiřitelný kód?
Ještě než začneme, pojďme si ukázat, co je myšleno rozšiřitelným kódem. Porovnejte tyto dva příklady:
public class UserProvider
{
public IEnumerable<User> CreateUsers(string dataType, object data)
{
switch (dataType)
{
case "json":
return CreateUsersFromJson(data);
case "csv":
return CreateUsersFromCsv(data);
default:
throw new NotSupportedException("DataType not supported");
}
}
private IEnumerable<User> CreateUsersFromJson(object data)
{
// ... logika pro JSON ...
}
private IEnumerable<User> CreateUsersFromCsv(object data)
{
// ... logika pro CSV ...
}
}vs
public class UserProvider
{
private readonly IEnumerable<IDeserializer> _deserializers;
public UserProvider(IEnumerable<IDeserializer> deserializers)
{
_deserializers = deserializers;
}
public IEnumerable<User> CreateUsers(string dataType, object data)
{
var deserializer = _deserializers.FirstOrDefault(e => e.CanDeserialize(dataType));
if (deserializer == null)
{
throw new NotSupportedException("DataType not supported");
}
return deserializer.Deserialize(data);
}
}
public interface IDeserializer
{
bool CanDeserialize(string dataType);
IEnumerable<User> Deserialize(object data);
}
public static class DeserializersRegistration
{
public void Install(IServiceCollection services)
{
services.AddTransient<IDeserializer, JsonDeserializer>();
services.AddTransient<IDeserializer, CsvDeserializer>();
}
}Druhý kód je rozšiřitelný – můžete přidávat nové deserializátory pouze s minimálními úpravami kódu.
Popularita rozšiřitelného kódu
O rozšiřitelném kódu se píší knihy, studuje se ve školách, vyžadují ho seniorní vývojáři v práci, používá se v open source knihovnách – je všude. Už jen množství návrhových vzorů a postupů, které byly vytvořeny s tímto cílem, je neuvěřitelné. Krátký výčet:
- Inheritance
- Composition
- Mediator pattern
- Dependency injection
- Open Closed Principle
- Pipeline pattern
- …
Celý tento systém vede mnoho programátorů k jedinému možnému závěru: Rozšiřitelný kód je lepší než jakýkoli jiný – proč bychom mu jinak věnovali tolik času a úsilí?
Je ale tento závěr správný? Pojďme se podívat na nevýhody rozšiřitelného kódu.
Nevýhody rozšiřitelného kódu
Nevýhoda 1: Rozšiřitelný kód je hůře pochopitelný
Obecně můžeme říct, že téměř každý rozšiřitelný kód je méně čitelný než kód, který není rozšiřitelný. Pro demonstraci můžeme použít ukázku z úvodu článku – asi žádný programátor by netvrdil, že druhý (rozšiřitelný) kód je jednodušší na pochopení.
Klikněte pro opětovné zobrazení příkladu z úvodu článku
public class UserProvider
{
public IEnumerable<User> CreateUsers(string dataType, object data)
{
switch (dataType)
{
case "json":
return CreateUsersFromJson(data);
case "csv":
return CreateUsersFromCsv(data);
default:
throw new NotSupportedException("DataType not supported");
}
}
private IEnumerable<User> CreateUsersFromJson(object data)
{
// ... logika pro JSON ...
}
private IEnumerable<User> CreateUsersFromCsv(object data)
{
// ... logika pro CSV ...
}
}vs
public class UserProvider
{
private readonly IEnumerable<IDeserializer> _deserializers;
public UserProvider(IEnumerable<IDeserializer> deserializers)
{
_deserializers = deserializers;
}
public IEnumerable<User> CreateUsers(string dataType, object data)
{
var deserializer = _deserializers.FirstOrDefault(e => e.CanDeserialize(dataType));
if (deserializer == null)
{
throw new NotSupportedException("DataType not supported");
}
return deserializer.Deserialize(data);
}
}
public interface IDeserializer
{
bool CanDeserialize(string dataType);
IEnumerable<User> Deserialize(object data);
}
public static class DeserializersRegistration
{
public void Install(IServiceCollection services)
{
services.AddTransient<IDeserializer, JsonDeserializer>();
services.AddTransient<IDeserializer, CsvDeserializer>();
}
}Je důležité zmínit, že ukázky jsou pro účely článku co nejjednodušší – v praxi bude rozdíl ve složitosti téměř vždy ještě větší.
Nevýhoda 2: Rozšiřitelný kód je často složitější upravit
Může to znít paradoxně, ale je to tak – rozšiřitelný kód lze často jednoduše upravit pouze způsobem, který autor zamýšlel.
Tento problém můžeme také demonstrovat na příkladu z úvodu článku. Řekněme, že máme nové požadavky:
- CSV může mít v prvním sloupci jméno i příjmení, nebo jen jméno.
- Zda se jedná o jméno, nebo příjmení, nemůžeme rozhodnout pouze z dat, protože někteří lidé mají dvě jména. O tom, co CSV obsahuje v prvním sloupci, rozhoduje uživatel, který metodu volá.
- Všechny ostatní formáty vždy obsahují jméno i příjmení.
První příklad můžeme upravit jednoduše:
public class CsvSettings
{
public bool FirstColumnIsFullName { get; set; };
}
public IEnumerable<User> CreateUsers(string dataType, object data, CsvSettings csvSettings = null)
{
switch (dataType)
{
case "json":
return CreateUsersFromJson(data);
case "csv":
// Nový parametr csvSettings
return CreateUsersFromCsv(data, csvSettings);
default:
throw new NotSupportedException("DataType not supported");
}
}Jak ale tento problém vyřešit u druhého příkladu? Asi nejjednodušší řešení je vytvořit objekt DeserializationSettings:
public class DeserializationSettings
{
public CsvSettings CsvSettings { get; set; };
}
public class CsvSettings
{
public bool FirstColumnIsFullName { get; set; };
}A ten pak předat do metody:
public IEnumerable<User> CreateUsers(string dataType, object data, DeserializationSettings settings = null)
{
var deserializer = _deserializers.FirstOrDefault(e => e.CanDeserialize(dataType));
if (deserializer == null)
{
throw new NotSupportedException("DataType not supported");
}
// Nový parametr settings
return deserializer.Deserialize(data, settings);
}Není to úplně hezké řešení, jelikož jsme museli přidat obecný objekt, který bude použit pouze v případě, že se jedná o CSV.
Pokud jste se sami zamysleli nad řešením, určitě uznáte, že úpravu bylo jednodušší vymyslet a provést v prvním příkladu, ve kterém nepoužíváme rozšiřitelný kód.
Opět je důležité zmínit, že ukázky jsou pro účely článku co nejjednodušší – v praxi bude rozdíl větší.
Nevýhoda 3: Rozšiřitelný kód ztrácí informace
Porovnejte tyto dvě metody:
User CreateUser(string name, string surname, string? email);vs
User CreateUser(object[] parameters);Druhá metoda je určitě o něco rozšiřitelnější – můžeme volně přidávat parametry, které metoda očekává, aniž bychom měnili její signaturu.
Pokud se ale podíváte na první metodu, okamžitě tušíte, jaké jsou povinné a nepovinné parametry pro vytvoření uživatele. Tato informace se u rozšiřitelného kódu ztrácí.
Proč autoři knihoven píší rozšiřitelný kód?
Možná si říkáte: Když je rozšiřitelný kód tak špatný, proč ho autoři knihoven používají tak často? Odpověď je jednoduchá – Autoři knihoven nepíší rozšiřitelný kód, protože je lepší, ale protože jsou k tomu donuceni. Knihovny mají několik omezení, která váš kód (pravděpodobně) nemá:
- Rozmanitost využití
- Váš kód – má poměrně jasně dané použití. Např. e-shop -> člověk, který chce nakoupit.
- Knihovny – mají často nespočet využití. Např. u standardní knihovny C nelze definovat „běžné“ použití, protože takové neexistuje.
- Cílová skupina
- Knihovny – jejich uživateli jsou programátoři, kteří mohou psát vlastní rozšíření.
- Váš kód – člověk, který vám zadává práci, pravděpodobně není schopný napsat rozšíření vašeho kódu.
- Velikost veřejného API
- Váš kód – má obvykle jen malé veřejné API. Např. webový server má pouze několik veřejných endpointů a vše ostatní je privátní kód, který se nemůže změnit.
- Knihovny – mají často obrovské veřejné API. Např. již zmíněná standardní knihovna C obsahuje stovky, možná i tisíce veřejných funkcí.
- Breaking changes
- Knihovny – si obvykle nemohou dovolit téměř žádné breaking changes. Migrace všech uživatelů na novou verzi je často proces na roky až desítky let.
- Váš kód – u mnoha API máte pouze malé množství uživatelů, které můžete donutit k přechodu na novou verzi.
Rozdílů je samozřejmě mnohem více, ale i těchto pár naprosto jasně vysvětluje, proč knihovny musí obsahovat rozšiřitelný kód.
Proč používáme rozšiřitelný kód v našich aplikacích?
Programátoři mají často pocit, že použití jednoduchého (nerozšiřitelného) kódu přidává do aplikace technický dluh, který se jednou bude muset zaplatit. Tato úvaha je sice správná, ale neúplná.
Pokud zvolíte rozšiřitelný kód, tak také přidáváte technický dluh – přidali jste kód, který je v tuto chvíli zbytečně složitý a zhoršuje čitelnost. Pouze doufáte, že se tento dluh sám v budoucnu zaplatí.
Prakticky tak sázíte na to, jaké budou budoucí požadavky. Následující tabulka ukazuje možné scénáře:
| Scénář | Jednoduchý kód | Rozšiřitelný kód |
|---|---|---|
| Mnoho nových implementací Zákazník požaduje X dalších implementací zapadajících do rozšiřitelného kódu | Prohra Nutný refactoring | Výhra Snadné přidání |
| Málo změn Zákazník požaduje pouze málo dalších implementací | Výhra Jednoduchý a dostatečný kód | Prohra Zbytečně složitý kód |
| Neočekávané změny Změny obtížné pro rozšiřitelný kód | Výhra Relativně snadná úprava | Těžká prohra Složitý + těžko upravitelný kód |
Jak můžete vidět, volba rozšiřitelného kódu není úplně jasná výhra.
Předčasné závěry
Všiml jsem si také, že programátoři často rychle sklouznou k závěru, že když dnes klient požaduje 2–3 varianty, tak zítra bude požadovat dalších 10, a proto hned volí rozšiřitelný kód.

Nemám žádná konkrétní data, ale osobně se setkávám spíše s opakem – obvykle se naimplementuje 2–5 variant a pak už se nikdy žádná nepřidá.
Pro kterou variantu se rozhodnout?
Pokud má software, který vyvíjíte, podobná omezení jako knihovny pro programátory, tak zvolte rozšiřitelný kód. Pokud v takové situaci nejste, tak bych vám téměř vždy doporučil řídit se pravidlem YAGNI – You Ain’t Gonna Need It. A začít s jednodušší variantou.
Martin Fowler to ve svém článku YAGNI skvěle popisuje. Doporučuji ho přečíst i těm, kteří YAGNI už znají. Kromě argumentů ve Fowlerově článku bych doplnil ještě 2 další důvody, proč preferovat jednodušší variantu:
U jednoduchého kódu je jednodušší poznat, že je čas na refactoring
Poznat, že jednoduchý kód potřebuje zrefaktorovat, je jednoduché – obvykle stačí hledat třídy s velkým množstvím řádků kódu. Na druhou stranu poznat, že rozšiřitelný kód by byl čitelnější, kdyby byl napsaný jednoduše, může být velice obtížné.
Nikdo nechce rozbíjet krásný kód
Programátoři jsou na svůj kód hrdí, a zvláště pak na kód, který obsahuje geniálně vymyšlené abstrakce. Když pak potřebujete kód změnit na jednoduchý, protože nevyhovuje požadavkům, tak se můžete dostat do nepříjemných situací.
Osobně jsem už mnohokrát zažil situace, kdy byl kód prošpikovaný rozšiřitelností, která byla neskutečně složitá. Programátoři na těchto projektech si tuto skutečnost ale nepřipouštěli, protože byli až příliš hrdí na svůj kód. Toto se vám málokdy stane u jednoduchého kódu.
Závěr
Rozšiřitelný kód má své místo – zejména v knihovnách a dlouhodobých API s velkým počtem uživatelů. Pro většinu business aplikací je ale jednoduchý kód lepší volbou. Je čitelnější, snadněji se upravuje a lépe se v něm poznají nevhodné části.
Příště, než sáhnete po design patternu nebo složité abstrakci, zeptejte se sami sebe: “Jak obtížné bude tento kód v budoucnosti upravit?”