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

標籤

2014年2月20日 星期四

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

Preface:
The scripting landscape has changed dramatically. Perl has added support for object orientation, Python has extended its object-oriented support, and more recently Ruby has made a name for itself as a full-fledged dynamic object-oriented scripting language with significant productivity benefits when compared to Java and C++.

Groovy follows the lead of Ruby by offering these dynamic object orientation features. Not only does it enhance Java by making it scriptable, but it also provides new OO features. You have already seen that Groovy provides reference types in cases where Java uses non-object primitive types, introduces ranges and closures as first-class objects, and has many shorthand notations for working with collections of objects. But these enhancements are just scratching the surface. If this were all that Groovy had to offer, it would be little more than syntactic sugar over normal Java. What makes Groovy stand apart is its set of dynamic features.

Class definition in Groovy is almost identical to Java; classes are declared using the class keyword and may contain fieldsconstructorsinitializers, and methods. Methods and constructors may themselves use local variables as part of their implementation code. Scripts are different—offering additional flexibility but with some restrictions too. They may contain code, variable definitions, and method definitions as well as class definitions. We will describe how all of these members are declared and cover a previously unseen operator on the way.

Defining fields and local variables:
In its simplest terms, a variable is a name associated with a slot of memory that can hold a value. Just as in Java, Groovy has local variables, which are scoped within the method they are part of, and fields, which are associated with classes or instances of those classes. Fields and local variables are declared in much the same way, so we cover them together.

Declaring variables
Fields and local variables must be declared before first use (except for a special case involving scripts, which we discuss later). This helps to enforce scoping rules and protects the programmer from accidental misspellings. The declaration always involves specifying a name, and may optionally include a type, modifiers, and assignment of an initial value. Once declared, variables are referenced by their name.

Scripts allow the use of undeclared variables, in which case these variables are assumed to come from the script’s binding and are added to the binding if not yet there. The binding is a data store that enables transfer of variables to and from the caller of a script. Section 11.3.2 has more details about this mechanism.

Groovy uses Java’s modifiers—the keywords private , protected , and public for modifying visibility; 2 final for disallowing reassignment; and static to denote class variables. A nonstatic field is also known as an instance variable. These modifiers all have the same meaning as in Java.

The default visibility for fields has a special meaning in Groovy. When no visibility modifier is attached to field declaration, a property is generated for the respective name. You will learn more about properties in section 7.4 when we present GroovyBeans.

Defining the type of a variable is optional. However, the identifier must not stand alone in the declaration. When no type and no modifier are given, the def keyword must be used as a replacement, effectively indicating that the field or variable is untyped (although under the covers it will be declared as type Object). Listing 7.1 depicts the general appearance of field and variable declarations with optional assignment and using a comma-separated list of identifiers to declare multiple references at once.
- Listing 7.1 Variable declaration examples
  1. class SomeClass {     
  2.     public    fieldWithModifier  
  3.     String    typedField  
  4.     def       untypedField  
  5.     protected field1, field2, field3  
  6.     private   assignedField = new Date()  
  7.       
  8.     static    ClassField  
  9.       
  10.     public static final String CONSTA = 'a', CONSTB = 'b'  
  11.       
  12.     def someMethod(){  
  13.         def localUntypedMethodVar = 1  
  14.         int localTypedMethodVar = 1  
  15.         def localVarWithoutAssignment, andAnotherOne  
  16.     }  
  17. }  
  18. def localvar = 1  
  19. boundvar1 = 1  
  20.                              
  21. def someMethod(){  
  22.     localMethodVar = 1  
  23.     boundvar2 = 1  
  24. }  
Assignments to typed references must conform to the type—that is, you cannot assign a number to a reference of type String or vice versa. You saw in chapter 3 that Groovy provides autoboxing and coercion when it makes sense. All other cases are type-breaking assignments and lead to a ClassCastException at runtime, as can be seen in listing 7.2.
- Listing 7.2 Variable declaration examples
  1. class Test{  
  2.     final static String PI = 3.14                 
  3.       
  4.     static void main(args)  
  5.     {             
  6.         assert PI.class.name == 'java.lang.String'  
  7.         assert PI.length() == 4  
  8.         new GroovyTestCase().shouldFail(ClassCastException.class){  
  9.             Float areaOfCircleRadiousOne = PI       // Cannot assign String to typed variable as Float  
  10.         }  
  11.     }  
  12. }  
