Interfaces
When dealing with a complex relational schemas you often end up with hierarchies of objects. For example a Person
object mightbe either a Student
or a Teacher
object or even none of them. But they all share common charaterstics like a name
and maybe a function that increaseSalary()
. The basic way would be to duplicate all those informations in each class, but it is not a proper way to do it because for example you might want to view all of those Student
and Teacher
objects as Person
objects.
Since Zig does not support inheritance, we are going to craft our own way to do it with the magic of pointers.
Everything I am going to show you is based on this great article. So if you have any doubt about what I am doing here, you should check it out.
In my project I have a few cases where it could be a good idea to implement those interfaces. Notably for my events
.
In my project there is a function where I poll for all the non-resolved events
and check if they are overdue or not. Before implementing the interfaces I could not iterate over all the events and call a function on them that would resolve the event. What I had to do is poll all the battles
, all the ressources_transfers
and so on if I happen to have more events
in the future. Which caused a lot of duplication.
With this way of doing things I had to implement a getAllXInOrder()
then iterate over all the X and call their own resolve()
function and check a few other things like if the battle was already resolved or not.
const allBattles = try Battle.getAllBattlesInOrder(db, allocator);
for (allBattles) |*battle| {
// If the battle is over
if (battle.duration + battle.time_start < std.time.timestamp()) {
if (battle.resolved == false) {
// Resolve the battle
try battle.resolve(db, allocator);
}
} else {
break; // this battle is not over so the next ones wont be either
}
}
With the newly implemented interfaces I can simply get all the next events that are not resolved and call executeEvent()
which is going to execute the specific function of the event under the hood.
const events = try Event.getAllTheNextEvents(db, allocator);
for (events) |event| {
if (event.getRemainingTime()) {
try event.executeEvent(db, allocator);
} else {
break; // this event is not over so the next ones wont be either
}
}
How I have implemented this is by first having an Event
struct that is going to be our parent "class". Which basically look something like this. Where I simply have a pointer ptr
to the child object which might be a Battle
or a RessourcesTransfer
and an executeEventFn
which also is a pointer to the specifi resolve function of either our Battle
or RessourcesTransfer
.
The rest are just the functions that can be used for any event
, like getting the ammount of time left with getRemainingTime()
.
const Event = @This();
id: usize,
time_start: i64, // nb of seconds between 1970 and the end of the event
duration: i64,
ptr: *anyopaque, // pointer to the child event (used for heritage)
executeEventFn: *const fn (ptr: *anyopaque, db: *sqlite.Db, allocator: std.mem.Allocator) anyerror!void,
pub fn getRemainingTime(self: Event) !i64 {
// negative value means the event is over
return std.time.timestamp() - self.time_start;
}
pub fn executeEvent(self: Event, db: *sqlite.Db, allocator: std.mem.Allocator) !void {
return self.executeEventFn(self.ptr, db, allocator);
}
pub fn getAllTheNextEvents(db: *sqlite.Db, allocator: std.mem.Allocator) ![]Event {
// Helped by ChatGPT
const query =
\\
\\ WITH CTE AS (
\\ SELECT
\\ event_ressources_transfer_id,
\\ event_battle_id,
\\ COUNT(*) OVER() AS total_count
\\ FROM
\\ events
\\ LEFT JOIN
\\ battles
\\ ON
\\ battles.event_battle_id = events.id
\\ LEFT JOIN
\\ ressources_transfers
\\ ON
\\ ressources_transfers.event_ressources_transfer_id = events.id
\\ WHERE
\\ resolved = 0
\\ ORDER BY
\\ (time_start + duration)
\\ )
\\ SELECT
\\ event_ressources_transfer_id,
\\ event_battle_id,
\\ total_count
\\ FROM
\\ CTE;
;
var stmt = try db.prepare(query);
defer stmt.deinit();
const EventsIds = struct {
event_ressources_transfer_id: ?usize,
event_battle_id: ?usize,
total_count: usize,
};
const raw_interfaces_events = try stmt.all(EventsIds, allocator, .{}, .{});
var totals: usize = 0;
for (raw_interfaces_events) |event| {
totals = event.total_count;
break;
}
const events = try allocator.alloc(Event, totals);
for (raw_interfaces_events, 0..totals) |unkown_event, i| {
if (unkown_event.event_battle_id) |battle_id| {
const b = try Battle.initBattleById(db, allocator, battle_id);
events[i] = b.event();
} else if (unkown_event.event_ressources_transfer_id) |ressources_transfer_id| {
const r = try ResourcesTransfer.initRessourcesTransferById(db, allocator, ressources_transfer_id);
events[i] = r.event();
} else {
return error.UnkownEventType;
}
}
return events;
}
The child "objects" have two things to implement, a function that return an Event
whit the pointers pointing to the child object himself. And the function it wants to override, which in our case is executeEventFn()
that is going to resolve the event when time is due.
const RessourcesTransfer = @This();
// Ressource transfer specific fields
giver_village_id: usize,
receiver_village_id: usize,
golds_given: u64,
// Events general fields
event_ressources_transfer_id: usize,
time_start: i64,
duration: i64,
resolved: bool,
// Create a point of view of Event for the Battle
pub fn event(self: *RessourcesTransfer) Event {
return Event{
.id = self.event_ressources_transfer_id,
.time_start = self.time_start,
.duration = self.duration,
.ptr = self,
.executeEventFn = executeEventFn,
};
}
// Overrided function
pub fn executeEventFn(ptr: *anyopaque, db: *sqlite.Db, allocator: std.mem.Allocator) !void {
const self: *RessourcesTransfer = @ptrCast(@alignCast(ptr));
const target_village = try Village.initVillageById(db, allocator, self.receiver_village_id);
target_village.gold += self.golds_given;
try target_village.persist(db);
self.resolved = true;
try self.persist(db);
return;
}
This design pattern allowed me to have cleaner code and better separation of concerns. I can now iterate over all the events and call the executeEvent()
function on them without having to worry about what kind of event it is. So even in Zig some kind of object oriented programming features are possible to handcraft.