PDA

View Full Version : [opinion][Research] API design



Ñuño Martínez
04-06-2021, 10:17 AM
So, I’m rewriting my engine and I’m struggling with API names.

I was using prefixes and suffixes to differentiate the engine and subsystems stuff. For example, this program for an hypothetical action game:


program Game;

uses
Mingro, mngSprites, Title, Playfield, sysutils;

function Initialize: Boolean;
begin
if mngInit and mngInitializeSprites then
begin
PlayerSpriteSheet := mngLoadSpriteSheet ('player.spr');
if PlayerSpriteSheet = Nil then
Exit (True)
else begin
mngLog.Trace (etError, 'Can''t load sprite sheet.');
Exit (False)
end
end
else begin
mngLog.Trace (etError, 'Can''t init engine!');
Exit (False)
end
end;

begin
if Initialize then while RunTitle do RunGame
end.

What I was thinking is to use the unit name as namespace, so the previous program will look like this:


program Game;

uses
Mingro, mngSprites, Title, Playfield, sysutils;

function Initialize: Boolean;
begin
if Mingro.Initialize and mngSprites.Initialize then
begin
Playfield.PlayerSheet := mngSprites.Load ('player.spr');
if Playfield.PlayerSheet = Nil then
Exit (True)
else begin
Mingro.Log.Trace (etError, 'Can''t load sprite sheet.');
Exit (False)
end
end
else begin
Mingro.Log.Trace (etError, 'Can''t init engine!');
Exit (False)
end
end;

begin
if Initialize then while Title.Run do Playfield.Run
end.

I don’t see that kind of API anywhere but I think it is clean and clear and more appealing than using prefix/sufix to differentiate between subsystems (except the fact that when two units declare an object with same name and you don’t precede it with the unit name, FPC doesn’t warns and picks one resulting in some funny debug sessions).

What do you think about this kind of API?

SilverWarior
04-06-2021, 01:38 PM
It is hard to say without having access to your entire codebase.

If you have each Subsystem in separate unit then your second code example actually seems more intuitive as you get clear information on which unit has defined certain object type. It also gives you ability to have different units for different platforms.

But if you ever decide to put your game engine into a single package I don't think such approach would be quite suitable. I'm not even sure if this would even work if built into a single package.

AthenaOfDelphi
04-06-2021, 07:34 PM
Naming conventions... ARGH!

I would say do whatever makes it work for you. We adapt to the different naming/organisation conventions and I personally don't really have a preference. I've tried numerous approaches and concluded some time in the future that they were all clunky or less than optimal. Now I just do what feels right at the time, although I don't publish a lot of code.

Ñuño Martínez
05-06-2021, 09:47 AM
If you have each Subsystem in separate unit then your second code example actually seems more intuitive as you get clear information on which unit has defined certain object type. It also gives you ability to have different units for different platforms.That's just why I asked. Since I put each Subsystem in separate unit I feel it is natural to use the unit name instead of prefixes and suffixes. I just wonder why it isn't used a lot.


