Improving the XCode IDE: More Than a Source Editor?
(Inspired by XCodeTooMuchLikeATextEditor)
Software, in the abstract, is like Cosmo Kramer’s apartment re-design: it’s got many levels. How much this is true in reality depends on how much reality you think exists in software and the process of software development. But I’ll just assume that the description holds.
One set of of levels are: statement, block, method, class, interface, module, application (these are construction-related � as the compiler and linker see them). Another set are the layers of library, model, controller, view (or a similar structure for a different kind of application � as the software designer sees them). The class and object hierarchies might also be looked at as levels, sharing some aspects with the each of the design and construction viewpoints.
Because you need to produce working code, the first role of development tools � usually your IDE � is to assist you in the construction phase. Compilers and linkers include after-the-fact assistance in the form of errors and warnings. Text editors add syntax-aware colouring and indenting, then code completion. Build tools track your source files, object code files, and dependencies. Higher level tools track versioning of code, platform-specific builds, or even automate construction of build specification files based on the platform. It’s safe to say that automation of software construction is at an advanced state. It could probably use a lot of consolidation and simplification, to speed set up, but basically the power is in the hands of the developer in the construction phase.
Progress in automating design of software has been a lot slower. Design is a harder problem. There are numerous approaches, and often they don’t mix well. Classically (in other disciplines), design has been cleanly separated from construction. The majority of programmers find this impractical, so high-level design tools are mostly shunned except by a minority of design nerds or by those who must use classical design methods to ensure adherence to fixed design criteria, for safety reasons.
For practical design abstraction, what is needed is not a monolithic approach, as found in UML or other design specification languages and tools. What is needed is an organic approach. Construction tools need to evolve in order to become software design tools.
The purpose of design is to improve the chance of successful construction of some artifact, measured solely by that artifact’s ability to function as intended. This assumption falls down when the artifact’s function is a moving target ��that is, when its function depends more on what is possible than on what is planned, as is often the case for software which does not yet exist.
So in the case of software, the role of design is slightly different than in mechanical or civil engineering, or architecture, or industrial design. While it is abstractly possible to use design to predict how software will work and why, it is more practical to use design in parallel with construction, the one guiding the other, taking full advantage of the feedback which most successful software development already employs. If other disciplines could take control of feedback over short time periods, they surely would do so. Even now, they use feedback over long time scales; each engineering project, and each product manufactured and sold, goes into use, and its successes and failures inform the next similar project, be it a kitchen table or a passenger aircraft. The successes and failures are addressed in the next project through formalized methods, but the process is still iterative.
Software development iterations are orders of magnitude faster. We can build a new application in minutes or seconds � in fact, sometimes we don’t have to re-build it at all. Trying to do all the design first and all of the construction second is thus a limiting factor. It is a trade-off which most developers are not keen to make. Why invest a long period in design only to find out, within five minutes of the first compile, that the design needs to be tweaked? Are you going to re-write all of your design documentation? No. And in fact it is a truism that design documents quickly get out of synch with the code base as practical decisions in construction override design decisions. This is true to some extent in some kinds of production, such as architecture (that is, of buildings), where certain deviations are allowed within parameters of safety and aesthetics, but in most cases, errors in design must be addressed in (re-) design, for legal and project management reasons.
To capitalize on the responsiveness � the fluidity � of software development, we should have tools which can let us inspect the software from a point of view that is higher up than source code. One day we will be able to build multiple variations of the same program and engage in parallel testing, leading to an evolutionary process in parallel instead of just serially like today, so software which can manage design decisions will become all the more valuable. But it requires more than a means to facilitate design � we must also be able to describe and examine the implicit design: to see the shape of the software as it exists at the moment.
In other words, design must be as fluid as the process to which it is applied, and it should be as descriptive as it is prescriptive, or it will not be sufficiently useful. Moreover, it has to be recognized by theorists, tool designers and programmers that design decisions happen at every stage. If these decisions are not made explicit, they cannot be considered � cannot be acknowledged or learned from, praised or censured, imitated or avoided.
At the level of the language statement or block, and even the procedure, the language itself is still the best way to describe the design. We may invent better languages, but chances are that they will always be textual, for their relationship with natural language, which is a human being’s most powerful tool for communication. But when a language cannot specify a design decision, it also cannot describe it, and other kinds of languages are necessary. These do not have to be graphical, but neither will they look like a programming language. Programming languages are meant for talking to compilers. Compilers have no sense of design, however, since they will as readily compile a useless program as a useful one, as long as the program is correct within the syntax and semantics of the language.
And that, to bring back my first point, is the value of design: to improve usefulness.
To say that design is not needed is to say that programs cannot be more or less useful, which is preposterous. On the other hand, to say that design is clumsy, or impractical, or at odds with the work of writing software might be correct, if the design process is badly … designed. And this is another important point: design is a process, which is a tool, which is designed, and can be designed badly. There are no tools for meta-design, at least not yet. We have to do this manually.
The design of our design tools and processes, then, being ideally software-based, ought to follow along with our current best methods for designing other software. It should be iterative, it should be fluid, and it should be descriptive as well as prescriptive. Ultimately, it should be integrated with the primary process of constructing useful software. What is necessary is to see that there is a secondary construction process � that of making useful software development tools. Like all software development, it is an ongoing process. (This puts software development tools in the Escherian state of being rather self-constructing, when the tools are used in their own development. In this way, they are vividly an extension of human beings themselves.)
Consider Apple’s XCode IDE. An IDE, for starters, is meant to integrate the tasks necessary in software construction: writing source code, specifying dependencies, and managing compiling and linking. It is a Swiss Army Knife of construction-centric software development. XCode extends its mandate as much as possible, allowing quick turnaround of code changes into development builds, speeding coding with code completion, integrated documentation and API browsing, support for source code versioning, and other treats. How can we extend this tool naturally to make design decisions more explicit (for inspection) and high level (for specification)?
We can learn a lot by considering possibilities for inspecting the design. Imagine features that could answer the following questions (couched in Cocoa terminology):
The list focusses on two of the major features of a speculative application in development: objects as areas of memory linked together by reference/pointer, and objects as entities which communicate with one another. These are features of a program which are partly visible by code inspection (if you can inspect multiple files simultaneously) and partly depend on extrapolations into runtime behaviour. But they all depend on making justified inferences from the code itself.
In order to answer questions such as those above, it would be necessary to have an analysis tool which was based on a language for describing object and class relationships � both the static and dynamic aspects. It would have to be able to accrue information about these relationships application-wide and be able to display them, either in a textual or pictorial manner. UML is an obvious candidate, but I think it is too oriented towards top down, prescriptive design, and is not suitable. I think it is also overly large and ponderous.
If such a descriptive language were developed, it would then perhaps be conceivable that we could move on to the next step: specifying such aspects of an application using the same language. At the simplest, we might be able to start with a root class, such as a document class or other controller class, and instead of asking what objects are available for it to send messages to, specify what messages it wants to send, and thus assemble a “dream API” for other classes to expose. In fact, this might allow a way to explicitly manage the design of protocols, both formal and informal, in Objective-C, which target classes will adopt.
Depending on the type and purpose of the application, we might be able to drive development starting with the class which is closest to the external force which puts the program into action. In the case of a user-centric application, the user interface will drive the development of a set of messages which capture the user’s intention. For business modelling, the high level inputs to the model will drive the messages which the model can accept. In an MVC application, the specification for the controller layer will result mostly from the messages which it must respond to, the messages it can send, and the specification of the mapping between these sets of messages. Ideally, sets of messages will break down into logical groups corresponding to how data is best aggregated into classes.
In such a process, the design of classes results less from some mapping of “real world” objects to software objects, and more from the partitioning of all application data and messages into smaller, related groups. In effect, it says, this application’s behaviour could be broken down along many different possible lines, or not broken down at all, according to different criteria. Moreover, the partitions could be dynamic. In a proper system where no class assumes local availability of instance data (that is, where instance data is always accessed through dedicated accessor methods), instance data, accessor methods, and related behaviour methods could freely move around from one class to another as best suits higher level design decisions.
Clearly, it would not be worth the trouble of applying this approach to the development of utility classes, which will always result from data-behaviour relationships based on real-world objects or metaphors. But where such metaphors are unclear or completely unavailable, such a partitioning approach would be more helpful. At the level of lines of code, such partitions often appear arbitrary, or at best intuitive. If it is possible to see the results of class design decisions from a higher level of abstraction, interactively and in real time, it is easier to see what drives those decisions, and how they feed back into the design of other classes and modules.
In the implicit paradigm I am describing, behaviour takes precedence over data � data enables behaviour, and is essential to the successful completion of tasks. In applications (or parts of applications) where data is paramount (such as media processing applications and database applications), a different paradigm holds and different design decisions are necessary. It then becomes a question of containing, accessing, and processing data in a controlled manner. But there is always support data, and classes and objects which use support data will benefit from a partitioning approach to the organization of that secondary data and the behaviour it supports.
In essence, when data drives the design, the design work is more constrained, and there are fewer choices where the priorities are vague or unclear. But in behaviour driven design, there are many unclear choices, because the line dividing one behaviour from another might be either very fine or completely arbitrary. Often those lines are there simply because it is necessary to break the behaviour up into manageable � comprehensible � units. In Cocoa, I believe this mostly happens in the controller layer of applications, but it can be found all over the place. And the larger an application, the more it will depend on manager, controller-type classes. Hence the relationship between controller classes, and between controller and model, or controller and view classes, is the focus and determinant of the design.
If the design is implicit, we need to be able to make it explicit. If we can make it explicit, we ought to try to make explicit the factors which determine the decisions which result in the design. And if we can do that, we have a good chance of being able to specify those decisions in some manner other than just source code, and simultaneously reflect changes in those decisions in the same manner.
The most likely candidate for expressing and specifying those decisions, and the resultant design, is with a language and a tool. The tool will not necessarily write source code for us, but it will at least allow us to organize that source code, and re-organize it dynamically � at the level of methods and support data, not just class and header files.
In order to work towards such a tool, we can start more simply. We should be able to specify a method � or a set of methods � and one or more pieces of dependent data, irrespective of class. In addition to this we can specify one or more sets of behaviour as a target class, and the code which specifies the interface body of the behaviour will be inserted into that class at compile time. In addition, dependencies (library or class imports) will be tied to individual methods, but will bubble up into behaviour and class specifications. Ideally, these should be handled automatically, but always transparently. By this I mean that the declaration of a local or behaviour-level instance object (if not an id) or a message to said object will automatically engage a search for the best interface which declares said object or method � if there is more than one, the developer can always choose from a list. The interface will, of course, be automatically imported when the class is defined.
The crux of the argument, here, is that the notion of a class is more fluid than is often acknowledged, especially for behaviour-driven classes, modules and applications. A good tool should recognize that fluidity, and acknowledge the different drivers which determine how the behaviours flow. It should allow the developer to see the results of different behaviour partitioning choices immediately, in terms of the shape and organization of the class and object hierarchies, and in terms of the complexity patterns of message passing. Are the trees sensibly shaped? Are behaviours grouped naturally by class and module? Are sets of messages steady and consistent, or do they happen in bursts? Does control of the application reside where it is supposed to � does it originate with the user? (The notion of the “user” will vary depending on the context. A user might be a different code module or a different application.)
By the nature of the tools we use, software development tends to put emphasis on the static representation of a program in source code. Many design tools attempt to get away from the specifics of source code, to look at higher level structures, but they usually still retain the focus on the static structure of the program pre-compilation. Even seemingly dynamic tools like sequence diagrams are tied to the structure of source code. They downplay other structures, specifically those that are created when the application is running � when it is actually working and doing what it was meant to. The next generation of development tools will have to shift the focus away from static structures towards dynamic behaviours. The way to start is to use the source code to find and reveal dynamic structure which is inherent, especially message passing patterns, which help predict the behaviour of an application at runtime.