程式扎記: [ In Action ] Groovy control structures - Conditional execution structures

標籤

2014年2月16日 星期日

[ In Action ] Groovy control structures - Conditional execution structures

Preface: 
Our first set of control structures deals with conditional execution. They all evaluate a Boolean test and make a choice about what to do next based on whether the result was true or false. None of these structures should come as a completely new experience to any Java developer, but of course Groovy adds some twists of its own. We will cover if statements, the conditional operator, switch statements, and assertions. 

The humble if statement: 
Our first two structures act exactly the same way in Groovy as they do in Java, apart from the evaluation of the Boolean test itself. We start with if and if/else statements. Just as in Java, the Boolean test expression must be enclosed in parentheses. The conditional block is normally enclosed in curly braces. These braces are optional if the block consists of only one statement. 

Below gives some examples, using assert true to show the blocks of code that will be executed and assert false to show the blocks that won’t be executed. 
- Listing 6.3 The if statement in action 
  1. if (true)        assert true  
  2. else             assert false  
  3. if (1) {  
  4.     assert true  
  5. else {  
  6.     assert false  
  7. }      
  8. if ('non-empty'assert true  
  9. else if (['x'])  assert false  
  10. else             assert false  
  11. if (0)           assert false  
  12. else if ([])     assert false  
  13. else             assert true  
The conditional ?: operator 
Groovy also supports the ternary conditional ?: operator for small inline tests, as shown in listing 6.4. This operator returns the object that results from evaluating the expression left or right of the colon, depending on the test before the question mark. If the first expression evaluates to true , the middle expression is evaluated. Otherwise, the last expression is evaluated. Just as in Java, whichever of the last two expressions isn’t used as the result isn’t evaluated at all. 
- Listing 6.4 The conditional operator 
  1. def result = (1==1) ? 'ok' : 'failed'  
  2. assert result == 'ok'  
  3. result = 'some string' ? 10 : ['x']  
  4. assert result == 10  
Again, notice how the Boolean test (the first expression) can be of any type. Also note that because everything is an object in Groovy, the middle and last expressions can be of radically different types. 

The switch statement: 
On a recent train ride, I (Dierk) spoke with a teammate about Groovy, mentioning the oh-so-cool switch capabilities. He wouldn’t even let me get started, waving his hands and saying, “I never use switch !” I was put off at first, because I lost my momentum in the discussion; but after more thought, I agreed that I don’t use it either—in Java. 

The switch statement in Java is very restrictive. You can only switch on an int type, with byte , char , and short automatically being promoted to int . With this restriction, its applicability is bound to either low-level tasks or to some kind of dispatching on a type code. In object-oriented languages, the use of type codes is considered smelly. 

The switch structure 
The general appearance of the switch construct is just like in Java, and its logic is identical in the sense that the handling logic falls through to the next case unless it is exited explicitly. We will explore exiting options in section 6.4. Listing 6.5 shows the general appearance. 
- Listing 6.5 General switch appearance is like Java or C 
  1. def a = 1  
  2. def log = ''  
  3. switch (a) {  
  4.     case 0  : log += '0'  
  5.     case 1  : log += '1'            // Fall through  
  6.     case 2  : log += '2' ; break    // Exited explicitly  
  7.     default : log += 'default'  
  8. }  
  9. assert log == '12'  
Although the fallthrough is supported in Groovy, there are few cases where this feature really enhances the readability of the code. It usually does more harm than good (and this applies to Java, too). As a general rule, putting a break at the end of each case is good style. 

Switch with classifiers 
You have seen the Groovy switch used for classification in section 3.5.5 and when working through the datatypes. A classifier is eligible as a switch case if it implements the isCase method. In other words, a Groovy switch like: 
  1. switch (candidate) {  
  2.     case classifier1  : handle1()      ; break  
  3.     case classifier2  : handle2()      ; break  
  4.     default           : handleDefault()  
  5. }  
is roughly equivalent (beside the fallthrough and exit handling) to 
  1. if      (classifier1.isCase(candidate)) handle1()  
  2. else if (classifier2.isCase(candidate)) handle2()  
  3. else     handleDefault()  
This allows expressive classifications and even some unconventional usages with mixed classifiers. Unlike Java’s constant cases, the candidate may match more than one classifier. This means that the order of cases is important in Groovy, whereas it does not affect behavior in Java. Listing 6.6 gives an example of multiple types of classifiers. After having checked that our number 10 is not zero, not in range 0..9 , not in list [8,9,11] , not of type Float , and not an integral multiple of 3 , we finally find it to be made of two characters. 
- Listing 6.6 Advanced switch and mixed classifiers 
  1. switch (10) {  
  2.     case 0          : assert false ; break  
  3.     case 0..9       : assert false ; break  
  4.     case [8,9,11]   : assert false ; break  
  5.     case {it%3 == 0}: assert false ; break                  // 2) Closure case  
  6.     case Float      : assert false ; break                  // 1) Type case   
  7.     case Number     : assert true  ; printf "I am Number\n" //    fall through next case without calling isCase  
  8.     case false      : assert true  ; printf "Fallthrough\n"       
  9.     case ~/../      : assert true  ; break                  // 3) Regular expression case  
  10.     default         : assert false ; break  
  11. }  
Note. 
在 switch 中只要有一個 case 被滿足, 而且沒有使用 break 來 exited explicitly, 則接下來的 case 都會直接執行而不用呼叫 isCase 進行比對!

In order to leverage the power of the switch construct, it is essential to know the available isCase implementations. It is not possible to provide an exhaustive list, because any custom type in your code or in a library can implement it. Table 6.2 has the list of known implementations in the GDK. 
 
Note. 這邊的 a 就是 classifier, b 則是傳入 switch 的值. 

Using the Groovy switch in the sense of a classifier is a big step forward. It adds much to the readability of the code. The reader sees a simple classification instead of a tangled, nested construction of if statements. Again, you are able to reveal what the code does rather than how it does it. 

Look actively through your code for places to implement isCase . A characteristic sign of looming classifiers is lengthy else if constructions. 
ADVANCED TOPIC. 
It is possible to overload the isCase method to support different kinds of classification logic depending on the type of the candidate. If you provide both methods,isCase(String candidate) and isCase(Integer candidate) , then switch('1') can behave differently than switch(1) with your object as classifier.


Sanity checking with assertions: 
We will look at producing meaningful error messages from failed assertions, reflect over reasonable uses of this keyword, and show how to use it for inline unit tests. We will also quickly compare the Groovy solution to Java’s assert keyword and assertions as used in unit test cases. 

Producing informative failure messages 
When an assertion fails, it produces a stacktrace and a message. Put the code 
  1. a = 1  
  2. assert a==2  
It is expected to fail, and it does so with the message 
 

You see that on failure, the assertion prints out the failed expression as it appears in the code plus the value of the variables in that expression. The trailing stacktrace reveals the location of the failed assertion and the sequence of method calls that led to the error. It is best read bottom to top
■ We are in the file Script.groovy.
■ From that file, a class Script was constructed with a method main .
■ Within main , we called Script.run , which is located in the file Script.groovy:4 of the file.
■ At that point, the assertion fails.

This is a lot of information, and it is sufficient to locate and understand the error in most cases, but not always. Let’s try another example that tries to protect a file reading code from being executed if the file doesn’t exist or cannot be read. 
  1. input = new File('no such file')  
  2. assert  input.exists()  
  3. assert  input.canRead()  
  4. println input.text  
This produces the output: 
 

which is not very informative. The missing information here is what the bad file name was. To this end, assertions can be instrumented with a trailing message
  1. input = new File('no such file')  
  2. assert input.exists()  , "cannot find '$input.name'"  
  3. assert input.canRead() , "cannot read '$input.canonicalPath'"  
  4. println input.text  
This produces the following: 
 

which is the information we need. However, this special case also reveals the sometimes unnecessary use of assertions, because in this case we could easily leave the assertions out: 
  1. input = new File('no such file')  
  2. println input.text  
The result is the following sufficient error message: 
 

This leads to the following best practices with assertions: 
■ Before writing an assertion, let your code fail, and see whether any other thrown exception is good enough.
■ When writing an assertion, let it fail the first time, and see whether the failure message is sufficient. If not, add a message. Let it fail again to verify that the message is now good enough. If you feel you need an assertion to clarify or protect your code, add it regardless of the previous rules.
■ If you feel you need a message to clarify the meaning or purpose of your assertion, add it regardless of the previous rules. 

Insure code with inline unit tests 
Finally, there is a potentially controversial use of assertions as unit tests that live right inside production code and get executed with it. Listing 6.7 shows this strategy with a nontrivial regular expression that extracts a hostname from a URL. The pattern is first constructed and then applied to some assertions before being put to action. We also implement a simple method assertHost for easy asserting of a match grouping. 
- Listing 6.7 Use assertions for inline unit tests 
  1. // Regular expression matching hosts  
  2. def host = /\/\/([a-zA-Z0-9-]+(\.[a-zA-Z0-9-])*?)(:|\/)/  
  3.   
  4. assertHost 'http://a.b.c:8080/bla',     host, 'a.b.c'  
  5. assertHost 'http://a.b.c/bla',          host, 'a.b.c'  
  6. assertHost 'http://127.0.0.1:8080/bla', host, '127.0.0.1'  
  7. assertHost 'http://t-online.de/bla',    host, 't-online.de'  
  8. assertHost 'http://T-online.de/bla',    host, 'T-online.de'  
  9. def assertHost (candidate, regex, expected){  
  10.     candidate.eachMatch(regex){assert it[1] == expected}  
  11. }  
  12. // Code to use the regular expression for useful work goes here  
Reading this code with and without assertions, their value becomes obvious. Seeing the example matches in the assertions reveals what the code is doing and verifies our assumptions at the same time. Traditionally, these examples would live inside a test harness or perhaps only within a comment. This is better than nothing, but experience shows that comments go out of date and the reader cannot really be sure that the code works as indicated. 

Some may fear a bad impact on performance when doing this style of inline unit tests. The best answer is to use a profiler and investigate where performance is really relevant. Our assertions in listing 6.7 run in a few milliseconds and should not normally be an issue. When performance is important, one possibility would be to put inline unit tests where they are executed only once per loaded class: in a static initializer. 

Relationships to other assertions 
Java has had an assert keyword since JDK 1.4. It differs from Groovy assertions in that it has a slightly different syntax (colon instead of comma to separate the Boolean test from the message) and that it can be enabled and disabled. Java’s assertion feature is not as powerful, because it works only on a Java Boolean test, whereas the Groovy assert takes a full Groovy conditional. 

Some people claim that for performance reasons, assertions should be disabled in production, after the code has been tested with assertions enabled. On this issue, Bertrand Meyer, 10 the father of design by contract, pointed out that it is like learning to swim with a swimming belt and taking it off when leaving the pool and heading for the ocean. In Groovy, your assertions are always executed. 

Assertions also play a central role in unit tests. Groovy comes with an included version of JUnit, the leading unit test framework for Java. JUnit makes a lot of specialized assertions available to its TestCases. Groovy adds even more of them. Full coverage of these assertions is given in chapter 14. The information that Groovy provides when assertions fail makes them very convenient when writing unit tests, because it relieves the tester from writing lots of messages. 

Assertions can make a big difference to your personal programming style and even more to the culture of a development team, regardless of whether they are used inline or in separated unit tests. Asserting your assumptions not only makes your code more reliable, but it also makes it easier to understand and easier to work with

Supplement: 
Groovy Document : Logical Branching

沒有留言:

張貼留言

網誌存檔

關於我自己

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