[Video] Designing and Evaluating Reusable Components (Casey Muratori)
source: https://www.youtube.com/watch?v=ZQ5_u8Lgvyk
Types of code reuse #
- Layer
- Like OpenGL or Direct3D: reused code abstracts some service
- Engine
- New code is called by the engine and conformig to its rules, giving it information back. It is responsible for the rest.
- Component
- Similar to layer, but return their result back. Used in physics-, character animation systems. They do not interface with services.
Layers vs Components #
Layers require services they can abstract over. Different layers can also conflict if they access the same services.
Components do not suffer from this problem, as they do not access any services. They solely act on the data you use to call them and return their result.
Components are the most powerful form of subsystem reuse, but they are much harder to design
Library integration process #
Integration steps happen for additional requirements:
- We have to hit memory budgets, …
- We have to add features we did not think we have to add
- …
While developing, as time goes by the component is integrated more and more into the product. The possible levels of integrations are displayed here as circles. During development usually the next best level of integration is chosen.
However at some point there is a huge jump in integration work to progress. For example: There is no easy way to get streaming to work with the exiting integration of the API, even if the component supports it; the whole integration has to be reworked.
It is desireble for an component to always porovide small possible steps for possible states of integration
For example for the right side: You want to be able to use streaming but maybe then you also have to manage all the components memory (integration discontinuity).
When components have integration discontinuities, then for the next integration level, major parts of the integration might have to be rewritten.
Goal: Eliminate API discontinuities
Many libraries only provide a few levels of integration, but Casey argues, that we want many possible levels of integration and eliminate discontinuities.
Characteristics of libraries #
Granularity #
“A or BC”
If I have an API A I can replace it with two APIs B and C to do the same thing but give me a little more control over it.
Example:
Redundancy #
“A or B”
A and B do the same thing but the API looks different.
Example:
- A and B accept diffent types of arguments
- C and D express the same idea in different ways
- E and F combine calls of finer granularity in different ways
Coupling #
“A implies B”
If you do A in the API then you are also required to do B. Pretty much always bad but also often unavoidable.
Example:
- A
- Doing things to a lot of objects. -> cannot be separated
- B
- Setting some state can affect other actors depending on the state which might not be intended. Programmer has to be careful.
- C
- like gl_begin and gl_end
- D
- if GetMungedName always returns the same char*
- E
- Allocation is coupled to its initialization -> cannot be separated
- F
- API only accepts its own types so for calling it you have to always construct its types from your data
- G
- API makes you dependent on their file format. I cannot pass them the preloaded data.
Retention #
“A mirrors B”
The library retains state that you have to keep in sync.
- A
- The API retains some values
- B
- The API retains some relations
- D
- The API retains servies from the application itself
Flow control #
“A invokes B”
Who is calling who?
- A
- Game calls library
- B
- Game calls library wich calls game
- C
- Game calls library wich calls game which calls library
- A
- like model A above
- B and C
- Same thing (virtual functions are function pointers)
- D
- Unwind the stack
Summary #
Characteristics can change as time goes by:
Code samples #
Data from files #
A and B have coupling: You combined, reading the file with interpreting it. C does not suffer from coupling. But there could be more:
C could also couple decompression with interpretation. There are then variants where the API has callbacks to allocate and deallocate memory to decompress it itself. The final variant decompresses it itself and just calls the library with the decompressed data, back to no callbacks.
If the file data is transparent, you can also just read the thing yourself:
Not to say that one is better than the other but it is good to have many options to offer different levels of granularity
Parameter redundancy #
- A
- using libraries transform struct
- B
- not using libraries transform, using position and rotation instead
- C
- Worse than B: If my transform struct also has packed position and rotations, which first have to be unpacked only for the API call, so that it can be inverted, only to be then unpacked by the API again only to be repacked by the programm into its transform struct.
- D
- API Provides many different ways it can be called to minimize the chance that C has to happen
Granularity transitions #
Retention missmatch #
- A
- dream of a retained mode api
- B
- happens in retained mode libraries, programmer has to keep data in sync
- C
- Immediate mode (Additional feature: Pole could change between frames and it would work)
Summary #
- Always write the usage code first
- Incorporate the supposed new API into your project and see what you come up with
-> best first pass api
- All retained mode constructs have immediate mode equivalents
- for every api that uses callbacks or inheritance, there is an equivalent API that does neither
- No API requires the use of an API specific datatype for which the average target software has an equivalent type
- Ani API function your system may not consider atomic can be rewritten using between 2 and 4 (not counting accessors) more granular APIs
- Any data which does not have a reason for being opaque should be transparent in all possible ways (construction / access / …)
- Use of the component’s resource management (memory, file, string, etc) is completely optional.
- Use of components file format is completely optional
- Full run-time source code is available