Modern Programming: Object Oriented Programming and Best Practices
上QQ阅读APP看书,第一时间看更新

Designing an Object

The object-oriented approach attempts to manage the complexity inherent in real-world problems by abstracting out knowledge and encapsulating it within objects. Finding or creating these objects is a problem of structuring knowledge and activities.

Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener, Designing Object-Oriented Software

An early goal of OOP was to simplify the work of software system design by reducing the big problem "design this large system to solve these problems" into the small problems "design these small systems" and "combine these small systems such that they solve these problems in concert". Brad Cox, an object technologist who built the Objective-C language and cofounded a company to exploit it, wrote an article "What if there's a Silver Bullet...And the Competition Gets It First?" in which he asserted that OOP represented a significant reduction in software complexity.

In the broadest sense, "object-oriented" refers to the war and not the weapons, the ends and not the means, an objective rather than technologies for achieving it. It means orienting on objects rather than on processes for building them; wielding all the tools programmers can muster, from well-proven antiques like Cobol to as-yet missing ones like specification/testing languages, to enable software consumers, letting them reason about software products with the common-sense skills we all use to understand the tangible objects of everyday experience.

It means relinquishing the traditional process-centered paradigm with the programmer-machine relationship at the center of the software universe in favor of a product-centered paradigm with the producer-consumer relationship at the center.

Nonetheless, many "object-oriented" design techniques still rely on considering the system as a whole, building artisanal, bespoke objects from scratch that will comprise the system that satisfies the customer's needs. In this sense, Cox's vision has not come to pass: he hoped for the "software industrial revolution" in which standardized components (software-ICs, analogous with integrated circuits in electronics design) could be specified based on their externally visible behavior and composed into a system relevant to the task at hand. Rather, we still have a craft industry, but now the application-specific components we build every time are called "objects."

This approach – designing a whole system as a single software product but calling the bits "objects" – goes under the name of Object-Oriented Analysis and Design. Typically, it is expressed as a way to decompose big problems according to the data used to solve the problem, so that OOP becomes an "alternative" to functional programming, in which the big problem is decomposed according to the operations used in its solution. An uncaptioned table in "Using Functions for Easier Programming" by Neil Savage — https://dl.acm.org/citation.cfm?id=3193776 from 2018 describes the term Object-Oriented:

The central mode for abstraction is the data itself, thus the value of a term isn't always predetermined by the input (stateful approach).

The term Functional programming is described as:

The central mode for abstraction is the function, not the data structure, thus the value of a term is always predetermined by the input (stateless approach).

Never mind that "functional" languages like Haskell have mechanisms designed for handling state, or that plenty of problems we might want to solve in the world have both stateful and stateless aspects!

This idea of objects-as-data does have its roots in the OOP movement. In his textbook "A Touch of Class" from 2009, in Section 2.3 "What is an object?", Bertrand Meyer uses the following definition:

An object is a software machine allowing programs to access and modify a collection of data.

This is in exact opposition to the usual goals of "encapsulation" or "data hiding" that we have heard about, in which we try to forbid programs from accessing and modifying our data! In this view, we have the object as a "software machine," which is good as it suggests some kind of independent, autonomous function, but unfortunately, we get the idea that the purpose of this machine is to look after some slice of our data from the overall collection used throughout the program.

It is this mindset that leads to objects as "active structures," like this typical example in C#:

class SomeClass

{

private int field;

public int Field => field;

}

This satisfies our requirement for encapsulation (the field is private), and our requirement that an object allows programs to access and modify a collection of data. What we have ended up with is no different from a plain old data structure:

struct SomeClass

{

int Field;

}

The exception is that the C# example requires a function call on each access of the field. There is no real encapsulation; objects with their own fields can make no guesses about the status of those fields, and a system including such objects can only be understood by considering the whole system. The hoped-for advantage that we could turn our big problem into a composition of small problems has been lost.

A contributor to this objects-as-data approach seems to have been the attempt to square object-oriented programming with Software Engineering, a field of interest launched in 1968 that aimed to bring product design and construction skills to computer scientists by having very clever computer scientists think about what product design and construction might be like and not ask anybody. Process-heavy and design-artefact-heavy systems, approaches, and "methodologies" (a word that used to mean "the study of method" until highfalutin software engineers took it to mean "method, but a longer word") recommended deciding on the objects, their methods, and properties; the data involved; and the presentation and storage of that data in excruciating detail, all in the name of satisfying a Use Case, which is Software Engineering speak for "a thing somebody might want to do."

The inside cover of "Applying UML and Patterns" by Craig Larman (1997) has 22 detailed steps to follow before Construction when constructing a product.

Objects can be thought of as simulations of some part of the problem we're trying to solve, and a great way to learn from a simulation is to interact with it. If our objects are just active structures that hold some data on behalf of a program, then we don't get that benefit: we can't interact with the simulation without building out all of the rest of the program. And indeed that is the goal behind a lot of the "engineering" processes that use objects: while they may pay lip service to iterative and incremental development, they still talk about building a system at once, with each object being a jigsaw puzzle piece that satisfactorily fits its given gap in the puzzle.

So, let's go back to Bertrand Meyer's definition, and remove the problematic bit about letting a program access an object's data:

An object is a software machine

A machine is a useful analogy. It's a device (so something that was built by people) that uses energy to produce some effect. Notice the absence of any statement about how the machine produces that effect, how the machine consumes its materials, or how the machine's output is supplied. We've got a thing that does a thing, but if we're going to compose these things together to do other things, we're going to need to know how to do that composition. Adding a constraint takes us from "it's a machine" to "it's a machine that we can use like this".

An object is a software machine that can collaborate with other software machines by sending and receiving messages.

Now we've got things that do things and can be used together. We don't restrict the level of complexity of the things that are done by each machine (so booking a flight and representing a number are both things that we could build machines to do); just how we would combine them. This has parallels with Brad Cox's software ICs analogy, too. An "integrated circuit" could be anything from a NAND gate to an UltraSPARC T2. We can use any of the IC's together, of any size, if we just know how to deal with their inputs and outputs: what voltage should appear on each pin and what that represents.

This analogy tells us that our software system is like a big machine that does something useful by composing, powering, and employing smaller component machines. It tells us to worry about whether the things coming out of one machine are useful as inputs to another machine, but not to worry about what's going on inside each machine except in the restricted context of the maintenance of those machines. It tells us to consider at each point whether the machine we have is more useful than not having that machine, rather than tracking the progress toward the construction of some all-powerful supermachine.

It even tells us that building an assembly line in which input of a certain type is transformed into output of a certain type is a thing we might want to do; something that, otherwise, we might believe is solely the domain of the functional programmer.