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

Objects Are Independent Programs

The easiest problem to solve is allowing developers to independently design objects without expressing constraints that inhibit the developers' design freedoms. One way is to provide a MetaObject protocol that allows developers to adapt the rules of a language to fit a particular context. An even easier way (both to create and to use) is to make the primitive parts of the message system available to developers, to combine as needed to fulfill their design goals.

This is easier to create because any more complex system would need these primitives anyway. It is easier to use because it allows the developers to construct solutions to problems as they encounter them, rather than trying to work out how to adapt existing rules onto the models they have for their solutions. That adaptation was one of the difficulties with using OOP we explored in Chapter 1, Antithesis: if what you've got is Java inheritance, you need to solve your problem using Java inheritance, even if your problem doesn't seem like it fits with Java inheritance.

The primitives needed are small in number. Here is a worked example in Python that is based on the functional programming view of objects explored in Chapter 1, Antithesis.

A selector type. This is a type that can be used to name messages, and thus it must be comparable: the receiver needs to know which selector was named in a message so it can decide what to do. Python's string type is sufficient as a selector type, though many OO languages use an interned string type (Ruby's symbols, for example) to make comparison cheaper.

A lookup function. This is a way, given a message, to decide what code to run. Python already uses a __getattr__() function to do this, both for its object.attribute syntax and to implement the getattr(object, attribute) function, and conveniently, it expects the attribute's name to be a string, so this works with the message selectors.

A way to send a message. This will let the object find the appropriate method implementation using its own lookup function, then execute that method with the arguments supplied in the message. It looks like this:

def msg_send(obj, name, *args):

message_arguments = [obj]

message_arguments.extend(args)

return getattr(obj,name)(*message_arguments)

Notice the convention that the first argument to any message is the receiving object. This allows the object to recursively message itself, even if the method being invoked was not found on the receiver but on a delegated object that would otherwise be ignorant of the receiver.

A recursive case for message lookup. If an object does not know how to implement a given message, it can ask a different object. This is delegation. It looks like this:

def delegate(other, name):

return getattr(other, name)

A base case for message lookup. Eventually, an object will need a way to say "sorry, I was sent a message that I do not understand". The doesNotUnderstand function provides that behavior (in our case, raising an error), and we'll also supply a Base type that uses doesNotUnderstand and can terminate any delegation chain:

def doesNotUnderstand(obj, name):

raise ValueError("object {} does not respond to selector {}".format(obj, name))

Base = type('Base', (), {

'__getattr__': (lambda this, name:

(lambda myself: myself) if name=="this"

else (lambda myself: doesNotUnderstand(myself, name)))

})

Due to the message-sending convention, myself is the object that received the message, while this is the object that is handling the message on its behalf: these could be, but do not have to be, the same.

Now these 13 lines of Python (found in objective-py at https://gitlab.labrary.online/leeg/objective-py) are sufficient to build any form of object-oriented delegation, including the common forms of inheritance.

An object can inherit from a prototype by delegating all unknown messages to it.

A class is an object that implements methods on behalf of its instances. A created instance of a class contains all of its own data, but delegates all messages to the class object.

The class can have no parents (it does not delegate unknown messages), one parent (it delegates all unknown messages to a single parent class object) or multiple parents (it delegates unknown messages to any of a list of parent class objects). It can also support traits or mixins, again by adding them to the list of objects to search for method implementations in.

A class could even have a metaclass: a class object to which it delegates messages that it has received itself. That metaclass could have a metametaclass, if desired.

Any, or multiple, of these schemes can be used within the same system, because the objects are ignorant of each other and how they are constructed. They simply know that they can use msg_send() to send each other messages, and that they can use delegate to have another object respond to messages on their behalf.

But, Python being Python, these objects all run synchronously on the same thread, in the same process. They are not truly independent programs yet.

Sticking with Python, it is easy to separate our objects out into separate processes by using a different Python interpreter for each object via the execnethttps://codespeak.net/execnet/index.html module.

A quick, but important, aside

The example here (and available at https://gitlab.labrary.online/leeg/objactive-py) focuses on demonstrating the possibility of running isolated objects, and is not really appropriate for using in a real application or system. The lack of production systems based around the simple object-oriented principles described in this book is the motivation for writing the book in the first place!

Each object can live in its own module. Creating an object involves creating a new Python interpreter and telling it to run this module:

def create_object():

my_module = inspect.getmodule(create_object)

gw = execnet.makegateway()

channel = gw.remote_exec(my_module)

return channel

When execnet runs a module, it has a special name that we can use to store the receiving channel and install the message handler. In this code, the receiver is stored in a global variable; as this is running in its own Python interpreter in a separate process from the rest of our system, that global is in fact unique to the receiving object:

if __name__ == '__channelexec__':

global receiver

receiver = channel

channel.setcallback(handler)

channel.waitclose()

The handler function is our object's message dispatch function: it inspects the message selector and decides what code to run. This can work in exactly the same way as in previous examples—in other words, it can work however we want. Once an object receives a message, it should be up to that object to decide what to do with it, and how to act in response.

An Object's Behavior Can Be Described in A Contract

While it is up to any one object to decide how it responds to messages, we need to know whether that object represents a useful addition to our system. In other words, we want to know what the object will do in response to what messages.

As seen in Chapter 2, Thesis, the Eiffel language encapsulates this knowledge about an object in the form of a contract, describing the preconditions and postconditions for each method along with the invariants that hold when the object has been created and whenever it is not executing a method.

This contract is, as the language in Object-Oriented Software Construction implies, a useful design tool: describe your object in terms of the messages it receives, what it expects when it receives those messages, and what the sender can expect in return.

Eiffel also demonstrates that the contract is an effective correctness testing tool, because the assertions contained in an object's contract can be checked whenever appropriate, whether the object is being used in a test or a production system. In principle, the contract could even be used to generate tests in the style of property-based testing; what is "for all (expected input structure) -> (assertions that some properties hold of results)" other than a statement of preconditions and postconditions? In practice, this integration does not yet exist.

As the contract describes what an object can do, what must be true for the object to do it, and what will be true after the object has done it, it's also a great candidate for the position of standard documentation structure for each object. We already see in the world of HTTP APIs that the Open API Specification (formerly Swagger, https://swagger.io/specification) is a machine and human-readable description of what operations an API supports, its parameters and responses. An approach like this could easily be adopted for individual objects; after all, an object represents a model of a small, isolated computer program and so its message boundary is an API supporting particular operations.