The shouldFail method as used in this example checks that a ClassCastException occurs. More details can be found in section 14.3.

Referencing and dereferencing fields
In addition to referring to fields by name with the obj.fieldName syntax, they can also be referenced with the subscript operator, as shown in listing 7.3. This allows you to access fields using a dynamically determined name.
- Listing 7.3 Referencing fields with the subscript operator
  1. class Counter {  
  2.     public count = 0  
  3. }  
  4. def counter = new Counter()  
  5. counter.count = 1  
  6. assert counter.count == 1  
  7. def fieldName = 'count'  
  8. counter[fieldName] = 2  
  9. assert counter['count'] == 2  
  10. assert counter.count == 2  
If you worked through the Groovy datatype descriptions, your next question will probably be, “can I override the subscript operator?” Sure you can, and you will extendbut not override the general field-access mechanism that way. But you can do even better and extend the field access operator!

Listing 7.4 shows how to do that. To extend both set and get access, provide the methods
  1. Object get (String name)  
  2. void   set (String name, Object value)  
There is no restriction on what you do inside these methods; get can return artificial values, effectively pretending that your class has the requested field. In listing 7.4, the same value is always returned, regardless of which field value is requested. The set method is used for counting the write attempts.
- Listing 7.4 Extending the general field-access mechanism
  1. class PretendFieldCounter {  
  2.     public count = 0  
  3.     @Override  
  4.     Object get (String name) {  
  5.         return 'pretend value'  
  6.     }  
  7.     @Override  
  8.     void set (String name, Object value) {  
  9.         count++  
  10.     }  
  11. }  
  12. def pretender = new PretendFieldCounter()  
  13. assert pretender.isNoField == 'pretend value'  
  14. assert pretender.count     == 0  // Field 'count' exist, so get method won't be called!  
  15. pretender.isNoFieldEither  = 'just to increase counter'  
  16. assert pretender.count     == 1  
With the count field, you can see that it looks like the get/set methods are not used if the requested field is present. This is true for our special case. Later, in section 7.4, you will see the full set of rules that produces this effect. Generally speaking, overriding the get method means to override the dot-fieldname operator. Overriding theset method overrides the field assignment operator.
FOR THE GEEKS:
What about a statement of the form x.y.z=something ? This is equivalent to getX().getY().setZ(something).

Referencing fields is also connected to the topic of properties, which we will explore in section 7.4, where we will discuss the need for the additional obj.@fieldNamesyntax.

Methods and parameters
Method declarations follow the same concepts you have seen for variables: The usual Java modifiers can be used; declaring a return type is optional; and, if no modifiers or return type are supplied, the def keyword fills the hole. When the def keyword is used, the return type is deemed to be untyped (although it can still have no return type, the equivalent of a void method). In this case, under the covers, the return type will be java.lang.Object . The default visibility of methods is public.

Listing 7.5 shows the typical cases in a self-describing manner.
- Listing 7.5 Declaring methods
  1. class TestClass {  
  2.     static void main(args) {  
  3.         def some = new TestClass()  
  4.         some.publicVoidMethod()  
  5.         assert 'hi' == some.publicUntypedMethod()  
  6.         assert 'ho' == some.publicTypedMethod()  
  7.         combinedMethod() // Call static method of current class  
  8.     }  
  9.     void publicVoidMethod(){}  
  10.     def publicUntypedMethod(){return 'hi'}  
  11.     String publicTypedMethod(){return 'ho'}  
  12.     protected static final void combinedMethod(){}  
  13. }  
The main method b has some interesting twists. First, the public modifier can be omitted because it is the default. Second, args usually has to be of type String[] in order to make the main method the one to start the class execution. Thanks to Groovy’s method dispatch, it works anyway, although args is now implicitly of static typejava.lang.Object . Third, because return types are not used for the dispatch, we can further omit the void declaration.

