Chapter 7, Part 1: Monolithic Frameworks Exploring dependencies Notation Impact Exercises Break Dependencies Cycles & circular dependencies?? Goal of having a small set of key objects Java packages don’t support other dependency structures directly. Monolithic frameworksA framework is monolithic ("one rock") when it is constructed such that if you need one piece, you need all pieces. This is the easiest form to develop: you have a flat space of packages (or objects), and add whatever you need, letting new packages depend on existing packages in whatever way is convenient. Although it’s the easiest to develop, it’s the hardest to understand. This manifests itself as, “You need to understand everything before you can understand anything.” The framework is a Gordian knot. Who would build a framework that’s hard to understand? Lots of people. There are times when it is hard to separate out objects and organize their dependencies (in the ways we’ll explore moving forward). For example, let’s look at the core Java libraries. You might like to say, “What are the essential objects?” and include only them. (This is an issue faced by reduced subsets of Java such as Embedded Java.) Exploring DependenciesTo figure out what objects or packages depend on each other,
we need to think like a tool from olden times: a linker. In languages like C, a
program is assembled from a number of objects files and libraries (which
contain more object files). To construct a program for execution, a linker will
explore the way object files depend on each other, and include the main
routine, and everything it depends on (directly or indirectly), such as math,
I/O, etc. This is known as the “transitive closure”: transitive because it
considers indirect dependencies, closure because it finds them all. The reason for doing this is to be smart about programs’ requirements. If a program doesn’t need I/O, we don’t want to waste space including I/O methods that won’t be used. Why is this idea important to Java? After all, in Java we have a ClassLoader that loads exactly the needed classes, on demand. We use the idea of a linker because it provides a static analysis: what classes or packages could you need? We’d like to think this through at compile time so we can know the dependencies implied by our code.
When we do dependency analysis, we have a choice of the level of granularity: are we exploring applications, packages, or objects? In looking at Java programs, we typically consider things at the package level, but there are other choices.
Another dimension of our analysis is protection level. What types of access imply a dependency? In Java, we typically consider public, public + protected, or all. “Public” looks at things from the perspective of a client. “Public + protected” looks at things from the perspective of a subclass. “All” considers the implementation as well.
Suppose we, like the creators of Embedded Java, want to understand the minimum required packages to support a subset of Java. Let’s do an analysis at the package level, considering public + protected dependencies. Clearly there’s one class we know we absolutely must have: java.lang.Object. We look at each of its methods (it has no superclass or interfaces). Most methods return simple types (such as int), which we’ll assume as given. But, getClass() returns a java.lang.Class, and toString() returns a java.lang.String. The wait(), clone(), and finalize() methods introduce InterruptedException, CloneNotSupportedException, and Throwable. If our analysis were at the class level, we’d include each of those classes and their dependencies. Here, we’re working at the package level, so once any class in a package is mentioned, all classes in that package are pulled in. Since we have java.lang.Object, we’ll include java.lang.*. This isn’t wholly unexpected: java.lang’s classes are so pervasive, we don’t have to import them explicitly. So what else is in java.lang? We see a lot of exception classes, which mostly depend on each other and String. We also have wrapper classes for the basic types (such as Integer or Long). These don’t add anything new, nor do Compiler, Thread, or ThreadGroup. Then we get to the troublemakers: Class: java.lang.reflect.Constructor, .Field, and .Method; java.net.URL; java.io.InputStream ClassLoader: java.net.URL, java.io.InputStream Process: java.io.InputStream, .OutputStream RunTime: java.io.InputStream, .OutputStream SecurityManager: java.net.InetAddress; java.io.FileDescriptor System: java.io.InputStream, .PrintStream; java.util.Properties This brings in new packages: java.io, java.lang.reflect, java.net, and java.util. (We determined this by going through the class definitions.) Should we have expected this? Pretty much. Network access is built right into Java, so the occurrence of I/O and URLs seems natural. Java has reflection for all objects, so this is appropriate too. Finally, utility packages are common, so that’s fine too. We’re not done though: the layer also needs to include what those packages need. Java.io: These classes depend on basic types, String, java.lang, or each other. Java.lang.reflect: Nothing new; depends on java.lang. Java.net: Depends on java.lang and java.io but introduces nothing new. Java.util: Depends on java.lang and java.io but introduces nothing new. Think how lucky we were with the java.util package. This is a grab-bag package: containers, random number generator, observer, time/date, etc. If any of these classes (essentially having nothing to do with Object per se) had required other packages, we’d have had to pull them in as well. java.lang | java.lang.reflect | java.io | java.util Where does awt fit? The java.awt package needs java.awt.event and java.awt.peer, java.awt.datatransfer, and java.awt.image. The java.awt.applet package only depends on java.awt directly. The java.beans, java.math, java.text, and java.util.zip packages are each pretty self-contained. Awt (applet) || beans || math || text || zip How should we layer these? Since they don’t really depend on each other, it isn’t right to have awt above zip, or vice versa. Putting them in the same layer is OK, but it couples the packages together: +-----------------------------------------------------------------------+ | applet | | | |
| || ||
|| || | +--------+ | event | peer | dt | image || beans || math || text || zip | |
awt | |
| | || || ||
|| | +=======================================================================+ |
lang | reflect
| io | util | +-------------+-------------+--------+----------------------------------+ You will get a different analysis when you consider the implementation level. This can reveal that while your design (from a client or subclass perspective) is not problematic, your implementation can bring you problems. For example, the C standard library is divided into two parts: functions all runtimes must support (even realtime systems) and those needed for “full” systems. The first group allows embedded systems to use a function like memcpy() without necessarily bringing in memory management or I/O. There is a function atoi() in stdlib.h to convert strings to integers. A simple implementation is "sscanf (str, "%s", &i);". But if you do this, you find that sscanf() brings in all of the file handling, sprintf, etc. - the whole I/O system. So, you expected a 100 byte cost for using atoi() - instead it costs 10K bytes (and 10K still matters in some environments). So a poor implementation choice undid the careful design delineated in the standard. Notation and Naming SchemesConsider a diagram showing the dependency structure of a set of packages. For example:
When we want to show this in a linear form, we can draw it as a set of boxes: [ A | B | C | D | E ] The vertical line separating the names should be viewed as porous: [A|B] means A and B may depend on each other. It could be A depending on B, or B on A, or both, or A and B independent. If we have [A | B | C], we might have A dependent on B or C, or vice versa. (That is, it doesn’t make a claim about the actual dependency.) Sometimes, the dependency graph will be two or more separable pieces:
In that case, we’ll use a double bar (||) to identify the separate parts, like this: [ A | B | C || D | E ] Note: Some people don’t use the double-bar convention. For some people, the single bar may imply independent parts. You’ll see a lot of diagrams with lines and boxes; be sure to know the conventions in use. How should packages be named for monolithic frameworks? Since the structure is flat, use a flat naming scheme. For example, [A | B | C] might have packages com.xyz.a, com.xyz.b, and com.xyz.c. In the case of separable graphs, try to find a meaningful name for the parts, and group them under a common name. For [A | B || C], we might have com.xyz.biz.a, com.xyz.biz.b, and com.xyz.test.c. Impact of Monolithic FrameworksThe monolithic approach has two potential benefits: · It provides an infrastructure that the whole program can depend on. In C programs, it was common for each program to have its own approach to memory management, exceptions, etc. In C++, persistence schemes vary from program to program. In Java, we can rely on common memory management, I/O, persistence, exceptions, etc. The Java environment provides a lot of infrastructure: approaches that might have clashed are handled in a standard way. · The monolithic approach is simple to develop. It doesn’t require a lot of planning or coordination to use any feature already in the application. These benefits are offset by some severe liabilities. Even though programs may start out using the monolithic approach, it’s not the best way to grow. Because parts can depend on each other with no structuring mechanism, they will tend to be more highly coupled in a way that makes it hard to change one part without affecting many others. It’s also hard to use just one piece: because the pieces can depend on each other in an unstructured way, you have the “plate of spaghetti” problem: pull on a part over here, and something twitches over there. Furthermore, the “learn everything to learn anything” aspect is clearly a problem when you begin to work with a monolith. Remember, the new programmer could also be you six months from now. Is it appropriate to be monolithic? Not for a program of any size, or one that intends to grow. You need to be aware of the approach, because many frameworks will reflect a monolithic attitude, and so you’re aware when your own software falls into this trap. |
|
Copyright 1994-2006, William C. Wake - William.Wake@acm.org |