Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cleancodeqa-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ In my experience, these two things cost a lot of programmer cycles. I prefer to
I don't know if "abstracting file IO" is the best example to pick, but it was the first one you brought up, and it _happens_ to contrast the two designs enough in my opinion, so it works for me. Here's why I think an enum-based design saves the programmer(s) cycles:

* In most systems, we do not know all the functions that we are going to do ahead of time. When operating across a hard boundary like a driver, using operation codes instead of virtual function calls allows us to add functions dynamically without recompiling all of our drivers. If we want, for example, to add a new function like "trim" (such as what happened with real IO protocols when SSDs happened), if we are just passing opcodes to a driver, we don't have to recompile _anything_. All the old drivers just ignore the code, whereas the new drivers act on it, which is what we want. In the class case, either all the drivers have to be recompiled, or we have to write a utility class which stubs the new function, and wrap all the old drivers in that utility class. If we don't do this, the vtables for the old drivers will all be the wrong size, so we can't use them.
* In any modern system, multithreading is a concern, but this is especially true for an operating system. Having the protocol be structure-based, with an operation code, allows us to trivially buffer operations in things like io rings or other intermediaries _without writing any new code_. The entire system remains identical, and we need only add the new buffer logic. In the class system, we don't have any way to represent function operations with data, so wse must _introduce_ a data format for storing the calls - which of course will end up looking basically like the code for the non-class version, so you end up having to write my version _as well as_ the class version. This, by the way, seems to happen in almost _all_ OOP systems I see, because eventually they need to serialize or something similar, and so they have to write my version _as well as_ their version, but they don't seem to realize how much time they're wasting this way!
* In any modern system, multithreading is a concern, but this is especially true for an operating system. Having the protocol be structure-based, with an operation code, allows us to trivially buffer operations in things like io rings or other intermediaries _without writing any new code_. The entire system remains identical, and we need only add the new buffer logic. In the class system, we don't have any way to represent function operations with data, so we must _introduce_ a data format for storing the calls - which of course will end up looking basically like the code for the non-class version, so you end up having to write my version _as well as_ the class version. This, by the way, seems to happen in almost _all_ OOP systems I see, because eventually they need to serialize or something similar, and so they have to write my version _as well as_ their version, but they don't seem to realize how much time they're wasting this way!
* If at some point we decide that users should be able to do multithreaded/bulk IO ops, with the enum version there are _zero_ changes to the internals and the drivers. All we do is add whatever user-land interface we want for this (presumably a circular buffer they write to), and everything "just works", with no translation necessary because the internals already worked on data instead of vtables/function calls anyway.
* If we would like to allow 3rd parties to have private communication channels for their devices, where user-level code can do IO ops that are only defined on those devices, there is no way to do this if we stick with the idea that each operation has to be represented by its own virtual function. It requires a complete side-channel implementation that has to be created specially and deployed by each vendor separately, because we have no idea how many functions each device will need or what they will be called. With the enum version, it's trivial. Just reserve half the opcode range for private stuff, and let anything in that range pass through to the device. Now the vendor ships an enum table to the end users, or an SDK with function calls that write those enums for them if you prefer, and that's it. Whether or not you'd actually _want_ to do this in an OS is questionable, because you're trusting the driver more here, but the point is the architecture _makes it trivial_. Whether or not you should allow it on security grounds is another question, and may depend on the type of OS.

Expand Down