On the intricacies of affordance-based interfaces in C++
First, i've done a little bit of work in the GO programming language, and i really enjoyed the mechanics of it's non-inheriting interface polymorphism (i've called them affordance-based interfaces)
Next, i found this talk explaining how to do type erasure in C++ in order to essentially emulate these affordance-based interfaces:
I wrote a prototype of this system such that i could make a set of affordance classes that describe a specific member function, and might serve as a "delegate" for that function, and an interface class that could combine several of these affordance classes into a single type-erasing interface.
This part actually uses a bit of "regular" polymorphism to attain it's goal, but since i've written macros that generate all the required code for you, you can effectively ignore that it's there.
I've also used the opportunity to incorporate a system i've wanted in C++ for a while now: extension functions.
This means that if i have an object class Foo{}; and a function void bar(Foo& self); I would like to be able to call Foo{}.bar();.
This system can create an affordance for void bar(Foo& self); such that it becomes a member function for an interface accepting class Foo{};, letting you do just that.
Then, across various talks related to ruby and other programming languages i don't normally use, like this one:
I learned that there are very good reasons to have a separation between creation and use of an object, leading to my understanding what the builder pattern actually tries to describe:
The step-wise construction/initialization of a public mutable datastructure, such that it's contents can be "frozen" to form a final product that's ready for use.
Finally, i have been toying with ideas about analogies between code structures and biological systems, cell membrane being your public interface, membrane-bound proteins being member functions, etc.
In context of the above discoveries i set out to redesign the UDP/TCP socket system i had envisioned.
I eventually settled on using a primitive "true" datastructure containing just some public, mutable values with a "cloud" of free non-member functions
Which i could then combine "loosely" with my affordance-based interfaces into "pseudo objects"(the badly-drawn lines).
These pseudo-objects have the interesting property that they can allow you to construct an object that hides access to it's data while using only entirely public parts to do it.
Of course, internally the system just hides this stuff via accessor functions/inheritance, but that is itself an implementation detail you shouldn't really consider in usage ;)
The really neat thing about it, however, is that it means you can "invert" the interface, by manually creating an instance of your datastructure, and passing it into your public functions by hand.
This allows you to look into the "guts" of your builder if you're writing tests that are hard to show with regular usage.
For instance, in my case i've completely blocked read access during construction, so if i want to see the result, i need to "finish" the building process.
That somewhat clashes with TCP where i would want to test the result of winsock's select statement during the process of connecting the two sockets together.
By inverting the interface and constructing the contained datatype by hand, i can call free functions as i'm building up to the point where i want to test, and then i can look at the entire state of the object.
Doing it this way might also still be a good idea for "weighty" datastructures, since the affordance API does tend to copy your object every time a function is called (Perhaps there's some way to fix this, but i'm not too concerned about that right now)
Of course, this is probably only a good idea when you're testing an external API (EX: in this case, i'm not actually testing any of my objects, i'm testing how the select function works, or select, cause they may or may not do the same thing)
Comments
Post a Comment