程式扎記: [ In Action ] Dynamic object orientation - Organizing classes and scripts

標籤

2014年2月23日 星期日

[ In Action ] Dynamic object orientation - Organizing classes and scripts

Preface: 
You saw that Groovy classes are Java classes at the bytecode level, and consequently, Groovy objects are Java objects in memory. At the source-code level, Groovy class and object handling is almost a superset of the Java syntax, with the exception of nested classes that are currently not supported by the Groovy syntax and some slight changes to the way arrays are defined. We will examine the organization of classes and source files, and the relationships between the two. We will also consider Groovy’s use of packages and type aliasing, as well as demystify where Groovy can load classes from in its classpath. 

File to class relationship: 
The relationship between files and class declarations is not as fixed as in Java. Groovy files can contain any number of public class declarations: 
* If a Groovy file contains no class declaration, it is handled as a script; that is, it is transparently wrapped into a class of type Script . This automatically generated class has the same name as the source script filename (without the extension). The content of the file is wrapped into a run method, and an additional main method is constructed for easily starting the script.
* If a Groovy file contains exactly one class declaration with the same name as the file (without the extension), then there is the same one-to-one relationship as in Java.
* A Groovy file may contain multiple class declarations of any visibility, and there is no enforced rule that any of them must match the filename. The groovyccompiler happily creates *.class files for all declared classes in such a file. If you wish to invoke your script directly, for example using groovy on the command line or within an IDE, then the first class within your file should have a main method.
* A Groovy file may mix class declarations and scripting code. In this case, the scripting code will become the main class to be executed, so don’t declare a class yourself having the same name as the source filename.

When not compiling explicitly, Groovy finds a class by matching its name to a corresponding *.groovy source file. At this point, naming becomes important. Groovy only finds classes where the class name matches the source filename. When such a file is found, all declared classes in that file are parsed and become known to Groovy. 

Listing 7.11 shows a sample script with two simple classes, Vendor and Address . For the moment, they have no methods, only public fields. 
- Listing 7.11 Multiple class declarations in one file 
  1. class Vendor {  
  2.     public String     name  
  3.     public String     product  
  4.     public Address    address = new Address()  
  5. }  
  6. class Address  {  
  7.     public String     street, town, state  
  8.     public int        zip  
  9. }  
  10. def canoo = new Vendor()  
  11. canoo.name            = 'Canoo Engineering AG'  
  12. canoo.product         = 'UltraLightClient (ULC)'  
  13. canoo.address.street  = 'Kirschgartenst. 7'  
  14. canoo.address.zip     =  4051  
Vendor and Address are simple data storage classes. They are roughly equivalent to structs in C or Pascal records. We will soon explore more elegant ways of defining such classes. 

Listing 7.11 illustrates a convenient convention supported by Groovy’s source file to class mapping rules, which we discussed earlier. This convention allows small helper classes that are used only with the current main class or current script to be declared within the same source file. Compare this with Java, which allows you to use nested classes to introduce locally used classes without cluttering up your public class namespace or making navigation of the codebase more difficult by requiring a proliferation of source code files. Although it isn’t exactly the same, this convention has similar benefits for Groovy developers. 

Organizing classes in packages: 
Groovy follows Java’s approach of organizing files in packages of hierarchical structure. The package structure is used to find the corresponding class files in the filesystem’s directories. Because *.groovy source files are not necessarily compiled to *.class files, there is also a need to look up *.groovy files. When doing so, the same strategy is used: The compiler looks for a Groovy class Vendor in the business package in the file business/Vendor.groovy

In listing 7.12, we separate the Vendor and Address classes from the script code, as shown in listing 7.11, and move them to the inaction.ch7.business package. 
- Listing 7.12 Vendor and Address classes moved to the inaction.ch7.business package 
  1. package inaction.ch7.business  
  2.   
  3. class Vendor {  
  4.     public String  name  
  5.     public String  product  
  6.     public Address address = new Address()  
  7. }  
  8. class Address  {  
  9.     public String  street, town, state  
  10.     public int     zip  
  11. }  
Classpath 
The lookup has to start somewhere, and Java uses its classpath for this purpose. The classpath is a list of possible starting points for the lookup of *.class files. Groovy reuses the classpath for looking up *.groovy files. When looking for a given class, if Groovy finds both a *.class and a *.groovy file, it uses whichever is newer; that is, it will recompile source files into *.class files if they have changed since the previous class file was compiled. 

Packages 
Exactly like in Java, Groovy classes must specify their package before the class definition. When no package declaration is given, the default package is assumed. Listing 7.12 shows the file business/Vendor.groovy, which has a package statement as its first line. To reference Vendor in the inaction.ch7.business package, you can either useinaction.ch7.business.Vendor within the code or use imports for abbreviation. 

Imports 
Groovy follows Java’s notion of allowing import statements before any class declaration to abbreviate class references. 
Note. 
Please keep in mind that unlike in some other scripting languages, import has nothing to do with literal inclusion of the imported class or file. It merely informs the compiler how to resolve references.

