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
- if (true) assert true
- else assert false
- if (1) {
- assert true
- } else {
- assert false
- }
- if ('non-empty') assert true
- else if (['x']) assert false
- else assert false
- if (0) assert false
- else if ([]) assert false
- else assert true
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
- def result = (1==1) ? 'ok' : 'failed'
- assert result == 'ok'
- result = 'some string' ? 10 : ['x']
- assert result == 10
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
- def a = 1
- def log = ''
- switch (a) {
- case 0 : log += '0'
- case 1 : log += '1' // Fall through
- case 2 : log += '2' ; break // Exited explicitly
- default : log += 'default'
- }
- assert log == '12'
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:
- switch (candidate) {
- case classifier1 : handle1() ; break
- case classifier2 : handle2() ; break
- default : handleDefault()
- }
- if (classifier1.isCase(candidate)) handle1()
- else if (classifier2.isCase(candidate)) handle2()
- else handleDefault()
- Listing 6.6 Advanced switch and mixed classifiers
- switch (10) {
- case 0 : assert false ; break
- case 0..9 : assert false ; break
- case [8,9,11] : assert false ; break
- case {it%3 == 0}: assert false ; break // 2) Closure case
- case Float : assert false ; break // 1) Type case
- case Number : assert true ; printf "I am Number\n" // fall through next case without calling isCase
- case false : assert true ; printf "Fallthrough\n"
- case ~/../ : assert true ; break // 3) Regular expression case
- default : assert false ; break
- }
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.
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
- a = 1
- assert a==2
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:
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.
- input = new File('no such file')
- assert input.exists()
- assert input.canRead()
- println input.text
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:
- input = new File('no such file')
- assert input.exists() , "cannot find '$input.name'"
- assert input.canRead() , "cannot read '$input.canonicalPath'"
- println input.text
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:
- input = new File('no such file')
- println input.text
This leads to the following best practices with assertions:
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
- // Regular expression matching hosts
- def host = /\/\/([a-zA-Z0-9-]+(\.[a-zA-Z0-9-])*?)(:|\/)/
- assertHost 'http://a.b.c:8080/bla', host, 'a.b.c'
- assertHost 'http://a.b.c/bla', host, 'a.b.c'
- assertHost 'http://127.0.0.1:8080/bla', host, '127.0.0.1'
- assertHost 'http://t-online.de/bla', host, 't-online.de'
- assertHost 'http://T-online.de/bla', host, 'T-online.de'
- def assertHost (candidate, regex, expected){
- candidate.eachMatch(regex){assert it[1] == expected}
- }
- // Code to use the regular expression for useful work goes here
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
沒有留言:
張貼留言