Naming conventions... ARGH! Yeah. :(


I would say do whatever makes it work for you. We adapt to the different naming/organisation conventions and I personally don't really have a preference. I've tried numerous approaches and concluded some time in the future that they were all clunky or less than optimal. Now I just do what feels right at the time, although I don't publish a lot of code.That's my fear: What if it is clunky. I know it may be less than optimal since you have to write more, and it would be prone to mistakes if calling a procedure that has the name duplicated in several units without the unit name (i.e. the "Init" method). If there were a way to force to write the unit name...

SilverWarior
07-06-2021, 11:13 AM
After thinking some more on this topic I must admit that I'm becoming less and less inclined to your second approach of using using unit names instead of prefixes. Why?
As you pointed out yourself calling certain method that is implemented in multiple units with same name without providing unit name will lead to unexpected behavior. I'm not sure about FPC but in case of Delphi I believe the method from the unit that was last loaded would be called in such scenario.
Another disadvantage of such approach is that you are basically forcing yourself to have each system in its own unit. While often this is perfectly fine there are times were you might want to have several connected systems in a single unit in order to improve code readability.
Wouldn't this also mean that you will have to ad every unit into uses section of your main application unit?
Also having bunch of small units could have significant affect to build time. Now wile Object Pascal compilers are know to have very fast compile times due the fact that only modified code is recompiled having bunch of small units could still lead to long build times. This mostly due the fact that linker still needs to link a bunch of small compiled code pieces together in order to build final application.

Ñuño Martínez
29-06-2021, 07:45 AM
You have some points here.

I was coding some time now and besides the build time it seems nice to me. Thankfully Lazarus is able to rename all references by itself, or I can use a tool I wrote some time ago to help with Allegro.pas development, so if I change my mind I can change all identifiers. It isn't TRUNK yet.

Ñuño Martínez
04-07-2021, 10:50 AM
Real life strikes back.

I was working on the disk access API, and I've found some of the issues you, SilverWarrior, were talking about. Of course, using the unit name fixes all problems, but I spend some valuable time wondering if TStream should be the original Classes.TStream or the one I extended...

So I've decided to back to the prefix. It was a nice experiment though.

SilverWarior
04-07-2021, 07:31 PM
but I spend some valuable time wondering if TStream should be the original Classes.TStream or the one I extended...

One piece of advice. If you are extending some class with new functionality it is recommended to change the name of the extended class in order to avoid confusing it with the original class that it was extended from.
That is unless you want your extended class to be used as a base class for other classes that have been extended from the original class and thus in a way inject additional functionality to all of them.

drezgames
07-07-2021, 06:10 AM
Maybe this:
<Action><Object><Attribute/State>();// Do an Action over some Object Attribute/State
This is how I tend to make my APIs, more info about this here:
raylib syntax analysis · raysan5/raylib Wiki (github.com) (https://github.com/raysan5/raylib/wiki/raylib-syntax-analysis)

Chebmaster
13-07-2021, 07:40 AM
The second one BUT use classes with class (static) methods instead of unit names.
This way you *cannot* call method without specifying which.
And can pack several ones in the same unit
And move them between units if you need to change architecture -- without touching the calling code

in my engine:
GAPI.TexImage2d(p, format)

where
class function GAPI.TexImage2d(
image: pointer;
format: TGAPITextureFormat;
level: GLuint = 0;
width: GLuint = 0; // 0 = get from image
height: GLuint = 0; // 0 = get from image
CreateTextureObject: boolean = false
): GLuint; //no return value unless CreateTextureObject is true

Ñuño Martínez
03-08-2021, 11:31 AM
There are a lot of nice advices here. Thanks to all. :)


One piece of advice. If you are extending some class with new functionality it is recommended to change the name of the extended class in order to avoid confusing it with the original class that it was extended from.
Yes, I've learned that the hard way. :'(

The injection thing is interesting but I think I'll not do that way.


Maybe this:
<Action><Object><Attribute/State>();// Do an Action over some Object Attribute/State
This is how I tend to make my APIs, more info about this here:
raylib syntax analysis · raysan5/raylib Wiki (github.com) (https://github.com/raysan5/raylib/wiki/raylib-syntax-analysis)
That's a nice way. Maybe I should review my API now I'm (re)starting it.


The second one BUT use classes with class (static) methods instead of unit names.
This way you *cannot* call method without specifying which.
And can pack several ones in the same unit
And move them between units if you need to change architecture -- without touching the calling code

I didn't realize I can do that. I did in some PHP projects but forgot completely there are also static class methods and properties in Pascal. Since I'll revisit my API I'll see how this may fit with the old-school I want to archieve.

Ñuño Martínez
07-08-2021, 12:27 PM
Well, I was working in this new API Chebmaster suggested and I must say I like it. It isn't as classic as I initially intended (pure Pascal, nothing classy) but it looks nice and is easy to read and maintain.

There are some quirks. For example, IMO this looks a bit odd:


var
AnimationInfo: TmngAnimationInfo; { A record. }
Animation: TmngAnimation; { Another record. }
begin
...
{ Gets animation information. }
mngAnimation.Assign (Animation, AnimationInfo)
...
end;


So it forces me to add methods to the records:



var
AnimationInfo: TmngAnimationInfo; { A record. }
Animation: TmngAnimation; { Another record. }
begin
...
{ Gets animation information. }
Animation.Assign (AnimationInfo)
...
end;


I think I can live with it, specially because the disk access (including an IFF implementation) is done by thread classes.

I'm also wondering if type declarations should be inside the static classes like:



var
AnimationInfo: mngAnimation.TAnimationInfo; { A record. }
Animation: mngAnimation.TAnimation; { Another record. }
begin
...
{ Gets animation information. }
Animation.Assign (AnimationInfo)
...
end;


Anyway there are a few functions and procedures I'll keep outside the static classes (for example, the mngInitialize function that initializes the engine).