We organize our state using objects.
Many programs have to deal with complex mutable state. The extreme cases are OS kernel state, or game state in an AAA videogame. For such programs, OOP is the only approach allowing people to reason about what’s going on inside these things.
If you don’t have complex mutable state, other approaches might become more practical. One example is compilers; their state is mostly immutable. Another is pure computations without state, especially symbolic computations, like what happens in Maple.
> the thread associated with the input travels through your code, going deeper, then shallower, deeper again
In many programs, input handling is minor part of overall software complexity. Sometimes, most of the code don’t even run on the input thread.
> decouples one set of statements from another, and makes it easier to substitute (or slide) out the second set.
Interfaces abstract away arbitrary stuff (statements, data, hardware devices, etc.), providing strongly-typed API to use that stuff, and hiding complexity of the implementation. In some case (COM interfaces), the statements you’re selecting were written in another language, are implemented in another DLL being loaded on demand, run in different process that starts up on demand, or for DCOM even run on different machine.
> since we are strongly-typed, the factory is only selecting from a finite set
In C#, some class factories take your strongly-typed interface, take other parameters you pass to the factory, and implement the interface in runtime. Because there is infinite set of possible class-factory parameters, that makes the set of statements infinite as well. BTW, this approach is often used in .NET RPC libraries, like WCF.
Unfortunately, OOP doesn't allow people to reason about complex systems, primarily because call-chains are totally unconstrained. An object can contain references to any other object held by the process, and this reference list is dynamic at runtime. In the presence of instantiation indirection it's even worse, even the concrete type of those references is unknown except at runtime.
There are code smells, and then there are architecture smells. One big architecture smell is when the incidental complexity of a solution vastly outstrips the intrinsic complexity of the problem. I have never (and I've been doing this a very long time) seen an OOP code base less than 10x the complexity of the business problem it was built to solve. It's sad to me that instead of asking why, they keep blaming the programmer/architect and inventing new complexity to mitigate old complexity.
The overall fraction might be small, however vast majority of software still contain substantial amount of code that does. Most serialization and DB access libraries do. For C#, this includes all framework-provided serializers (XML, data contract, etc.), the ubiquitous Json.NET, and my own one: https://github.com/Const-me/EsentSerialize
> OOP doesn't allow people to reason about complex systems
Respectfully disagree, ‘coz I have never saw complex systems that aren’t OOP. Even most compilers are OOP, take a look: https://github.com/llvm-mirror/llvm https://github.com/gcc-mirror/gcc/tree/master/gcc https://github.com/dotnet/roslyn
> An object can contain references to any other object held by the process
This is true for very small class of objects, like GUI controls with loosely typed event handlers, or objects that hold lambdas/functions that may capture anything at all (but the latter is not strictly OOP).
An object can only reference other objects of types that can be stored in its properties/fields. For many objects especially simple lower-level ones, this means objects can reference no other objects at all.
> have never seen an OOP code base less than 10x the complexity of the business problem it was built to solve
My guess is, you’re systematically underestimating complexity of the business problems solved by the code you look at.
In every case, codebase complexity was dominated by administrative overhead. My passing experience with apps written in C#, ASP, and PHP, tell me that it is the same story there, too.
This is my subjective view. One way you might understand my perspective (and perhaps even share it, to some degree) is to consider the act of adding a column to a database table, and how a typical OOP/RDB webapp codebase must change to incorporate a new DB value. (And this is only the tip of the iceberg, since usually adding a feature requires a view, an index or two, plus column and table changes.)
As you see, almost nothing common with your background.
For the kind of software I work on, if the code is 10x more complex than necessary, the software might fail the performance targets. Complex code is also prohibitively harder to maintain and debug. And esp.in embedded, quality requirements are just higher: web requests come and go, but embedded software needs to reliably work for days without restarts.
I think the reason why I’m generally OK with other people’s OOP-style code I see in the projects I work on, is that code is just better then what’s in an average web.app. Especially if the web app ain’t Google or Facebook, AFAIK for these guys, software quality translates to substantial saving on electric bills.
In the steady-state, your process handles input as a method invocation (the main(String[]) of a CLI program or the handleRequest(req) method of a servlet, for example). In both cases, the thread associated with the input travels through your code, going deeper, then shallower, deeper again (the depth is the stack frame count).
Each time we go deeper (each time we conditionally invoke a method) we are solving a combinatorial problem across our own code-base. We are effectively crafting, at runtime, a unique program (concrete list of statements) to deal with this input. We don't normally learn OOP this way, or think of it this way, but it really is what we are doing. But there are some benefits to thinking in this way.
For example, what is an interface? Like all indirection, it's like lubricant. It decouples one set of statements from another, and makes it easier to substitute (or slide) out the second set. And a factory? Well, it produces bundles of statements that conform to an interface. But since we are strongly-typed, the factory is only selecting from a finite set. (In fact, the factory pattern is a perfect example of how OOP practitioners are doing combinatorics all the time, but we don't know it, and we do it crudely.)