Seznam všech (mých) changesetů vytvořených v nějakém časovém intervalu od-do lze získat z příkazové řádky utilitou tf.exe následovně:
tf history * /r /user:username /version:Dyyyy-mm-dd~Dyyyy-dd-mm
Interval se zadává jako specifická verze, datum je určeno prefixem "D" a data jsou oddělena "~" (vlnovkou). Je-li zadáno pouze jedno datum, má význam do.
Starší, nikoliv však zastaralé články a připrovavovaných novinkách v .NET 4.0 (VS 2010)
Expozé
Tento příspěvěk by se stejně dobře mohl jmenovat "lovcovy zápisky z honu na duchy". Bude asi trochu zmatečný, stejně jako moje ponětí o tom, co se vlastně dělo a čím že jsem to nakonec vyřešil.
Úvod do problému
Chtěl jsem dalšímu uživateli umožnit přístup do TFS přes TFS Web Access, zopakoval jsem tedy postup, který v minulosti zafungoval u jiného uživatele, tj. přidal jsem ho do skupiny Contributors příslušného projektu. Ale ouha - to co předtím fungovalo, tentokráte naprosto selhalo. Každý jeho pokus o přístup do TFS skončil s chybou "Unable to create workitemstore see exceptions for more details."
InnerException jsem se samozřejmě nikde nedohledal, ale občas se podařilo v TFS-WA vymámit tuto chybu: "RebuildCallersViews: Connecting Domain User could not be validated. ---> RebuildCallersViews: Connecting Domain User could not be validated. ---> RebuildCallersViews: Connecting Domain User could not be validated."
To už bylo lepší, ale pořád málo
Vyšetřování
Po chvíli práce s vyhledávačem jsem zjistil, že chyba je pravděpodobně v synchronizaci informací mezi doménou a tabulí TFSWorkItemTracking..ADObjects. To lze ověřit tímto SQL:
select next_id - 1 as GSSMaxIdenditySeqId from TFSIntegration..tbl_sequence_ids where name = 'identity_cache'
select max(seqid) as WITMaxIdentitySeqId, MAX(LastSyncUTC) as LastSyncUTC from TFSWorkItemTracking..ADObjects
Pokud jsou data synchronní, oba dotazy by měly vrátit stejné ID. V mém případě stejná nebyla, takže stopa.
Jenže proč k synchronizaci nedochází?
Nejprve jsem zkontroloval joby v SQL serveru a zjistil, že některé z nich jsou nastaveny tak, aby běžely pod účtem kolegy, který již před delší dobou odešel. Přenastavil jsem je tedy na platný účet - a nic.
Vyšetřování pokračuje
Po další chvíli práce s vyhledávačem jsem se dostal k tomuto SQL:
select [address], [domain], [account_name], [deleted]
from TfsIntegration..tbl_subscription s
join TfsIntegration..tbl_security_identity_cache i
on s.subscriber = i.sid
where s.delivery_type = 2
Account name by měl být platný účet a příznak deleted by měl být false (respektive 0). A zde byl opět již neexistující účet bývalého kolegy. Zajásal jsem a tbl_subscription.account_name nastavil na SID platného účtu - a nic.
Řešení?
Poslední krok, po kterém se tabule sesynchronizovaly a nově přidaný uživatel se mohl konečně přihlásit, byl restart IIS a smazání cache TFS-WA (ta je standardně v \Program Files\Microsoft Visual Studio 2008 Team System Web Access\Cache\).
A proč řešení s otazníkem? Protože si nejsem po všem tom laborování jistý, co přesně bylo rozhodující pro vyřešení problému s přihlašováním, zda všechno (i špatný account jobu), nebo jen část (oprava v tbl_subsriptions a restart IIS), a jestli jsem náhodou neudělal ještě něco důležitého (například update TfsWorkItemTracking.dbo.Persons set fRebuildWrite = 1, kterýžto SQL jsem objevil na jedné otevřené záložce browseru), na co jsem mezitím zapomněl.
Někdy se stává, že v Solution Exploreru jsou u souborů uložených v TFS zobrazeny špatné stavové ikony, které naznačují, že soubor je check-out někým jiným, přestože to není pravda (jak lze ověřit přes Source Control Explorer). Příkaz Refresh Status z menu File / Source Control nepomůže, nepomůže ani znovuvytvořit workspace... Ne že by kvůli tomu nešlo kompilovat, nebo tak něco, ale mate to.
Nakonec jsem s největší pravděpodobností odpověď nalezl: stává se to u souborů, které jsou shelvsetu. Každopádně zrušit shelvset pomohlo, jenom mne mate, že u klienta jsem měl shelvsetů nepočítaně a přitom se ikonky zobrazovaly správně.
Během práce u klienta jsem si pro "Compare" operace ve Visual Studiu (TFS) nainstaloval WinMerge a po návratu od něj (u klienta jsem dělal na jeho počítači) jsem zjistil, že už se bez něj nedokáži obejít a že mi standardní komparátor ve VS nestačí. A že jsem zapomněl, jak WinMerge pro spolupráci s VS správně nakonfigurovat. A protože se mi to nechtělo zjišťovat, obrátil jsem se s důvěrou na Strýčka Googla a našel původní dva články, ze kterých jsem vycházel:Using
winmerge as a merge tool in Visual Studio Team Suite a Using
WinMerge with TFS.
Postup je jednoduchý:
- Nainstalovat WinMerge
- Menu Tools, položka Options, pak ve stromu Source Control, pod ním Visual Studio Team Foundation Server a na něm tlačítko Configure User Tools...
- Vybrat .exe WinMerge a správně zadat Arguments
- Compare: /x /e /ub /wl /dl %6 /dr %7 %1 %2
- Merge: /x /e /ub /wl /dl %6 /dr %7 %1 %2 %4
Má to jenom jednu chybu, a sice když je v nastavení WinMerge zašrtnuto "Allow only one instance tu run" a člověk se jej z VS pokusí otevřít ještě jednou, neotevřou se porovnávané soubory jako nová záložka v běžící instanci WinMerge, ale skončí to s podivnou chybou "The system cannot find the file specified". A vypadá to spíše na chybu VS, protože ten file (v TFSTemp), kterého se WinMerge domáhá, skutečně neexistuje.
Zajímavý článek od Jeremy Millera Cohesion And Coupling vyšel již před časem v MSDN Magazine.
Obsah
- Decrease Coupling
- Increase
Cohesion
- Eliminate Inappropriate Intimacy
- The Law of Demeter
- Tell,
Don't Ask
- Say It Once and Only Once
- Wrapping Up
Základní principy
- Keep things that have to change together as close together in the code as
possible.
- Allow unrelated things in the code to change independently (also known as
orthogonality).
- Minimize duplication in the code.
Užitečnou pomůckou pro organizaci projektů (větší solution jich může obsahovat i řádově desítky) jsou solution folders - složky, které existují pouze v rámci solution a projekty v nich mohou být zařazeny bez ohledu na své fyzické umístění na disku. Velmi dobrý příklad je zde, jiný dobrý příklad nabízí např. Enterise Library, viz následující screenshoty:

Organizace na disku
Organizace v Solution Exploreru
Nikdy nedefinovaný zákona zachování souborů by mohl znít: Lze smazat pouze tolik souborů, kolik jich bylo na filesystém uloženo. Total Commander jich však zvládne smazat mnohem více:

(Jistě, je to důsledek numerické chyby, nebo nějaké optimalizace, ale přeci jenom to člověka zarazí: co když opravdu začal mazat i něco víc, než co smazat měl?)
Problém, o kterém chci dnes mluvit, je úzce svázán s konkrétními technologiemi, takže asi nikomu trn z paty nevytrhne, nicméně jako inspirace a příklad toho, jakým způsobem se jednotlivé komponenty vzájemně ovlivňují a k jakým potížím to může vést, poslouží myslím celkem dobře.
Budiž chyba: Do prvku UI ButtonEdit (součást DevExpress), který je nabindován na dataset a má nastavenu numerickou maskou, nejde zadat více než 10 číslic jinak, než tak, že uživatel pole opustí a opět se do něj vrátí.
Po nikam nevedoucím laborování s nastavením masky a bindingu, následovaném chvilkou hledání na stránkách podpory výrobce je bezprostřední příčina a tedy i řešení na světě: The problem is that your TextEdit control is bound to an integer value. However, the integer type cannot accept more than 10 digits. The solution is to bind the control to a long value.
S touto znalostí je oprava již snadná: stačí při inicializaci (v InitializeComponent()) nastavit vlastnost EditValue controlu na hodnotu typu, který má větší rozsah než Int32, např: EditValue = 0L; nebo EditValue = new decimal(new int[] {0, 0, 0, 0}); (pozn. nelze použít EdiValue = 0m; protože s tímto zápisem se nevypořádá VS designer).
Tím se také vysvětluje, proč pomáhalo zadávání nadvakrát, tedy opustit kontrol a zase se do něj vrátit. Při opuštění se hodnota zapsala do datasetu, to vyvolalo změnovou událost, na kterou kontrol zareagoval načtením hodnoty z datového zdroje. A protože tato hodnota byla již typu Decimal, kontrol se tomu přizpůsobil a umožnil zadávání čísel přesahujících rozsah Int32.
Jenže stále zůstává otázka proč? Proč se kontrol chová, jako by byl nabindován na hodnotu typu Int32, když příslušný sloupec v datasetu je typu Decimal, respektive Int64? Tak na tuto otázku bohužel nemám lepší odpověď, než že to někdo naprogramoval tak, že se nezjišťuje typ vlastnosti, na kterou je kontrol nabindován, ale typ hodnoty, kterou binding vrátí. A je-li tato hodnota null (což u nově založené řádky v datasetu je), pak se kontrol chová, jako by byl bindován na defaultní Int32.
Cituji z (jinak velmi zajímavého, doporučuji k přečtení) článku The cost of enumerating in .NET:
Delegate dispatch still isn’t quite the speed of virtual method dispatch. And
delegates bound to static methods are actually slightly slower than those bound
to instance methods, which is why you’ll notice a slight difference in the
original “s” versus “i” measurements. The reason is subtle. There is a
delegate dispatch stub that is meant to call the target method: when the
delegate refers to an instance method, the ‘this’ reference pushed in EAX points
to the delegate object when it is invoked and the stub can simply replace it
with the target object and jump; for static methods, however, all of the
arguments need to be “shifted” downward, because there is no ‘this’ reference to
be passed and therefore the first actual argument to the static method must take
the place of the current value in EAX.
Virtuální vlastnosti jsou definovány takto:
public class Base
{
private bool isValid;
public virtual bool IsValid
{
get { return this.isValid; }
set { this.isValid = true; }
}
}
Kód, který pro předefinování takto definované vlastnosti v potomkovi vygeneruje VS po "expanzi" klíčového slova
override (a který jsem většinou slepě používal) je následující:
public class Derived : Base
{
private bool hasInvalidItems = false;
public override bool IsValid
{
get
{
// & this.hasInvalidItems je implementace specifická pro třídu Derived
return base.IsValid & this.hasInvalidItems;
}
set
{
base.IsValid = value;
}
}
}
Jazyk C# však umožňuje předefinovat pouze getter/setter, přičemž pro nepředefinovaný accessor se použije implementace z předka:
public class Derived : Base
{
private bool hasInvalidItems = false;
public override bool IsValid
{
get
{
return base.IsValid & this.hasInvalidItems;
}
}
}
Musím kajícně přiznat, že přes letitou praxi jsem na tuto vlastnost C# přišel teprve nedávno. Přitom to není nic překvapivého, uvědomí-li si člověk, jak jsou vlastnosti realizovány na úrovni IL, je to zcela logické (tak jsem to ostatně také přišel - úvahou).
P.S. Jaký je překlad termínu override do češtiny? Nenapadá mne nic, co by nebylo matoucí a zavádějící.
Byl jsem poněkud zaskočen tím, že se mi najednou nepodařilo spustit službu, která dříve bez problému chodila, a na níž se nezměnilo nic jiného, než že byla nově zkompilována po přechodu na VS2008. Chybová zpráva zněla:
System error 14001 has occurred.
This application has failed to start because the application configuration is incorrect. Reinstalling the application may fix this problem.
Protože se v ní hovoří o "application configuration", začal jsem hledat příčinu v chybném .config souboru. Také jsem si ale povšiml, že k selhání služby dojde tak rychle po pokusu o spuštění, že velmi pravdepodobně není vůbec nataženo CLR a spuštěn .NET kód, takže .config není vůbec použit.
Začal jsem tedy pečlivěji pátrat v Event Logu, ve kterém jsem nakonec nalez několik záznamů ze zdroje SideBySide, které se týkají příslušné služby:
- Syntax error in manifest or policy file "Service.exe.Config" on line 2. The manifest file root element must be assembly.
- The application failed to launch because of an invalid manifest.
- The CliView Application Server Host Service service failed to start due to the following error: This application has failed to start because the application configuration is incorrect. Reinstalling the application may fix this problem.
- A také poněkud matoucí Generate Activation Context failed for Service.exe. Reference error message: The operation completed successfully.
Z těchto záznamů jsem usoudil, že chyba je někde v manifestu aplikace, což bylo nepříjemné, protože je to pro mne španělská vesnice, a navíc s ním nikde vědomě nepracuji.
Nakonec jsem však řešení nalezl poměrně rychle ve Visual Studiu v Project Properties na záložce Application. Zde je ve spodní části sekce Resources, v ní volba Icon and Manifest a pod ní Manifest. Zde je možno vybrat mezi Embed manifest with default settings (defaultní volba) a Create application without a manifest. Stačí přepnout na Create application without a manifest, překompilovat a chodí to.
Mám však jisté pochybnosti o tom, že toto je skutečná příčina problémů, spíše si myslím, že by měl být způsob, jak vytvořit aplikaci s manifestem tak, aby mohla být zároveň spouštěna jako windows service.
Jednou z novinek v jazyce C# 3.0 jsou inicializéry
(anglicky initializers), typický zástupce rodu syntaktický cukr.
Jedná o speciální konstrukci, která volitelně následuje po volání konstruktoru a
ve které je umožněn implicitní přistup k nově vytvořené instanci a jejím datovým
(pole, vlastnosti) členům. Zřejmější to bude na příkladu:
Mějme třídu Alfa:
public class Alfa
{
public int i;
public string s;
}
Její instanci je s pomocí inicializéru možno vytvořit takto:
Alfa a = new Alfa { i = 1, s = "a" };
což je zcela ekvivalentní zápisu:
Alfa newAlfa = new Alfa();
newAlfa.i = 1;
newAlfa.s = "a";
Alfa a = newAlfa;
(Poznámka: Závorky ve volání konstroktoru nejsou při použití inicializátoru
povinné, takže Alfa a = new Alfa { i = 1, s = "a"
}; a Alfa a = new Alfa () { i = 1, s = "a" };
jsou zcela rovnocenné zápisy.)
Z tohoto pohledu se tedy zdá, že inicializér nic nového nepřináší. Využití
inicializéru je především v souvislosti s anonymní typy a LINQ. V
následujícím příkladě (alfas je kolekce instancí třídy Alfa):
var small = from a in alfas select new {Number = a.i};
představuje výraz new {Number = a.i}
vytvoření instance anonymního typu. Kompilátor z inicializéru vyčte strukturu
anonymního typu a především jména vlastností, která by ze standarní syntaxe
konstruktoru nebylo vyčíst nedala.
Inicializace kolekcí
Větší použití zřejmě nalezne inicializér při vytváření kolekcí. Výše
zmiňovanou kolekci alfas je s pomocí inicializéru možno vytvořit následovně:
List<Alfa> alfas = new List<Alfa>
{
new Alfa {i=1, s="one"},
new Alfa {i=2, s="two"},
new Alfa {i=3, s="three"},
};
což je ekvivalentní zápisu:
List<Alfa> alfas = new List<Alfa>();
Alfa a;
a = new Alfa();
a.i = 1;
a.s = "one";
alfas.Add(a);
a = new Alfa();
a.i = 2;
a.s = "two";
alfas.Add(a);
a = new Alfa();
a.i = 3;
a.s = "three";
alfas.Add(a);
Který z obou zápisů je čitelnější je nabíledni.
Inicializace členských proměnných
Inicializéry umožňují též komplexní inicializaci členských proměnných třídy v
deklaraci, což bylo zatím nutno řešit v explicitně konstruktoru. Opět ukáži na
příkladu, protože tak to bude nejsrozumitelnější:
public class UsingInitializers
{
private List<Alfa> alfaList = new List<Alfa>
{
new Alfa {i=1, s="one"},
new Alfa {i=2, s="two"},
new Alfa {i=3, s="three"},
};
};
Vnořené inicializéry
Mějme kromě výše zmíněné třídy Alfa ještě třídu Omega:
public class Omega
{
public Alfa alfa;
}
Její instanci je s pomocí inicializéru možno vytvořit takto:
Omega w = new Omega { alfa = new Alfa { i = 3, s = "w" } };
Pozor, vnořený inicializér nevytváří instanci, kterou inicializuje.
Proto je ve výše uvedeném příkladě třeba použít new
Alfa(). Pokud by ovšem třída Omega byla definována následovně:
public class Omega2
{
public Alfa alfa = new Alfa();
}
Potom lze napsat jen:
Omega2 w2 = new Omega2 { alfa = { i = 3, s = "w" } };
Podobný zápis, ale s třídou Omega, tedy:
Omega w = new Omega { alfa = { i = 3, s = "w" } };
způsobí run-time exception.
(Pozn. Opravil jsem některé chyby, na které jsem byl upozorněn
v komentářích).
V diskusi pod článkem Můžeme se obejít bez dědičnosti vyvstala otázka, jaké že jsou vlastně základní pilíře OOP. Ukázalo se totiž, že každý za ně považuje něco jiného. Následujícím text je reakcí na tuto diskusi.
Základní vs. odvozené
Předně je třeba si ujasnit, jaký je rozdíl mezi pojmy primárními a sekundárními, čili základními a odvozenými. Odvozené (sekundární) pojmy je možno vyjádřit pomocí pojmů základních (primárních), zatímco naopak to neplatí. Jinak řečeno, existence sekundárních vlastností je bez existence primárních nemožná.
Bohužel, v případě OOP je realita taková, že primární a sekundární pojmy jsou často směšovány a zaměňovány, a nejednou to jde až tak daleko, že jsou sekundární pojmy ztotožňovány přímo s OOP samotným. Důvod tohoto nepochopení je nasnadě: sekundární pojmy se odrážejí přímo v objektových programovacích jazycích a bývají tím prvním (a pohříchu často jediným), s čím přijde nový adept OOP do styku.
Dělení pojmů na základní a odvozené je následující: Primární pojmy jsou identita, zapouzdření a skládání; sekundární jsou třída, dědění, polymorfismus.
V dalším textu proberu jednotlivé pojmy podrobněji a pokusím se vysvětlit, proč mnou uváděné rozdělení mezi pojmy primární a sekundární odpovídá výše uvednému.
Objektového paradigma
Výpočetní model
Možná poněkud netradičně začnu výpočetním modelem, který stojí ve středu programu vytvořeného podle paradigmatu OOP. Platí, že objektový program je systém, tedy množina prvků a vazeb mezi nimi (statická struktura), která vykazuje v čase identifikovatelné chování (dynamika). A právě tato dynamika, tedy v čase rozpoznatelné změny, je reprezentována výpočetním modelem, který je v OOP založen na posílání zpráv.
Jinými slovy, běh objektového programu spočívá v posílání zpráv mezi objekty a v jejich zpracováním na straně příjemce.
Je ovšem důležité mít na paměti, že toto platí pro konceptuální úroveň. Na fyzické úrovni, tedy v počítači, může být konkrétní realizace jiná, například volání procedur, skoky do podprogramů, přerušení.
Základní pojmy
Identita
Identita objektu znamená, že každé dva objekty jsou od sebe navzájem odlišitelné na základě prostého faktu své existence. Identita objektu nijak nezávisí na hodnotách vnitřních atributů objektu.
Toto je kardinální rozdíl například ve srovnání s relačními databázemi, ve kterých není z konceptuálního hlediska možno rozlišit dva záznamy, které mají stejné hodnoty všech atributů. Mimo jiné proto se do relačních tabulek zavádějí primární klíče, aby bylo možno přesně určit, který záznam je který. Identita objektu na konceptuální úrovni však není žádné číslo, není to primární klíč, je to fakt.
Identita je nezbytným předpokladem pro správnou funkci výpočetního modelu, protože pouze na jejím základě je možno přesně určit, kterému objektu je zpráva určena.
Zapouzdření
Každý objekt je vzhledem k svému okolí „černá skříňka“, která se navenek reprezentuje pouze svým protokolem – množinou zpráv, které je objekt schopen přijmout. Žádný z dalších prvků systému (objektů) nemá žádnou znalost o tom, co je uvnitř, jak je dotyčný objekt implementován.
Zapouzdření je klíčovým předpokladem pro zachování konzistence systému, protože vylučuje, aby data (tedy stav) objektu mohl neočekávaně či nesmyslně změnit jiný objekt. Zapouzdření přesně vymezuje odpovědnosti a jeho důsledkem je každý objekt může svá data považovat vždy za validní, protože nikdo jiný než on sám je nemůže změnit.
Zapouzdření představuje nejenom nástroj pro zajištění bezpečnosti a konzistence dat (potažmo celého systému, respektive programu), ale v podstatě vynucuje (opět na konceptuální úrovni) anonymitu klienta, čímž přispívá k lepší udržovatelnosti a škálovatelnosti programu.
Bez zapouzdření by nebylo možno jednoznačně určit, kde jsou hranice objektu, která data jsou jeho, a tedy za jaká data je objekt ještě odpovědný, a za jaká již ne.
Skládání
Skládání je vyjádřením HAS-A vztahu. Skládání znamená, že jeden objekt v sobě může obsahovat jiný objekt. Často se v této souvislosti hovoří o vztahu pán-sluha, protože typickou motivací pro vytvoření tohoto vztahu je, že pán (celek) pověřuje (deleguje) sluhu (část) vykonáním těch operací, které sám neumí. Navenek to však vypadá, jako kdyby vše vykonával sám pán (viz zapouzdření).
Skládání umožňuje vyvářet vazby mezi objekty, tedy definovat strukturu programu, a tím jednoduše a intuitivně směrovat zprávy k jejich příjemcům. Bez skládání by nebylo možno vytvořit navigační graf, ale program by sestával s chaotické změnit objektů, z níž by se příjemci zpráv museli složitě vybírat.
Je dobré rozlišovat dva typy skládání: silnou a slabou vazbu. Silná vazba (agregace) představuje takový vztah, kdy existence části bez existence celku nemá smysl. Slabá vazba (asociace) je vztah dvou objektů, které mohou existovat samostatně.
Odvozené pojmy
Význam odvozených pojmů tkví v tom, že usnadňují tvorbu programu, zlepšují udržovatelnost kódu, zvyšují kvalitu (a čitelnost) výsledného kódu. Odvozené pojmy tedy představují velmi silné a prospěšné nástroje pro úspěšnou tvorbu programu podle objektového paradigmatu, nejsou však pro vytvoření funkčního programu nezbytné, na rozdíl od pojmů základních.
Třída
Třída je šablona na instance. Třída určuje, jaké atributy (datové členy) a metody (funkční členy) bude objekt mít a umožňuje rychle a bezpečně vytvářet objekty, které mají stejný protokol, stejný počet a typ datových členů a stejné metody (jejichž kód je často sdílený na úrovni třídy), a liší se od sebe (není to však podmínkou) pouze v hodnotách svých datových členů.
Třída ovšem není totéž, co typ. Objekty, které reprezentují entity (není míněna entita v kontextu relačního paradigmatu), stejného typu, jsou rovněž stejného typu, přestože k jejich vytvoření nemusí být použita třída (šablona).
Třída není pro vytváření objektů nezbytná, každý objekt je vždy možno postavit „na zelené louce“, tedy vždy zas a znova specifikovat všechny jeho členy. Neexistence tříd neznamená nemožnost vytváření objektů, ovšem z důvodů značné redundance nebezpečně zvyšuje rizika spojená s tvorbou a údržbou programu.
Dědění
Dědění je vztah IS-A, tedy vztah mezi podobnými typy, z nichž jeden je spíše abstraktní (předek) a druhý spíše konkrétní (potomek).
Pro dědění platí obdobně jako pro třídu, že není nezbytné pro vytvoření programu, protože lze členy všech typů definovat vždy znova; samozřejmě s riziky, která jsou spojena s redundancí. Vzhledem k existenci zapouzdření je jedno, zda objekt získal některé své vlastnosti děděním, nebo je má nadefinovány znova, navenek to nelze nijak rozlišit, protože protokol (na kterém z pohledu výpočetního modelu jedině záleží) je v obou případech stejný.
Dědění lze velmi dobře nahradit skládáním tak, že „potomek“ skládá svého „předka“ a deleguje na něj ty operace, které jsou pro oba stejné, a sám vykonává pouze ty, ve kterých se liší.
Polymorfismus
Polymorfismem je obecně míněno, že stejná zpráva vyvolá různé reakce, které se mohou lišit podle typu příjemce (polymorfismus 1. druhu), ale i podle času (polymorfismus 2. druhu), či dokonce podle typu volajícího (polymorfismus 3. druhu).
Existence polymorfismu implicitně vyplývá z existence protokolu, výpočetního modelu a zapouzdření. Objekt zprávě (výpočetní model) buď rozumí, nebo nerozumí (protokol), pokud jí rozumí, je zcela na jeho odpovědnosti, jak jí zpracuje (zapouzdření).
Polymorfismus tedy není třeba definovat jako základní pojem, protože ze základních implicitně vyplývá.
Závěr
Jistá zmatečnost, která v ohledu dělní na základní a odvozené pojmy panuje, je podle mého přesvědčení způsobena tím, jak jsou navrženy (dominantně používané) programovací jazyky, které ve své většině neumožňují přístup k základním pojmům jinak, než prostřednictvím pojmů odvozených.
Všechny mně známé objektové programovací jazyky neumožňují vytvořit objekt jinak, než jako instanci třídy, přičemž každá třída vždy dědí (často implicitně) od jednoho triviálního předka (většinou třídy Object).
Mojí snahou bylo osvětlit rozdělení pojmů obecně užívaných v OOP na základní a odvozené a toto dělení i náležitě zdůvodnit. Obecný princip dělení je takový, že funkční (což ještě neznamená snadno realizovatelný a snadno udržovatelný) objektový program je možno vytvořit pouze na základně pojmů základních, naopak pojmy odvozené k vytvoření takového programy samy o sobě nestačí.
Když si Martin položil otázku Dědičnost: prokletí, hrozba a nebo přínos? (2. díl), připomněl mi referát, který jsem měl v roce 2002 na konferenci Objekty. Kupodivu se mi jej podařilo nalézt, takže jsem ho pouze oprášil, opravil několik překlepů a bez jakýchkoliv dalších změn vám jej nyní nabízím.
Nad některými body by se dalo jistě diskutovat, něco bych dnes již možná napsal jinak, nicméně základní sdělení považuji za stále aktuální.
Abstract. Odpověď na otázku z titulku zní ano. V článku bude vysvětleno proč a bude nastíněna a částečně zodpovězena otázka, co z toho plyne. Na programovacím jazyce Smalltalk bude ukázáno, jakým způsobem by se dal v praxi vytvořit program, který pracuje bez dědičnost. Pozornost bude rovněž věnována otázce, nebylo–li již toto téma, alespoň částečně zpracováno například v konstruktu Interface.
Prolog
Můžeme se obejít bez dědičnosti? Na první pohled je to kacířská otázka – vždyť dědičnost je většinou to první (a pohříchu často i poslední), co studenti OOP pochopí. Přesto se pokusím poukázat na to, že to možné je a že takovéto úvahy nejsou pouze samoúčelným akademickým cvičením, ale byly vyprovokovány problémy, se kterými se setkávám ve své praxi.
Alternativa k prostému dědění
Začněme od Adama – připomenutím základních objektových vlastností. Jsou to zapouzdření, skládání a identita objektů. Dědění mezi nimi není. Pokud tedy dědičnost není základní objektovou vlastností, mělo by být možno ji, v případě potřeby, s pomocí základních vlastností nahradit. Jak toho dosáhnout?
Nejprve je třeba si uvědomit, co vlastně máme na mysli, hovoříme-li o dědičnosti. První, co se pravděpodobně každému vybaví, je znovupoužitelnost vlastností, kdy potomek získává vlastnosti (atributy, metody) od svého předka a přidává k nim své vlastní. To je zajisté pravda, mnohem důležitější však je ta skutečnost, že potomek rozšiřuje protokol předka. Nesmíme totiž zapomínat na vlastnost zapouzdření, díky které vidíme z objektu ve skutečnosti pouze jeho protokol, který zakrývá jeho vnitřní implementaci.
Mechanismus dědění spočívá v tom, že přijde-li objektu zpráva, pro kterou nemá metodu, požádá o tuto metodu, respektive o její provedení, svého předka. Předek je však také objekt a co nám brání místo předka žádat libovolný jiný objekt? Nic. Pokud bude použité programové prostředí tuto možnost podporovat, není důvod, proč nepostupovat právě takto. Na mechanismu zpracování zprávy se nic nezmění.
Teoretická implementace
Jak takovýto přístup naimplementovat? Důraz by měl být kladen na to, abychom si vystačili s těmi prostředky, které jsou v (objektových) programovacích jazycích přítomny. Základem je zde skládání – objekt, který si potřebuje rozšířit protokol, bude skládat libovolný jiný objekt, který mu může potřebné metody poskytnout.
Dědění je skutečnosti pouze zvláštním případem tohoto skládání, který je výjimečný tím, že do něj ještě vstupuje třída. Objekt – instance žádá o potřebnou metodu svoji třídu, která skládá svoji nadtřídu, kterou v případě potřeby žádá o příslušnou metodu.
Dědění tedy můžeme nahradit tímto obecnějším principem. Pojmy „předek“ a „potomek“ se tak stanou relativními, spíše se bude jedna o vztah více (potomek) a méně (předek) specializovaného objektu.
V takovém případě bychom samozřejmě přišli o možnost, aby spolupracující objekty navzájem přímo sdílely své atributy (instanční proměnné), jak je tomu v případě třídní hierarchie. Takovýto stav však ve skutečnosti nejenže není na závadu, ale může být dokonce žádoucí. Zůstává totiž otázka, nakolik je správné, aby metody potomka využívaly instanční proměnné definované v předkovi. A vždy nám zůstane možnost použití přístupových metod. Ostatně takto psaný kód je mnohem přijatelnější z hlediska Demeterova zákona.
Praktická realizace
Jak to vše zrealizovat v praxi ukáži na příkladu jazyka Smalltalk. Nejjednodušším, avšak nikoliv nejefektivnějším, způsobem je vlastní implementace metody #doesNotUnderstand: Tato metoda je volána přímo virtuálním strojem v okamžiku, kdy se pro poslanou zprávu nepodaří najít příslušnou metodu. Standardní implementace je taková, že se zdvíhá výjimka MessageNotUnderstood. Naše implementace bude taková, že se zpráva, které nebylo porozuměno, pošle spolupracujícímu objektu. V případě, že je spolupracujících objektů více (viz dále), bude správný kolaborant vybrán jednoduše tak, že se zeptáme buď přímo jeho (#respondsTo: ), nebo jeho třídy (#canUnderstand:), jestli dané zprávě rozumí.
Metody vypadají následovně:
doesNotUnderstand: aMessage
^colaborant evaluate: aMessage
doesNotUnderstand: aMessage
^( colaborant respondsTo: aMessage selector)
ifTrue: [colaborant evaluate: aMessage]
ifFalse: [super doesNotUnderstand: aMessage]
Variantu číslo dvě považují za lepší, protože případná výjimka MessageNotUnderstood je zdvihána z klientského objektu, a nikoliv ze spolupracujícího.
Druhý, o něco sofistikovanější přístup, je založen na tom, že programátor specifikuje třídu spolupracujícího objektu. Smalltalk sám potom změní klientský objekt tak, že doplní instanční proměnnou a pro vybrané metody vygeneruje v klientském objektu metody se stejným jménem, které nebudou dělat nic víc než delegovat volání na spolupracující objekt a zpět vracet takto získanou návratovou hodnotu. Důležité je, že delegované metody nebudou všechny, ale budou podle nějakého klíče vybrány. Je možné delegovat takové metody, které specifikuje programátor, popřípadě všechny kromě nějakých výjimek. Jaké metody budou patřit mezi výjimky je sice otázka otevřená, ale dá se předpokládat, že to typicky budou odpovídající protokolu třídy Objekt. Zmatky, které by vznikly neočekávanými výsledky metod jako jsou #initialize, #yourself či #size si dovede každý představit.
Tento druhý způsob je mimo jiné zajímavý tím, že se jedná o pěknou a jednoduchou ukázku meta–programování. Vše se totiž může odehrávat na meta–úrovni posíláním zpráv, aniž by bylo nutno do celého procesu jakkoliv zapojovat kompilátor. Třída klientského objektu (což je instance meta–třídy tohoto objektu) je měněna pouze pomocí změny hodnot jejích instančních proměnných – seznamu jmen instančních proměnných a kolekce instančních metod. Kompilátor je v příkladě použit pouze proto, že umí velmi snadno udělat to, co by jinak musel řešit složitý a těžko srozumitelný kód.
Takový kód by mohl vypadat následovně (Client je třída, která potřebuje využít schopnosti třídy Collaborator):
Client class >> extendBehaviorWith: : Collaborator
| candidates methods ivName |
ivName := Collaborator name.
self addInstVarName: ivName.
candidates := Dictionary withAllSuperclasses copyWithout: Object.
methods := (candidates collect: [:e | e selectors collect: [:ee | e compiledMethodAt: ee]]) flatten
methods do: [:e |
| s fragments |
s := String new writeStream.
fragments := e selector tokensBasedOn: $:.
fragments last ifEmptyToss: [:o | fragments remove: o].
e numArgs = 0
ifTrue:[
s nextPutAll: fragments first.
s cr.
s nextPutAll: '^',ivName.
s nextPutAll: fragments first.
]
ifFalse:[
#('' '
^', ivName, ' ') do: [:ee |
s nextPutAll: ee.
1 to: fragments size do: [:i |
s nextPutAll: (fragments at: i).
(e selector includes: $:) ifTrue:[s nextPut: $:].
s space.
s nextPutAll: 'value'.
s nextPutAll: i printString.
s space.
]
]
].
Klient compile: s contents.
].
S ohledem na to, že první možnost dává programátorovi silnější kontrolu nad programem, osobně bych se stavěl za tuto možnost. A to i přesto, že vyžaduje od programátora mnohem větší úsilí a může degradovat rychlost výpočtu.
Alternativa k vícenásobné dědičnost
Výše navržený postup zároveň nabízí i poměrně elegantní možnost řešení problému, který se někdy nazývá vícenásobná dědičnost. Vícenásobnou dědičnost, přesto že je některými programovacími jazyky podporována, je možno hodnotit velmi rozporuplně. Pokud totiž přijmeme, možná poněkud odvážnou, analogii s biologickou realitou, dospějeme nutně k závěru, že objekt, který vznikne „křížením“ dvou předků, by měl mít nové kvalitativní vlastnosti, a ne být pouhým součtem vlastností těchto předků.
Pokud však použijeme koncept rozšiřování protokolu bez dědičnosti, můžeme snadno několik předků nahradit několika spolupracujícími objekty. Takovéto pojetí je zajisté mnohem blíže známé realitě. Může zde nastat situace, kdy dva (či více) spolupracující objekty jsou schopny rozumět stejné zprávě. Pak vyvstává otázka. který objekt by měl být příjemcem této zprávy. Je těžké si zde představit nějaké pravidlo, snad s výjimkou pravidla vytvořeného na základě statisticky. Řešení se skrývá ve skutečnosti, že by taková situace vůbec neměla nastat. Dá se totiž předpokládat, že v takovém případě v návrhu chybí ještě jeden – třetí – objekt, který má na starosti ty funkce, o které se zbylé dva přetahují.
Je zde místo pro Interface?
V důsledku statě o vícenásobné dědičnosti zcela samozřejmě přicházejí na mysl Interfacy, jakožto již existující řešení. Dovolím si tvrdit, že tomu tak není. Především interfacy nenesou žádnou funkcionalitu, ale (jak sám jejich název napovídá) představují pouze popis protokolu. To má snad místo v technologiích jako je CORBA či COM, v rámci homogenního programovacího jazyka to však považuji za zbytečné. Viz třeba použití v jazyce Java, kde jsou interface především nástrojem pro obejití jednoho z nejsilnějších omezení tohoto jazyka, totiž polymorfismu závislého na dědění. Na implementační úrovni je však takovýto konstrukt , alespoň teoreticky, zbytečný – objekt buď zprávě rozumí, nebo ne. A to by mělo platit i pro výše zmiňovanou CORBU, nebo COM.
Navíc interface nemohou existovat samy o sobě, ale pouze spolu s objektem, ke kterému jsou připojeny, což je rozdíl oproti zde popisovanému přístupu, který je založen na spolupráci dvou plnohodnotných objektů.
Interface ve Smalltalku
Ve své praxi programátora ve Smalltalku jsem se sám dostal k bodu, kdy jsem měl pocit že Interfacy jsou přesně to pravé, co vyřeší mé problémy. Naštěstí jsem nahlédl marnost takového počínání dříve, než jsem tomu věnoval jakékoliv úsilí, Přesto podpora Interface ve Smalltalku existuje. Jejím autorem je Benny Sadeh a je možno ji nalézt na http://wiki.cs.uiuc.edu. Tato komponenta doopravdy umožňuje i ve Smalltalku definovat Interface a následně je připojovat ke třídám. Takové připojení má za následek především to, že protokol dané třídy je rozšířen o zprávy, definované připojeným Interface, které jsou implementovány jako výjimka a programátor je musí nějakým rozumným způsobem předefinovat. Zvláště v kombinaci s extrémním programováním, které umí dobře využít poddajnost Smalltalku, považuji toto za zjevnou brzdu. Změna protokolu Interfacu se totiž nijak neprojeví do protokolu tříd, které jej implementují. Zajímavá je tak jedině možnost zjišťovat, které třídy daný interface implementují, což by se dalo realizovat mnohem jednodušeji.
Správné místo pro Interface
Bylo by však škoda Interface zavrhovat. Mají své nezastupitelné místo ve fázi analýzy. Interface totiž představují velmi silný nástroj k popisu rolí jednotlivých objektů. Ovšem business objektů. Platí totiž, z Odpovědností se dají vyvodit Role a z Rolí se dají vyvodit Interface. To platí, i když by se to dalo sloučit do jediného pojmu – role. Ve fázi (business) analýzy nám přesto mohou Interface prokázat neocenitelné služby, zejména v oblasti dokumentace.
Můžeme se tedy obejít bez dědičnosti?
Odpověď na otázku z titulku je tedy jednoznačná – ano. Musíme si ovšem položit i tu otázku, jestli, se bez dědičnosti obejít chceme? A zde bych stejně směle odpověděl ne. Dědičnost sama o sobě je užitečný nástroj, který nám umožňuje řešený problém strukturovat a tak lépe pochopit a lépe vyřešit. Dědičnost by však měla být především nástrojem pro modelování; pro programování až v druhé řadě.
V současné situaci je zcela běžné, že jsou z programátorských důvodů vytvářeny umělé a nepřirozené hierarchie pouze za účelem opětovného využití již jednou napsaného kódu, popřípadě pro zajištění polymorfismu. V případě aplikace navrženého přístupu je tak možno z hierarchie objektů eliminovat umělé podpůrné objekty a soustředit se na modelování samotného problému.
Praktické aplikace
Jedním z velmi silných návrhových vzorů je takzvaný Null Object Pattern. Je to však pattern programový, který není nutno začleňovat do řešení samotného problému. Pokud je tento vzor použit ve složitější hierarchii objektů, narazí programátor brzy na Nerudovskou otázku „Kam s ním?“ Začleníme-li ho do hierarchie objektů, tak aby mohl využívat metody předků, popřípadě jejich protokol, s vysokou pravděpodobností podědí atributy, které pro něj nebudou mít smysl. Řešením je například vytvořit abstraktní třídu, od které bude dědit jak třída konkrétní, tak třída pro Null Object Patternu. Pak však dostaneme pro změnu abstraktní třídu, jejíž existence je ospravedlněna pouze potřebou pomocného objektu pro pattern.
Mnohem elegantnější řešení je ponechat objet pro pattern zcela samostatný a pouze ho naučit ty metody, které jsou potřeba pro jeho funkci. Stejně jsou to ve své většině triviální metody, vracející nějakou hodnotu, typicky true/false, nebo nedělající nic. Byla–li by taková potřeba, může mít patternový objekt svůj služebný objekt, který mu bude poskytovat další potřebné metody.
Epilog
Současné programovací jazyky jsou velmi silně svázány s mechanismem dědičnosti, počítají s ním a jsou pro něj optimalizovány Snažit se tedy používat výše navržený princip jenom pro něj samý by tedy bylo dosti krkolomné, přesto si myslím, že se jedná přinejmenším o dobrý příspěvek k zamyšlení se nad technikami a možnostmi objektově orientovaného programování.