NOTE.
The Java compiler fails on missing return statements when a return type is declared for the method. In Groovy, return statements are optional, and therefore it’s impossible for the compiler to detect “accidentally” missing returns.

The main(args) example illustrates that declaring explicit parameter types is optional. When type declarations are omitted, Object is used. Multiple parameters can be used in sequence, delimited by commas. Listing 7.6 shows that explicit and omitted parameter types can also be mixed.
- Listing 7.6 Declaring parameter lists
  1. class TestClass2 {  
  2.     static void main (args){  
  3.         assert 'untyped' == method(1)  
  4.         assert 'typed'   == method('whatever')  
  5.         assert 'two args'== method(1,2)  
  6.     }  
  7.     static method(arg) {return 'untyped'}  
  8.     static method(String arg){return 'typed'}  
  9.     static method(arg1, Number arg2){return 'two args'}  
  10. }  
In the examples so far, all method calls have involved positional parameters, where the meaning of each argument is determined from its position in the parameter list. This is easy to understand and convenient for the simple cases you have seen, but suffers from a number of drawbacks for more complex scenarios:
* You must remember the exact sequence of the parameters, which gets increasingly difficult with the length of the parameter list.
* If it makes sense to call the method with different information for alternative usage scenarios, different methods must be constructed to handle these alternatives.This can quickly become cumbersome and lead to a proliferation of methods, especially where some parameters are optional. It is especially difficult if many of the optional parameters have the same type. Fortunately, Groovy comes to the rescue with using maps as named parameters.

Note.
Whenever we talk about named parameters, we mean keys of a map that is used as an argument in method or constructor calls. From a programmer’s perspective, this looks pretty much like native support for named parameters, but it isn’t. This trick is needed because the JVM does not support storing parameter names in the bytecode.

Listing 7.7 illustrates Groovy method definitions and calls supporting positional and named parameters, parameter lists of variable length, and optional parameters with default values. The example provides four alternative summing mechanisms, each highlighting different approaches for defining the method call parameters.
- Listing 7.7 Advanced parameter usages
  1. class Summer {  
  2.     // 1) Explicit arguments and a default value  
  3.     def sumWithDefaults(a, b, c=0){  
  4.         return a + b + c  
  5.     }  
  6.       
  7.     // 2) Define arguments as a list  
  8.     def sumWithList(List args){  
  9.         return args.inject(0){sum,i -> sum += i}  
  10.     }  
  11.       
  12.     // 3) Optional arguments as an array  
  13.     def sumWithOptionals(a, b, Object[] optionals){  
  14.         return a + b + sumWithList(optionals.toList())  
  15.     }  
  16.       
  17.     // 4) Define arguments as a map  
  18.     def sumNamed(Map args){  
  19.         ['a','b','c'].each{args.get(it,0)}  
  20.         return args.a + args.b + args.c  
  21.     }  
  22. }  
  23.     
  24. def summer = new Summer()  
  25. assert 2 == summer.sumWithDefaults(1,1)  
  26. assert 3 == summer.sumWithDefaults(1,1,1)  
  27.   
  28. assert 2 == summer.sumWithList([1,1])  
  29. assert 3 == summer.sumWithList([1,1,1])  
  30.     
  31. assert 2 == summer.sumWithOptionals(1,1)  
  32. assert 3 == summer.sumWithOptionals(1,1,1)  
  33.   
  34. assert 2 == summer.sumNamed(a:1, b:1)  
  35. assert 3 == summer.sumNamed(a:1, b:1, c:1)  
  36. assert 1 == summer.sumNamed(c:1)  
All four alternatives have their pros and cons. In (1)sumWithDefaults , we have the most obvious declaration of the arguments expected for the method call. It meets the needs of the sample script—being able to add two or three numbers together—but we are limited to as many arguments as we have declared parameters.

Using lists as shown in (2) is easy in Groovy, because in the method call, the arguments only have to be placed in brackets. We can also support argument lists of arbitrary length. However, it is not as obvious what the individual list entries should mean. Therefore, this alternative is best suited when all arguments have the same meaning, as they do here where they are used for adding. Regarding the inject method, it will iterates through the given Collection, passing in the initial value to the 2-arg closure along with the first item.

