Agreed… macOS seems to have avoided the massive fragmentation problems plaguing Linux and Windows.

It’s not perfect… the iOS ecosystem is just different enough that common frameworks don’t always go up to the UI layer (UIKit and AppKit are different, although most of their dependencies are the available on both platforms), and that sometimes manifests itself in slightly odd behaving apps in macOS (Messages, News, Stocks, etc are just UIKit apps recompiled for the mac), but it still feels like the different stacks share enough similarities, that from a developer’s perspective it doesn’t feel too jarring.

I think this can be attributed to the fact that Objective-C was such a perfect design choice for its time. While Microsoft was busy being ashamed of COM and spent a lost decade chasing .NET as the New One Way to create windows apps, only to double back and shore up COM again through the WinRT stack, Apple (post-NextStep) had that problem fully solved with Objective-C. While MS’s environment needed a separate IDL and object model to allow ABI-resilient dll’s (C++ is not ABI stable, it needs COM to do that), Objective-C solves this problem naturally through its fundamentally dynamic nature.

Examples:

- In C++, you can’t just construct an object that’s defined in another .dll. You may know its size now, but you can’t be sure that its size won’t change in the future, and you may not be in a position to recompile your code (say, when a new windows version comes out and your customer tries to install your program.) So to allocate an object, you have to ask COM to do it, which means following the relevant conventions and using COM API methods to ask the remote class to create an instance of itself (CoCreateInstance, etc), and refcounting the resulting pointer with AddRef/Release. This isn’t a bad design choice, but it’s just different from how C++ programmers are used to constructing objects.

Contrast this with ObjC, where `[[SomeClass alloc] init]` is always the way you construct objects, and there’s no jarring change between “how you allocate objects defined in another framework” and “how you always allocate objects”. And refcounts are automatically managed by ARC, which is part of the ObjC compiler.

- COM works by having objects query each other’s interfaces and exchange vtables around so that the function offsets are known, which requires special calling conventions that are different from how normal C++ code works… You’d need to call QueryInterface on a COM object to get a pointer with the right vtable, so that you can call the right methods on it, etc. Various high-level wrappers (MFC, ATL, etc) abstract this from you, but they’re imperfect, and the right wrapper depends on the use case. Not to mention, you only get access to the COM APIs which are actually exposed via these wrappers, so they won’t help you with “general purpose” COM code. I have been out of that game for a while but I’m assuming WinRT has made a lot of this simpler.

Contrast this with ObjC, which simply always uses dynamic dispatch with objc_msgSend to actually invoke methods on objects. It’s “ugly” (`[[[lots of] brackets] etc];`), and “slow” (because the method dispatch is essentially passing a string around and dynamically discovering which code will handle it) but it’s at least uniform.

COM was unwieldy to non-microsoft engineers, but Microsoft mostly “solved” this by creating friendlier programming interfaces on top of it. They didn’t really want engineers being confronted with COM’s complexity, and so technologies like MFC, ATL, (OG) Visual Basic, and then later .NET were touted as solutions that had a better developer UX. But that’s just the problem… there are too many solutions to this problem in Windows, and nobody could agree on the “right” way to write windows apps, so everyone either did some flavor of either raw win32 programming (if you’re up for doing a lot of work yourself), or some rotating framework du jour which would hide the complexity for you.

But on Apple’s platforms there was no such awkwardness, and Apple just kept innovating. ObjC’s internals were not some implementation detail to be hidden but the fundamental way you do all object-oriented programming on the system, so there’s no reason to have different conflicting wrappers on top of it. Developing for AppKit was just plain old ObjC, and you get ABI resilience for free because of its dynamic nature.

Anyway this was a long rant but I really do feel like the lowest level technologies are to blame/praise for the situation with OS software in general, and COM was such a sore spot for microsoft that their attempts to paper over it just made the platform worse and fragmented things, leaving developers to fend for themselves with how to develop applications.

Addendum:

I’m actually sorta worried about Swift as the replacement for ObjC, because it’s not as “fundamentally dynamic” as ObjC is. Apple is aware of this, and has gone to great lengths to enable library evolution/resiliency in Swift frameworks, but they’re coming at it from a totally different place from how ObjC solved it. In ObjC, it’s just `objc_msgSend` all the way down. In swift you have value witness tables, protocol witness tables, reabstraction thunks, and all this massively complex infrastructure to make it feel like objects in another Framework are the same as the objects you define locally, and it all sorta works, but… it’s just not nearly as elegant. It’s technically faster though (since method dispatch through these witness tables is likely faster than objc_msgSend), so at least it was done for a reason. But I’ll always have a special place in my heart for ObjC due to just how perfectly it fits into the problem space.