Software engineering perspectives

\seindex{perspectives} Of the various phases of the software life cycle, the design phase is perhaps the most interesting one since it aims at reconciling the requirements resulting from the analysis phase and the restrictions that are a priori imposed on the implementation phase. On the one hand a design must specify the structure and functionality of a system in a conceptually clear way and on the other hand it must take into account the feasibility of an implementation. To conclude this introductory chapter, we will investigate what support the three paradigms just treated provide with respect to the design of a system, that is to which problems they promise a solution and which questions they leave unanswered.

Logic programming

\seindex{logic programming} The contribution of logic programming as a program design formalism lies primarily in its declarative nature. Designing a program in a logic programming formalism enables an easy transition to an actual implementation in a language such as Prolog. Prolog is a general purpose language based on first order logic with a simple semantics. It uses backward chaining inference as a computation mechanism. Due to its declarative semantics a Prolog program may be read as a logical theory concerning a particular domain. This aspect has contributed to the popularity of Prolog for AI-applications. It has been extensively used to implement expert systems. See  [Bu83]. Further, it has been used to model British legislation, drug design and even systems programming. In addition, in  [Webster] Prolog is characterized as a potentially powerful design representation tool, since it may be used to specify the functionality of systems in a concise and formally precise way.\ftn{ In a similar vein, constraint logic programming languages can be thought of as providing an even more powerful formalism, since these languages have knowledge of a particular (formal) domain built-in, which allows them to solve equations over these domains. } For the representation of knowledge, however, Prolog has a number of drawbacks. It lacks first of all facilities to modularize knowledge, that is to partition the knowledge in module-like entities. It lacks, secondly, a means to construct hierarchies of concepts in a structural way. A number of extensions have been proposed to deal with these deficiencies, extensions with hierarchical frame-based representations, with inheritance hierarchies and object oriented features. According to  [Su85], none of these extensions have matured sufficiently to gain wide acceptance. Promising approaches seem to be those aiming at incorporating object oriented features in the logic programming paradigm, that allow to create logical theories dynamically as first-class entities.

Object oriented programming

\seindex{object oriented programming} Object oriented software technology has major impacts on the traditional software life cycle. One of these impacts is the shift of emphasis brought about in the relation between the design and the implementation phases of a software development project. The design phase has become increasingly important since the availability of object oriented programming languages allows to regard the implementation as a process of refining the decisions made in designing the system. The popularity of object oriented programming languages is partly due also to their successful use in prototyping systems of medium complexity. According to  [Meyer88] the introduction of object oriented programming has meant the beginning of a revolution in the program development process, since these languages allow to design a system bottom-up, instead of in the traditional way, top-down. The end result of developing a system, bottom-up or top-down, is a set of classes that specifies abstractly the functionality of the corresponding objects by means of external method interfaces. After a while, designing a system comes down to selecting a number of already existing components and assembling them according to the requirements resulting from analysis. Although, undeniably, the object oriented approach promotes the reuse of software, there are many problems to be solved before this view becomes reality. See for example  [Meyer90] and  [BV90]. One of the major problems that we encounter here is the problem of granularity. In many cases, a class will be a too low-level entity to serve as the unit of reuse. A visual metaphor that illustrates this problem is given by the term ravioli-code to characterize a large collection of well-structured objects with highly complex interrelations. To overcome problems of granularity, one has to think in terms of application frameworks to capture the functionality of subsystems. Even then, the specification of a system in an object oriented programming language falls short of providing insight into the conceptual structure of the system, that is the design underlying the program.\ftn{ The plethora of constructs that may be encountered in actual object oriented programming languages makes this insight even more difficult. } Experience has shown that prototyping is advantageous in situations where the requirements are likely to change. To enhance the conceptual understanding of the system embodied by the prototype, the language in which the prototype is implemented needs to be of a sufficiently high level to allow the code to be read as a formal specification of the system. An object oriented approach is only fruitful then, if the objects implementing the system provide an abstract view that corresponds with the way we perceive the problem domain.

Parallel/distributed programming

\seindex{parallel/distributed programming} The choice for a parallel or distributed implementation may be motivated either by the application domain, in which concurrency is a natural phenomenon, or by efficiency considerations. From either perspective, designing a distributed system involves a partitioning of the system into separate components and an allocation of these components to (possibly distinct) processes. See  [Shatz87]. In order to distribute the computation among a number of concurrently executing components, the design will have to indicate a partitioning of the functionality among a number of distinct logical modules. Further, the design must specify the synchronization and possibly the communication that occurs between these logical modules. Logical modules, in the sense used here, may be active objects, single-threaded processes or even multi-threaded processes. For the actual exploitation of parallelism, an allocation of these modular entities to processes must be given, taking into account possible precedence relations that may hinder the independent execution of these modules. Allocation may be done statically, before actually starting a system, or may be determined dynamically by an allocation algorithm that takes system parameters such as the workload and the availability of processors into account. Using a performance oriented cost function to determine an optimal allocation, further factors that may be taken into account are the amount of interprocessor communication and the total completion time. One of the principal difficulties in designing a distributed system is to ensure the correctness of the program. Verifying a distributed system is difficult because of the non-deterministic behavior of such a system, that results from the independence of the concurrently executing components. This non-determinism makes dynamic testing almost infeasible. First of all, it is hard to reproduce the state of the system that has resulted in an error. Secondly, since there is no global state there is no way to freeze the execution in order to inspect the states of the individual components. Before freezing takes effect, the relevant components may already have changed their local state. As a more fruitful approach,  [Shatz87] recommend to employ static testing, an analysis that may be performed without executing the program. In a static test all syntactically specified control-flow paths must be considered. Analysis may then reveal the potential for deadlock, or the occurrence of particular communication events. Given the difficulty of applying a verification method to ordinary sequential languages, the difficulty of applying such a method to a distributed program will be clear.

Distibuted logic programming

\seindex{distributed logic programming} In the previous discussion we have indicated some of the problems that arise in designing a software system. Whatever formalism is used, the partitioning of a program into modules and the specification of the interaction between modules will always remain a non-trivial problem that can only be solved by creative thinking. Our effort of introducing a new language is not meant as a promise to make this task any easier but to provide the means to encode a solution in a high level formalism, including all relevant aspects of a system (including its physical distribution). Such a high level description promotes the conceptual understanding of the system specified and makes the task of verifying the program easier. In developing the language DLP, which will be the subject of the rest of this book, we have made choices to combine the paradigms introduced in this chapter. With this discussion we have made an attempt to motivate these decisions from a software engineering perspective.