Lasagne/J [6] is an example of a hybrid approach where the primary extension mechanism is object-based wrappers, i.e. a non-invasive technique, but it also.
Independently Extensibile Contexts Martin Rytter and Bo Nørregaard Jørgensen The Maersk Mc-Kinney Moller Institute, University of Southern Denmark, Campusvej 55, 5230 Odense M, Denmark {mlrj,bnj}@mmmi.sdu.dk http://www.sdu.dk/mmmi Abstract. Building and maintaining non-trivial software systems that are independently extensible is a difficult task. This is because the combination of independent extensions tends to produce conflicts that are difficult to anticipate, and to which no general resolution strategy exists. In this paper, we show how some of these conflicts can be avoided if domain-specific contexts are modeled using a representation that is open for extension and safe for sharing among independent extensions. Keywords: Independent extensibility, openness, sharing, context.
1
Introduction
The creation of software systems that are independently extensible is a difficult but important challenge [14]. To be independently extensible, it must be possible to combine independently developed extensions of a system without performing a global integrity check. Independent extensibility would be easy to achieve, if system designers were able to anticipate dimensions of extension that would be needed in the future. Unfortunately, this is often not the case: – It is difficult for a programmer to anticipate extension points required by future extensions. Whenever the programmer fails to do so, and the required extension point cannot be introduced, the system fails to be extensible. – It is difficult for a programmer to anticipate interactions among mutually unaware extensions developed independently of each other. When the combination of extensions may lead to undesirable interactions, the system fails to be independently extensible. In other words, the combination of requirements for independent extensibility and unanticipated extensibility is difficult to achieve. We suggest that the above problems may be minimized, if we improve our ability to model independently extensible contexts. Specifically, the representation of domain-specific contexts must be open to extension and yet safe to share among independent extensions. The rest of the paper is organized as follows. Section 2 presents the state of the art of independent extensibility mechanisms. Section 3 motivates the importance of context representations that are open and suitable for sharing. Finally, section 4 concludes the paper. M. Ali Babar and I. Gorton (Eds.): ECSA 2010, LNCS 6285, pp. 327–334, 2010. c Springer-Verlag Berlin Heidelberg 2010
328
2
M. Rytter and B.N. Jørgensen
State of the Art
Independent extensibility cannot be achieved when the means of extensibility violate the mutual independence of extension providers [14]. In the context of this observation, we now provide an overview of approaches for achieving extensibility. When dimensions of extension can be anticipated, it is possible to constrain interaction using a component framework approach [17]. In this approach, existing systems ensure that all supported dimensions of extension are coordinated in such a way that independent extensibility can be guaranteed. Such coordination generally relies on high-level contracts among components [1]. Modern operating systems are examples of component frameworks. They work by letting the subject of extension (the operating system core) coordinate shared resources (processor, memory, etc.) through which independent extensions (programs, drivers, etc.) interact [11]. The difficulty of anticipating the future may be used to advocate mechanisms that support unanticipated extensibility. Two categories of unanticipated extensibility exist: i) Invasive in-place modification, and ii) non-invasive refinement that leads to client migration. Unfortunately, both categories of solutions lead to independent extensibility problems [12]. With invasive in-place modification, an extension provider modifies the existing system in order to allow for extension. Since modification is performed in place, changes become globally visible to all extensions. Modifications may be performed directly in source code, i.e. traditional evolutionary pressure [9], or on an intermediate representation of the system, e.g. using open classes [3] or aspects [7]. Invasive in-place modification techniques suffer from the problem that some conflicts are not detectable during development of the individual extensions, but first when extensions are combined. Such conflicts may be due to the introduction of similar members, overlapping invariants, or the need for an order in which to advice, or overwrite methods [13]. Non-invasive refinement is an extension strategy where modifications are performed on copies. This approach avoids conflicts that may emerge from in-place modification. Instead, we encounter the problem of client migration, i.e. the task of making existing clients refer to a newly refined version of a source file, module, or class. The ability to migrate a client may itself be unanticipated, and thus lead to the need for migration of clients-of-clients [12]. In the extreme case, the user is “migrated” to use a new version of the entire system. The oldest and most basic non-invasive refinement strategy is the practice known as copy-and-paste reuse. Other examples include Hyper/J [16] and object-based wrappers [4,2]. Lasagne/J [6] is an example of a hybrid approach where the primary extension mechanism is object-based wrappers, i.e. a non-invasive technique, but it also supports adherent methods, i.e. an invasive technique similar to aspect-oriented advices.
Independently Extensibile Contexts
3
329
Independently Extensible Contexts
We now proceed to discuss independently extensible contexts. First, we discuss what a context is, and how it is usually represented in mainstream software systems. Subsequently, we discuss the role of openness and sharing in making contexts independently extensible. 3.1
Context
In general terms, a context is a setting in which statements may be interpreted or claims verified [10]. E.g. “a letter” is a context in which we can interpret statements such as “read it”, “write it”, “send it”, and so on. There are constraints on which statements can be meaningfully interpreted in a context. In context of “a letter” we can meaningfully interpret statements such as “send it”, while other statements such as “drive it” has no obvious meaning. A context may change. What this really means is that we maintain the perception of a context’s identity, while we allow its state or constraints to change. E.g. given “a letter” we may change its state by “writing it”, or we may change its constraints by discovering meaningful semantics for a statement such as “shred it”. Despite these changes the identity of the letter remains the same. It is possible to see the state of a context as a simple constraint that must always be true, e.g. “the letter must be blank”. However, we use the word “state” because it is extremely intuitive. Our motivation for discussing contexts’ ability to change is to highlight how modeling of contexts using networks of objects often constrains this process. We may do so by outlining three levels of context openness: – A closed context never changes. E.g. a String object never changes. This is also known as immutability. – A restricted context may change in well-defined anticipated ways. E.g. a Letter object may change the value of its address field, but only because the developer of the Letter class anticipated this scenario by introducing an appropriate set method. – An open context may change in unanticipated ways. In a statically-typed language this level is difficult to achieve because every object is constrained by its class, i.e. an explicit definition of the forms of change that can occur. A Letter cannot suddenly have a return address if no setReturnAddress()method was anticipated. In order to achieve openness of any context, one must be able to modify its representation – in case of the inability to do so, the context is effectively closed. 3.2
Open Context
The openness of a software system relies on our ability to modify networks of objects. We now discuss how such modification is constrained in statically-typed
330
M. Rytter and B.N. Jørgensen
Greenhouse
Sensor
-s: Sensor
+get(): Value
+getSensor(): Sensor +setSensor(s:Sensor)
g : Greenhouse
s : Sensor
Fig. 1. A restricted network of objects
object-oriented languages, and we outline a simple modeling technique that may increase openness. The modification of a network of objects is closely related to the concept of object composition. Object composition is traditionally seen as the combination of simple objects into more complex ones – i.e. composing a network of objects. However, a widely accepted benefit of object composition lies not in the ability to create, but in the ability to modify, a composition at runtime [4]. In order to understand how a network of objects is restricted by classes we will introduce a trivial example. The example is focusing on only two objects, a Greenhouse object, g, and a Sensor object, s. The diagram in figure 1 shows a class view and an object view of the example. Now let us consider the openness of this object network, i.e. which modifications are possible without imposing change on existing classes. Below we consider openness with respect to the g object – a similar analysis can be made for the s object. – It is possible to set the existing link outgoing from g to i) null, ii) a new object of class Sensor, or iii) a new object of a subtype of class Sensor. – Without invasive change it is impossible to modify g to i) have a link to an object of a class that is not a subclass of Sensor, or ii) have a new number of outgoing links. Given this analysis we may say that the object, g, is constrained by its class, Greenhouse. We may also say that the specific object-level link, g.s, is constrained by the class-level association, Greenhouse.s. While it is possible to introduce an object with an unanticipated implementation, e.g. a SpecificSensor, it is not possible to introduce an object with an unanticipated interface, i.e. an object that is not a subtype of Sensor. Figure 2 demonstrates how the lack of openness can be avoided by using the lookup pattern [8]. The idea is that a class, e.g. Greenhouse, by associating a Lookup at the class level, may allow for links to objects with unanticipated interfaces, e.g. Actuator objects. By introducing the lookup pattern [8] when designing Greenhouse, the developer can facilitate openness with respect to unanticipated interfaces. E.g. an independent extension may decide to compose g with an Actuator object, a, using the Lookup: g.getLookup().addLink(Actuator.class, new Actuator());
Independently Extensibile Contexts
Greenhouse
Lookup
+getLookup(): Lookup
+lookup(c:Class): T +lookupAll(c:Class): T[] +addLink(c:Class,obj:T): Link l : Lookup
g : Greenhouse
331
Actuator +get(): Value +set(v:Value)
a : Actuator
Fig. 2. An open network of objects
The openness offered in our example is not specific to a single extension. Another extension may navigate the link from g to a indirectly through the Lookup: Actuator a = g.getLookup().lookup(Actuator.class); if(a != null) { /* use actuator */ } The increased openness comes from the idea that links can be qualified by a Class object. In Java, the Class object can be obtained through the special class-field, e.g. Actuator.class. The lookup pattern has helped us to create a network of objects that is open to unanticipated changes. Specifically, a network of objects is not just open to objects with unanticipated implementations but also objects with unanticipated interfaces. 3.3
Shared Context
In addition to being open, the representation of a context must also be designed to be shared among multiple independent clients. An object is shared when more than one alias refer to it [5]. The owner of an alias may be called a client of the object to which the alias refers. To illustrate a simple case of sharing, we will now consider two independently developed extensions being clients of the Actuator, a, in figure 2. For this example, we will assume that a controls the position of a window in the Greenhouse, g. Extension 1 attempts to prevent condensation in the Greenhouse by ventilating when humidity becomes critically high: // Extension 1: Prevent condensation. if(humidityCriticallyHigh) { a.set(Value.OPEN); } Extension 2 attempts to prevent loss of heat by closing all windows when the Greenhouse is being heated up: // Extension 2: Prevent heating energy loss. if(heatingValveOpen) { a.set(Value.CLOSED); }
332
M. Rytter and B.N. Jørgensen
While the code snippets from extension 1 and extension 2 work fine in isolation, their combination may lead to undesirable behavior. Specifically, it may happen that condensation takes place when extension 2 runs after extension 1, or a loss of heating energy may happen when extension 1 runs after extension 2. Note that this problem emerges even though both extensions are non-invasive. In a monolithic system, the sharing of a would merely be a bug that should be fixed. However, in an independently extensible system the problem is more severe, because no extension developer can be blamed for the undesirable behavior that emerges from sharing [14,15]. If someone is to blame, it is the developer of Actuator, who has published an interface that allows a conflict to emerge at a point in time where no general resolution strategy applies. For this reason we hold that: The interface of an object, o, shared among independent extensions, must ensure that any contract offered to a client, c1, cannot be violated by any other client, c2. In the example outlined above, no explicit contract is stated for Actuator.set(Value) above the language level. However, traditionally programmers expect the effects of a client invoking a set method to persist until the same client invokes the same method again. In a non-sharing scenario this contract would hold, but not always in a sharing scenario. Note that the formal specification of a contract may facilitate detection of undesirable interactions [1]. However, a formal specification does not ensure that independent clients will actually find the offered contract useful – i.e. independent extensibility is limited by the nature of contracts, and not merely by the lack of formal specification. A set method is one of the most common examples of an idiom that does not support sharing and thus hinders independent extensibility. The solution to this problem is to assign state using a protocol that supports multiple clients. In our example we may substitute a traditional assignment for a simple priority-based protocol. Figure 3 demonstrates how this can be done. Instead of invoking a set method, a client can influence state by adding a ValueProvider. A ValueProvider provides not only a desired value but also a priority. The protocol ensures that a value of higher priority will always be preferred over one of lower priority. Thus, instead of being arbitrary, the conflict resolution is now handled using a high-level protocol known to all extensions. The core protocol behavior is implemented in Actuator.get() that statically depends on the ValueProvider interface: Value get() { List l = getValueProviders(); sort(l, DESC_PRIORITY); return (l.size() > 0) ? l.getValue(0) : null; } In figure 3 all interfaces, i.e. Actuator and ValueProvider, support sharing. This has been achieved by promoting the desire to modify a shared state
Independently Extensibile Contexts
333
Actuator +add(v:ValueProvider) +get(): Value
ValueProvider +getValue(): Value +getPriority(): Priority v1 : ValueProvider
l : Lookup
g : Greenhouse
a : Actuator
v2 : ValueProvider
v3 : ValueProvider
Fig. 3. A network of objects suitable for sharing
to an object, i.e. ValueProvider. In doing so, we have eliminated the method Actuator.set(Value) which, as we have seen, does not support sharing. Instead we have introduced Actuator.add(ValueProvider). An extension invoking this method does not cause any undesirable interactions with other independent extensions – thus, this method is safe in sharing scenarios.
4
Conclusion
Our inability to model independently extensible contexts poses a threat to the design of software systems, for which not all dimensions of extension can be anticipated. To remedy this problem, we have emphasized two techniques, which we believe offer a pragmatic approach to improve the situation. First, by using the lookup design pattern, it is possible to model fine-grained domain-specific contexts as networks of objects that allow for non-invasive introduction of links to objects with unanticipated interfaces. The technique is useful when it is possible to anticipate the existence of an open context, but not specific interfaces required by future extensions that must be used in that context. Second, we suggest that interfaces of objects representing an open context must be carefully designed in order to support sharing. Specifically, a shared interface must ensure that a contract offered to a specific client cannot be violated by any other client. This can be achieved by accepting the selection of high-level coordination protocols to be an integral part of interface design. Improved modeling of independently extensible contexts is essential in order to achieve independent extensibility of non-trivial software systems.
References 1. Beugnard, A., J´ez´equel, J., Plouzeau, N., Watkins, D.: Making Components Contract Aware. Computer 32(7), 38–45 (1999) 2. B¨ uchi, M., Weck, W.: Generic Wrappers. In: Bertino, E. (ed.) ECOOP 2000. LNCS, vol. 1850, pp. 201–225. Springer, Heidelberg (2000)
334
M. Rytter and B.N. Jørgensen
3. Clifton, C., Leavens, G., Chambers, C., Millstein, T.: MultiJava: Modular Open Classes and Symmetric Multiple Dispatch for Java. In: OOPSLA 2000 – Proceedings of the 15th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications, pp. 130–145 (2000) 4. Gamma, E., Helm, R., Johnson, R., Vlissides, J.: Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional, Reading (1994) 5. Hogg, J., Lea, D., Wills, A.: deChampeaux, D., Holt, R.: The Geneva Convention – On The Treatment of Object Aliasing. ACM SIGPLAN OOPS Messenger 3(2), 11–16 (1992) 6. Jørgensen, B.: Integration of Independently Developed Components through Aliased Multi-Object Type Widening. Journal of Object Technology 3(11), 55–76 (2004) 7. Kiczales, G., Hilsdale, E., Hugunin, J., Kersten, M., Palm, J., Griswold, W.: An Overview of AspectJ. In: Knudsen, J.L. (ed.) ECOOP 2001. LNCS, vol. 2072, pp. 327–354. Springer, Heidelberg (2001) 8. Kircher, M., Jain, P.: Pattern-Oriented Software Architecture. Patterns for Resource Management, vol. 3. Wiley, Chichester (2004) 9. Lehman, M.: Programs, Life Cycles, and Laws of Software Evolution. Proceedings of the IEEE 68, 1060–1076 (1980) 10. McGregor, J.: Context. Journal of Object Technology 4(7), 35–44 (2005) 11. Oreizy, P., Taylor, R.: Coping with Application Inconsistency in Decentralized Software Evolution. In: International Workshop on the Principles of Software Evolution (1999) 12. Ostermann, K., Kniesel, G.: Independent Extensibility – An Open Challenge for AspectJ and Hyper/J. In: ECOOP 2000 – Workshop on Aspects and Dimension of Concerns (2000) 13. Steimann, F.: The Paradoxical Success of Aspect-Oriented Programming. In: OOPSLA 2006 – Proceedings of the 21st Annual ACM SIGPLAN Conference on ObjectOriented Programming Systems, Languages, and Applications, pp. 481–497 (2006) 14. Szyperski, C.: Independently Extensible Systems – Software Engineering Potential and Challenges. In: Proceedings of the 19th Australasian Computer Science Conference (1996) 15. Szyperski, C.: Component Software – Beyond Object-Oriented Programming, 2nd edn. Addison-Wesley Professional, Reading (2002) 16. Tarr, P., Ossher, H., Sutton, S.: Hyper/JTM : Multi-Dimensional Separation of Concerns for JavaTM . In: Proceedings of the 24th International Conference on Software Engineering, pp. 689–690 (2002) 17. Weck, W.: Independently Extensible Component Frameworks. Special Issues in Object-Oriented Programming, pp. 177–183 (1997)