µObjects: Where code meets Operating System
There's a couple types of places where the code we write interacts with the code someone else wrote. This could be another team, a 3rd party library, or the operating system. When we interact with these components, we're tying our code to their code in some fashion.
The more these libraries are referenced in our code; the tighter we're coupling our code to their code. The issue here is that ... Their code doesn't care about ours.
There's a concept I recently read about; but was innately utilizing known as Asymetric Marriage in chapter 32 of Robert Martin's Clean Architecture.
The basic of this is when you use a libraries code throughout your project you are heavily married to that library. It'll be hard to extract if you ever had to.
The library - It doesn't know or care about your code. It can change on a whim and ... you can't do anything about it.
I see a few types of places where out code is interacting with their code. In general; these are referrerd to as BookEnds.
The hardest to ignore; the Operating System. If we're not careful here; we're setting ourselves up to write firmware.
Operating System
I feel that the Operating System Abstraction is the simplest to accomplish. I do this by... wrapping the component. It will seem silly initally. I'll make an IFile
interface and an FileBookEnd
class. I tend to suffix the BookEnd's with BookEnd
. It makes it very clear that it has special considerations. The most frequent of which is that it can't be unit tested. To test it; we'd be testing the OS, or the language, or the platform. Because we can't unit test this; there can be no logic in it. FileBookEnd
simply delegates behavior to it's internal instance of the operating systems File
class. Super basic code that is guaranteed to function as long as the langauge and OS function.
The reason for BookEnding like this is to follow best practices. Code to the interface, not the implementation. In this case our code will use IFile
. Our code becomes far more testable when we don't have to actually use a File
object. I know there's ways to override the system behavior - It's a hack. A hack that exists because people wrote non-OO code. The systems and frameworks don't help us do good OO... I know; and it's sad.
While it's a lesser advantage; and probably not as frequent; but will ensure your software doesn't become firmware is when the OS changes behavior. Obviously SomeNix 5.1 isn't going to change the behavior of the default File Open command for itself; but if SomeNix 9.3 changed the default from creating the file if it didn't exist to throwing an error - Then your code will not be portable to the new version of the OS.
Assume you've used File
throughout your code. Every place you call
var file = new File(somePath);
file.write(someStuff);
you'll have to go edit to be
var file = new File(somePath);
file.create();
file.write(someStuff);
This is going to be a MASSIVE violation of the Single Responsibility Principle. A lot of files are all changing for the same reason; and it's not the "one" reason they should be changing.
Let's get out of that hell of change madness. We're back into our µObject Oriented world. We understand the value of creating an Operating System Abstraction Layer (OSAL) (thank you Clean Architecture). The OS can change. It's a much bigger decision but it can happen. Or you can be using an undocumented effect... There's a number of things that using an OSAL can save you from suffering through.
A former co-worker has a system stuck on a versions old OS because there was a change; and the code had no OSAL. It was tightly coupled and would take, currently, too much effort to update.
With the FileBookEnd
we have a single place to make the change to create the file first.
class FileBookEnd : IFile{
void write(string stuff){
file.create(Mode.Append);
file.write(stuff)
}
}
This would be the single place to change File based behavior; no other files would be touched. No searching or missing something... We have a place... A class with the single responsibility for encapsulating file behavior. It ensures we can migrate across any versions and keep the behavior our code expects - not whatever the OS decides to do.
For those still worried about class explosion... you're looking at the wrong blog. Good OOP will be many classes; but easier to maintain.
I use the File as an example; but any OS integration will start to tie your code to that version of the OS.
Android code is a huge offender; and their documentation and code samples support the bad coding practices. They're so far from OOP that it's disheartening... Anyway; Simplest example I have that every Android developer is going to know - Log
. If you're calling the Log
directly in your code; you're making it untestable. I have a wrapper to do this isolation. I wrote it before the OSAL concept was crystalized for my. It was required for me because I couldn't write unit tests otherwise.
There's some other simple examples, especially that change with the advancement of Android - AlertDialog
and Toast
. Calling these directly is coupling your code to a specific OS implementation. If you start to put the "API checks" around things in your code - you're doing it wrong.
Encapsulate the dependency.