The sumWithOptionals method at (3) can be called with two or more parameters. To declare such a method, define the last argument as an arrayGroovy’s dynamic method dispatch bundles excessive arguments into that array.

Named arguments can be supported by using a map as in (4)It is good practice to reset any missing values to a default before working with them. This also better reveals what keys will be used in the method body, because this is not obvious from the method declaration.

When designing your methods, you have to choose one of the alternatives. You may wish to formalize your choice within a project or incorporate the Groovy coding style.
Note.
There is a second way of implementing parameter lists of variable length. You can hook into Groovy’s method dispatch by overriding the invokeMethod(name, params[]) that every GroovyObject provides. You will learn more about these hooks in section 7.6.2.

Advanced naming
When calling a method on an object reference, we usually follow this format:
objectReference.methodName()

This format imposes the Java restrictions for method names; for example, they may not contain special characters such as minus ( - ) or dot ( . ). However, Groovy
allows you to use these characters in method names if you put quotes around the name:
objectReference.'my.method-Name'()

The purpose of this feature is to support usages where the method name of a call becomes part of the functionality. You won’t normally use this feature directly, but it will be used under the covers by other parts of Groovy. You will see this in action in chapter 8 and chapter 10.
FOR THE GEEKS.
Where there’s a string, you can generally also use a GString. So how about obj."${var}"() ? Yes, this is also possible, and the GString will be resolved to determine the name of the method that is called on the object!

Safe dereferencing with the ?. operator:
When a reference doesn’t point to any specific object, its value is null . When calling a method or accessing a field on a null reference, a NullPointerException (NPE) is thrown. This is useful to protect code from working on undefined preconditions, but it can easily get in the way of “best effort” code that should be executed for valid references and just be silent otherwise.

