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

The Open-Closed Nature of Independent Objects

In his book Object-Oriented Software Construction, Bertrand Meyer introduced the Open-Closed Principle. This principle may be one of the most confusingly stated ideas in all of computing and has led to a whole sub-industry of articles and podcasts explaining how a ShapeRenderer can draw Squares and Circles (of course, I have also partaken of such, and will continue here).

The Open-Closed Principle says that a module (an object, in our case) should be open to extension – it should be possible to extend its behavior for new purposes – and yet closed to modification – you should not need to change it. This design principle comes with a cost, as you need to design your objects to support extensibility along lines that are not yet known (or at least, to make it clear which lines are or are not going to be fruitful) in return for the benefit that maintainers and users of the objects know that they are going to be stable and will not introduce breakages into a system through unexpected changes.

The nature of objects explored above, their treatment as completely independent programs, supports the Open-Closed Principle by keeping each object at arm’s length from the others. Their only point of contact is their messaging interface, even to their parent classes (remembering, of course, that they may not have any).

Therefore, to be open and closed, an object also needs to be ignorant: it should know as little as possible about its context. It knows what to do when it receives messages, and it knows when to send messages, but should otherwise remain uninformed as to what is happening around it. An ignorant object can be used in multiple contexts – open to extensions of its use – due to the fact that it cannot distinguish these contexts. It requires no contextual changes, and thus is closed to modification.

The Correctness of Independent Objects

When each object is its own separate program, then we turn the problem of “does this big system work” into two separate problems:

  • Do these independent objects work?
  • Are these independent objects communicating correctly?

Each of these problems has been solved repeatedly in software engineering, and particularly in OOP. An object’s message interface makes a natural boundary between “this unit” and “everything else”, for the purposes of defining unit tests. Kent Beck’s Test-Driven Development approach sees developers designing objects from the message boundary inwards, by asking themselves what messages they would like to send to the object and what outcomes they would expect. This answers the question “do these independent objects work?” by considering each of the objects as a separate system under test.

The London School of TDD, exemplified by the book Growing Object-Oriented Software, Guided by Tests, takes an extreme interpretation of the message-boundary-as-system-boundary rule, by using mock objectshttp://xunitpatterns.com/Mock%20Object.html as stand-ins for all collaborators of the object under test. This object (the one being tested) needs to send a message to that object (some collaborator), but there’s no reason to know anything about that object other than that it will respond to the message. In this way, the London School promotes the ignorance described above as supporting the Open-Closed Principle.

With the Eiffel programming language, Bertrand Meyer also addressed the question of whether each object works by allowing developers to associate a contract with each class. The contract is based on work Edsger Dijkstra and others had done on using mathematical induction to prove statements about programs, using the object’s message interface as the natural outer edge of the program. The contract explains what an object requires to be true before handling a given message (the preconditions), what an object will arrange to be true after executing its method (the postconditions), and the things that will always be true when the object is not executing a method (the invariants). These contracts are then run as checks whenever the objects are used, unlike unit tests which are only executed with the inputs and outputs that the test author originally thought of.

Contracts have turned up in a limited way in the traditional software development approach in the form of property-based testinghttp://blog.jessitron.com/2013/04/property-based-testing-what-is-it.html, embodied in Haskell’s QuickCheck, Scala’s ScalaCheck, and other tools. In Eiffel, the contract is part of the system being constructed and describes how an object is to be used when combined with other objects. Property-based tests encapsulate the contract as an external verifier of the object under test by using the contract as a test oracle from which any number of automated tests can be constructed. A contract might say “if you supply a list of e-mail messages, each of which has a unique identifier, this method will return a list containing the same messages, sorted by sent date and then by identifier if the dates are equal”. A property-based test might say “for all lists of e-mail messages with unique identifiers, the result of calling this method is...”. A developer may generate a hundred or a thousand tests of that form, checking for no counter-examples as part of their release pipeline.

The second part of the problem – are these independent objects communicating correctly? – can also be approached in multiple ways. It is addressed in a contract world such as Eiffel by ensuring that at each point where an object sends a message to a collaborator, the preconditions for that collaborator are satisfied. For everybody else, there are integration tests.

If a unit test reports the behavior of a single object, then an integration test is trivially any test of an assembly containing more than one object. Borrowing Brad Cox’s Software ICs metaphor, a unit test tells you that a chip works, an integration test tells you that a circuit works. A special case of the integration test is the system test, which integrates all of the objects needed to solve some particular problem: it tells you that the whole board does what it ought to.