Exposing object sequences via public data interfaces (C++)
Posted: Mon Aug 06, 2012 9:58 pm
More and more often, I design my code around object sequences rather than around individual objects. Having a single interface that manages the whole sequence brings a lot of flexibility and optimization opportunities on the implementation side. That’s exactly where the usual OOP methodology fails as all the common OOP idioms deal with 1-1 object relations and blindly insist on per-object encapsulation.
So it’s time to move a bit towards DOD. Here, I’m going to show a table idiom I’ve been using quite often recently. The idea is that the table holds all the objects in a sequence indirectly addressed via index (or generally via handle). There’s no per-object interface, objects are manipulated through table’s interface using a bunch of setters. Each object consists of a public and private state. Both states are managed and kept in sync internally via the table. The best thing is that the full object’s public state can be obtained by a single get() call that returns a const reference to the state struct, this call is essentially for free and can be easily inlined. All the bookkeeping is atomically done in setters. See the example below to get the idea:
VIEW THE CODE BELOW IN FULL-SCREEN (public_data_interface_sample.cpp)
So it’s time to move a bit towards DOD. Here, I’m going to show a table idiom I’ve been using quite often recently. The idea is that the table holds all the objects in a sequence indirectly addressed via index (or generally via handle). There’s no per-object interface, objects are manipulated through table’s interface using a bunch of setters. Each object consists of a public and private state. Both states are managed and kept in sync internally via the table. The best thing is that the full object’s public state can be obtained by a single get() call that returns a const reference to the state struct, this call is essentially for free and can be easily inlined. All the bookkeeping is atomically done in setters. See the example below to get the idea:
VIEW THE CODE BELOW IN FULL-SCREEN (public_data_interface_sample.cpp)
Code: Select all
/*
(c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=727
*/
#include <vector>
#include <string>
#include <cstdio>
#include <cassert>
/// Represents a read-only entity state (a kind of public data interface).
struct Entity
{
const char *name;
int x, y;
Entity(const char *name, int x, int y) : name(name), x(x), y(y) {}
};
/// Entity table with just a single getter and multiple setters.
class Entity_table
{
public:
/// Adds a new entity, returns its index.
size_t add(const char *name, int x, int y)
{
assert(name);
_records.emplace_back(Entity_record(Entity_data(name, x, y)));
return _records.size() - 1;
}
/// Returns the size of the table.
size_t size() const { return _records.size(); }
/// Returns an existing entity (its whole read-only state).
const Entity & get(size_t index) const
{
assert(index < _records.size());
return _records[index].entity;
}
/// Sets entity name.
void set_name(size_t index, const char *name)
{
assert(index < _records.size());
assert(name);
const Entity_data &orig_data = _records[index].data;
_records[index] = Entity_record(Entity_data(name, orig_data.x, orig_data.y));
}
/// Sets entity position.
void set_position(size_t index, int x, int y)
{
assert(index < _records.size());
const Entity_data &orig_data = _records[index].data;
_records[index] = Entity_record(Entity_data(orig_data.name, x, y));
}
private:
/// Represents a private entity state (data holder).
struct Entity_data
{
std::string name;
int x, y;
// here might come more implementation details that are not exposed in Entity
Entity_data(const std::string &name, int x, int y) : name(name), x(x), y(y) {}
};
/// Contains entity and its data, it's responsible for keeping them linked and in sync.
struct Entity_record
{
Entity_data data;
Entity entity;
/// Initializes data, links entity to the data.
Entity_record(const Entity_data &data):
data(data),
entity(data.name.c_str(), data.x, data.y)
{}
/// Copies data, links entity to the new data.
Entity_record(const Entity_record &rhs):
data(rhs.data),
entity(data.name.c_str(), data.x, data.y)
{}
/// Copies data, links entity to the new data.
Entity_record & operator = (const Entity_record &rhs)
{
data = rhs.data;
entity = Entity(data.name.c_str(), data.x, data.y);
return *this;
}
};
std::vector<Entity_record> _records;
};
int main()
{
Entity_table entities;
entities.add("foo", 10, 20);
entities.add("bar", 30, 40);
for(size_t i = 0, n = entities.size(); i < n; ++i)
{
const Entity &e = entities.get(i); // atomic getter
printf("entity[%u] : name='%s', pos=[%d, %d]\n", unsigned(i), e.name, int(e.x), int(e.y));
}
// fine grained setters
entities.set_name(0, "FOO");
entities.set_position(0, 50, 60);
entities.set_name(1, "BAR");
entities.set_position(1, 70, 80);
for(size_t i = 0, n = entities.size(); i < n; ++i)
{
const Entity &e = entities.get(i); // atomic getter
printf("entity[%u] : name='%s', pos=[%d, %d]\n", unsigned(i), e.name, int(e.x), int(e.y));
}
return 0;
}
// output:
// entity[0] : name='foo', pos=[10, 20]
// entity[1] : name='bar', pos=[30, 40]
// entity[0] : name='FOO', pos=[50, 60]
// entity[1] : name='BAR', pos=[70, 80]