Listing 7.8 shows several alternative approaches to protect code from NPEs. As an example, we wish to access a deeply nested entry within a hierarchy of maps, which results in a path expressiona dotted concatenation of references that is typically cumbersome to protect from NPEs. We can use explicit if checks or use the try-catch mechanism. Groovy provides the additional ?. operator for safe dereferencing. When the reference before that operator is a null reference, the evaluation of the current expression stops, and null is returned.
- Listing 7.8 Protecting from NullPointerExceptions using the ?. operator
  1. def map = [a:[b:[c:1]]]  
  2. assert map.a.b.c == 1  
  3.   
  4. // 1) Protect with if: short-circuit evaluation  
  5. if (map && map.a && map.a.x){  
  6.     assert map.a.x.c == null  
  7. }  
  8.   
  9. // 2) Protect with try/catch  
  10. try {  
  11.     assert map.a.x.c == null  
  12. catch (NullPointerException npe){npe.printStackTrace()}  
  13.   
  14. // 3) Safe dereferencing  
  15. assert map?.a?.x?.c == null  
Some software engineers like to think about code in terms of cyclomatic complexity, which in short describes code complexity by analyzing alternative pathways through the code. The safe dereferencing operator merges alternative pathways together and hence reduces complexity when compared to its alternatives; essentially, the metric indicates that the code will be easier to understand and simpler to verify as correct.

Constructors:
Objects are instantiated from their classes via constructors. If no constructor is given, an implicit constructor without arguments is supplied by the compiler. This appears to be exactly like in Java, but because this is Groovy, it should not be surprising that some additional features are available.

Previously, we examined the merits of named parameters versus positional ones, as well as the need for optional parameters. The same arguments applicable to method calls are relevant for constructors, too, so Groovy provides the same convenience mechanisms. We’ll first look at constructors with positional parameters, and then we’ll examine named parameters.

Positional parameters
Until now, we have only used implicit constructors. Listing 7.9 introduces the first explicit one. Notice that just like all other methods, the constructor is public by default. We can call the constructor in three different ways: the usual Java way, with enforced type coercion by using the as keyword, and with implicit type coercion.
- Listing 7.9 Calling constructors with positional parameters
  1. class VendorWithCtor {  
  2.     String name, product  
  3.       
  4.     // Constructor definition  
  5.     VendorWithCtor(name, product) {  
  6.         this.name    = name  
  7.         this.product = product  
  8.     }  
  9. }  
  10. // Normal constructor use  
  11. def first = new VendorWithCtor('Canoo','ULC')  
  12.   
  13. // 1) Coercion with as  
  14. def second = ['Canoo','ULC'] as VendorWithCtor  
  15.   
  16. // 2) Coercion in assignment  
  17. VendorWithCtor third = ['Canoo','ULC']  
The coercion in (1) and (2) may be surprising. When Groovy sees the need to coerce a list to some other type, it tries to call the type’s constructor with all arguments supplied by the list, in list order. This need for coercion can be enforced with the as keyword or can arise from assignments to statically typed references. The latter of these is called implicit construction, which we cover shortly.

Named parameters
Named parameters in constructors are handy. One use case that crops up frequently is creating immutable classes that have some parameters that are optional. Using positional parameters would quickly become cumbersome because you would need to have constructors allowing for all combinations of the optional parameters.

As an example, suppose in listing 7.9 that VendorWithCtor should be immutable and name and product can be optional. We would need four constructors: an empty one, one to set name , one to set product , and one to set both attributes. To make things worse, we couldn’t have a constructor with only one argument, because we couldn’t distinguish whether to set the name or the product attribute (they are both strings). We would need an artificial extra argument for distinction, or we would need to strongly type the parameters.

But don’t panic: Groovy’s special way of supporting named parameters comes to the rescue again. Listing 7.10 shows how to use named parameters with a simplified version of the Vendor class. It relies on the implicit default constructor. Could that be any easier?
- Listing 7.10 Calling constructors with named parameters
  1. class Vendor {  
  2.     String name, product  
  3. }  
  4. new Vendor()  
  5. new Vendor(name:   'Canoo')  
  6. new Vendor(product:'ULC')  
  7. new Vendor(name:   'Canoo', product:'ULC')  
  8. def vendor = new Vendor(name: 'Canoo')  
  9. assert 'Canoo' == vendor.name  
The example in listing 7.10 illustrates how flexible named parameters are for your constructors. In cases where you don’t want this flexibility and want to lock down all of your parameters, just define your desired constructor explicitly; the implicit constructor with named parameters will no longer be available.

Coming back to how we started this section, the empty default constructor call new Vendor() appears in a new light. Although it looks exactly like its Java equivalent, it is a special case of the default constructor with named parameters that happens to be called without any being supplied. (只要有空 constructor 存在, 就可以使用 named parameters constructor!)

Implicit constructors
Finally, there is a way to call a constructor implicitly by simply providing the constructor arguments as a list. That means that instead of calling the Dimension(width, height) constructor explicitly, for example, you can use:
  1. java.awt.Dimension area   
  2. area = [200100]  
  3. assert area.width  == 200  
  4. assert area.height == 100  
Of course, Groovy must know what constructor to call, and therefore implicit constructors are solely available for assignment to statically typed references where the type provides the respective constructorThey do not work for abstract classes or even interfaces.

Implicit constructors are often used with builders, as you’ll see in the SwingBuilder example in section 8.5.7.

That’s it for the usual class members. This is a solid basis we can build upon. But we are not yet in the penthouse; we have four more levels to go. We walk through the topic of how to organize classes and scripts to reach the level of advanced object-oriented features. The next floor is named GroovyBeans and deals with simple object-oriented information about objects. At this level, we can play with Groovy’s power features. Finally, we will visit the highest level, which is meta programming in Groovy—making the environment fully dynamic, and responding to ordinary-looking method calls and field references in an extraordinary way.

Supplement:
Documentation > User Guide > Operators
In general all operators supported in Java are identical in Groovy. Groovy goes a step further by allowing you to customize behavior of operators on Groovy types...

GUI Programming with Groovy > SwingBuilder
SwingBuilder allows you to create full-fledged Swing GUIs in a declarative and concise fashion. It accomplishes this by employing a common idiom in Groovy, builders. Builders handle the busywork of creating complex objects for you, such as instantiating children, calling Swing methods, and attaching these children to their parents. As a consequence, your code is much more readable and maintainable, while still allowing you access to the full range of Swing components...

This message was edited 69 times. Last update was at 20/02/2014 22:37:24

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!