Listing 7.13 shows the use of the import statement, with the .* notation advising the compiler to try resolving all unknown class references against all classes in the inaction.ch7.business package. 
- Listing 7.13 Using import to access Vendor in the business package 
  1. import inaction.ch7.business.*  
  2.   
  3. def canoo = new Vendor()  
  4. canoo.name          = 'Canoo Engineering AG'  
  5. canoo.product       = 'UltraLightClient (ULC)'  
  6. assert canoo.dump() =~ /ULC/  
Note. 
By default, Groovy imports six packages and two classes, making it seem like every groovy code program contains the following initial statements:
  1. import java.lang.*  
  2. import java.util.*   
  3. import java.io.*   
  4. import java.net.*  
  5. import groovy.lang.*  
  6. import groovy.util.*  
  7. import java.math.BigInteger  
  8. import java.math.BigDecimal  

Type aliasing 
The import statement has another nice twist: together with the as keyword, it can be used for type aliasing. Whereas a normal import allows a fully qualified class to be referred to by its base name, a type alias allows a fully qualified class to be referred to by a name of your choosing. This feature resolves naming conflicts and supports local changes or bug fixes to a third-party library. 

Consider the following library class: 
  1. package thirdparty  
  2. class MathLib {  
  3.     Integer twice(Integer value) {  
  4.         return value * 3         // intentionally wrong!        
  5.     }  
  6.     Integer half(Integer value) {  
  7.         return value / 2  
  8.     }  
  9. }  
Note its obvious error (although in general it might not be an error but just a locally desired modification). Suppose now that we have some existing code that uses that library: 
  1. assert 10 == new MathLib().twice(5)  
We can use a type alias to rename the old library and then use inheritance to make a fix. No change is required to the original code that was using the library, as you can see in listing 7.14. 
- Listing 7.14 Using import as for local library modifications 
  1. import thirdparty.MathLib as OrigMathLib  
  2.   
  3. class MathLib extends OrigMathLib {  
  4.     Integer twice(Integer value) {  
  5.         return value * 2  
  6.     }  
  7. }  
  8.   
  9. // nothing changes below here: Usage code for library remains unchanged     
  10. def mathlib = new MathLib()    
  11. assert 10 == mathlib.twice(5)   // Invoke fixed method  
  12. assert 2 == mathlib.half(5)     // Invoke original method  
Now, suppose that we have the following additional math library that we need to use: 
  1. package thirdparty2  
  2.   
  3. class MathLib {  
  4.     Integer increment(Integer value) {  
  5.         return value + 1  
  6.     }  
  7. }  
Although it has a different package, it has the same name as the previous library. Without aliasing, we have to fully qualify one or both of the libraries within our code. With aliasing, we can avoid this in an elegant way and also improve communication by better indicating intent within our program about the role of the third-party library’s code, as shown in listing 7.15. 
- Listing 7.15 Using import as for avoiding name clashes 
  1. import thirdparty.MathLib as TwiceHalfMathLib  
  2. import thirdparty2.MathLib as IncMathLib  
  3.   
  4. def math1 = new TwiceHalfMathLib()  
  5. def math2 = new IncMathLib()  
  6. assert 3 == math1.half(math2.increment(5))  
You should consider using aliases within your own program, even when using simple built-in types. For example, if you are developing an adventure game, you might aliasMap to SatchelContents. This doesn’t provide the strong typing that defining a separate SatchelContents class would give, but it does greatly improve the human understandability of the code

Further classpath considerations: 
Finding classes in *.class and *.groovy files is an important part of working with Groovy, and unfortunately a likely source of problems. If you installed the J2SDK including the documentation, you will find the classpath explanation under %JAVA_HOME%/docs/tooldocs/windows/classpath.html under Windows, or under a similar directory for Linux and Solaris. Everything the documentation says equally applies to Groovy. 

A number of contributors can influence the effective classpath in use. The overview in table 7.1 may serve as a reference when you’re looking for a possible bad guy that’s messing up your classpath. 
 

Groovy defines its classpath in a special configuration file under %GROOVY_ HOME%/conf. Looking at the file groovy-starter.conf reveals the following lines (beside others): 
  1. # Load required libraries  
  2. load ${groovy.home}/lib/*.jar  
  3. # load user specific libraries  
  4. # load ${user.home}/.groovy/lib/*  
Uncommenting the last line by removing the leading hash sign enables a cool feature. In your personal home directory user.home, you can use a subdirectory .groovy/lib, where you can store any *.class or *.jar files that you want to have accessible whenever you work with Groovy. 

If you have problems finding your user.home, open a command shell and execute: 
> groovy -e "println System.properties.'user.home'"

Chances are, you are in this directory by default anyway. 

Chapter 11 goes through more advanced classpath issues that need to be respected when embedding Groovy in environments that manage their own classloading infrastructure—for example an application server

Supplement: 
User Guide > Scripts and Classes

沒有留言:

張貼留言

網誌存檔