.oisyn schreef op donderdag 06 juli 2006 @ 02:29:
[...]
Hier wil ik dan wel weer even op inspringen door te zeggen dat het misschien niet altijd even netjes is maar wel bloedje efficient is. Globale variabelen (wat singletons effectief zijn als je de resulterende assembly code bekijkt) kosten vrij weinig en zijn snel te accessen.
Het hangt er natuurlijk een klein beetje vanaf hoe je je singleton precies opzet. Dikwijls worden singletons via een factory pattern verkregen. Dat implementeer je typisch door een object van alleen non-public ctor's te voorzien en een public method getInstance() die een object van z'n eigen type terug geeft. Dit object is voor de gebruikers ervan een gewoon object en alle calls erop zijn in Java gewoon virtual. Er zit geen speciale performance winst in deze variant (tenzij je anders 1000'den keren een instantie van dit object had moeten aanmaken, maar dit is niet echt typisch om te doen).
De andere variant is om in Java static methods te gebruiken. Static methods zijn nooit virtual en er staat ook een aparte opcode voor (invokestatic vs invokevirtual). Dit zou in Java theoretisch een performance winst kunnen opleveren, maar vanwege de run-time optimalisaties die de VM doet kun je op voorhand niet zeggen naar welke cpu assembly deze twee high-level opcodes uiteindelijk gaan worden omgezet (hangt mede van het feit af of de code in kwestie een hotspot wordt tijdens run-time).
Ik kan me wel goed voorstellen dat voor commerciele games het in C++ inderdaad de moeite waard is om singletons op de manier te gebruiken zoals .oisyn verteld.
In business software speelt dit echter een stuk minder. Bekende situaties waar een singleton wel bewezen op z'n plaats is, is bijvoorbeeld als er sprake is van een job scheduler. Hier gebruik je geen singleton omdat je dan zo makkelijk erbij kunt en absoluut niet omdat dan de method calls op de scheduler een nanoseconde minder duren, maar simpelweg vanwege het feit dat er logisch maar 1 scheduler mag bestaan (zie bv Quartz). Een ander bekend voorbeeld vanwege dezelfde reden is een driver voor iets.
Een soort variant op de singleton die je ook nog wel eens ziet is een soort semi-singleton. In dit geval is er niet 1 globaal 'ding', maar is er 1 instantie per thread. Deze maken dan gebruik van een factory icm TLS ipv statics. Dit kan handig zijn bij frameworks en event-handling. Het framework zet dan een context object in TLS en roept de event handler code aan. Deze heeft dan ook vele methods diep nog makkelijk toegang tot de 'context'. In Java EE is de JSF context daar een voorbeeld van. Het alternatief is overal dit context object doorgeven. Omdat de handler code (verwar niet met de business logic die deze weer oproept) toch al framework specific is, is een well known dependency hier niet (zo) erg.
In vele andere gevallen zijn singletons in business software niet zo wenselijk.
Een bekend 'fout' voorbeeld is bv business logic die een DB connectie uit een singleton haalt met static methods. Dit gaat namelijk fout bij unit testen en gaat ook fout als je die code in een ander project wil gebruiken waar de DB connectie op hele andere wijze verkregen wordt. Het punt hier is dat object een black-box is met harde dependencies ergens heen. Als er een static call ergens midden in een functie staat, kun jij extern dat object niet bewegen om z'n connectie ergens anders vandaan te halen.
Een ander zo'n bekend voorbeeld is een singleton (static) settings object hebben. Dit zie je ook hele volkstammen van programmeurs doen. Er is binnen 1 project wel sprake dat er logisch maar 1 settings object is, maar de fout zit hem er hierin dat vele programmeurs dit object dan gebruiken om zelf hun object intern te configgen. Binnen dat ene project werkt dit wel, maar ook hier gaat dit dan fout zodra je deze code wilt unit testen of in een ander project wilt gebruiken wat zijn settings op een hele andere manier afhandeld.
Behalve unit-testen en re-use hebben singletons nog het nadeel dat het snel code op kan leveren die niet thread-safe is. In multi-threaded omgevingen (is Java EE eigenlijk automatisch) moet je dus extra goed opletten dat alles bewaakt wordt met iets dat mutual exclusion garandeerd voor alles dat een veranderbare state heeft.
Als elk object in je game referenties moet gaan houden naar verschillende systemen (Game, RenderDevice, ResourceLoader, WeetIkNietWat) kost dat meer mem, wat op z'n beurt ook weer meer cache-misses en dus tragere code oplevert.
Dat hangt er natuurlijk vanaf of al die referenties 'toevallig' naar 1 en hetzelfde systeem wijzen. Als dat zo is zal het niet meer memory kosten, maar heb je nog wel de flexibiliteit om waar nodig een object een andere referentie te laten gebruiken. Je hebt dan practisch voor elk systeem nog steeds maar 1 object zonder dat je de gebruikende objecten afdwingt 1 specificieke class te gebruiken.
Het werkt ook goed tegen onnodige dependencies. Iemand die een character instantieert is niet geïnteresseerd in de renderer, terwijl dat object zichzelf wel moet kunnen tekenen.
Ik snap wat je bedoeld. Aan de andere kant wordt het voorbeeld wat jij noemt vaak juist een dependency genoemt. Character is in jouw geval tightly coupled aan een specificieke renderer. Het zal concreet niet spelen, maar wat als jij nu als gebruiker van character een andere renderer wil gebruiken?