Implementation and Testing
How much design effort is needed before coding starts?
It is a spectrum:- If the application is small and the initial design is pretty obvious, a quick design sketch may be all that is needed before coding begins.
- If there are major architecture issues, on the other hand, it may take many discussions and many iteration on the class diagrams to arrive at a reasonable initial design.
Use-case-driven development vs feature-driven development
The book pp428-429 compares the two approaches.- Question: focus on implementing features one-by-one, or implementing use-cases one-by-one?
- The two are complementary approaches:
- use-cases imply building some concrete messaging pathway through the classes
- features can be spread out a lot more, many classes and messaging paths may need to change to implement one feature.
- Suggestion:
- be feature-driven for small, isolated features (not the ones spread out too much)
- be use-case driven for more complex processes, where the feature-driven approach will be too unmanageable.
Starting to implement
- Make sure you have previously identified the key features and/or the key use-cases.
- Start implementing when the class design you have
feels solidified:
- the use-cases and the GUI sketches have all the actions/buttons mapped on to classes/methods.
- You can "run" all the use-cases for the key features on your UML class diagrams: there is a messaging pattern present there to implement the use-case.
- Some lower-level "leaves" (stuff you feel you will not have a problem directly implementing) can be left out.
- Start by implementing and testing the classes for the key features
- You should have a skeleton design proposal that includes all the features
Iteration / Release Planning
Software iterations and releases
Terminology- An iteration of a project is a planned global step in the development of a piece of software.
- An iteration should not be too big: add a couple features, modify the design to do one aspect differently, etc.
- Iterations should be planned in advance: before starting, make a proposal of what will be accomplished in the forthcoming iteration.
- A release is a stable iteration; after some number of iterations the project has stabilized and is released to users. A release will be extensively tested beforehand.
An iteration
Each iteration will involve activities of requirements capture, design, testing, and coding. So, its not like all the requirements are known at the start. For example:- During initial design it will become obvious that some requirements were missing or incorrect and so need to be revised
- After implementing an initial design it may in fact need to be thrown out since it contained major misunderstandings.
- Perhaps many features have been implemented, but for the next new feature to implement some flaws in the requirements are noted which requires significant work to adjust the class design before coding can proceed.
A Release
Each release is assigned a sequence number: 0.5, 1.2, etc. Releases generally go through phases- alpha release: Some basic features haven't been implemented yet and there are quite a few bugs. More for in-house testing.
- beta release: all features more or less work but will have a few bugs. Outside advanced users can use it.
- pre-release: generically refers to any non-final release, either alpha, beta, or whatever.
- final release: an "official" release; version number is at least 1.0.
The Iteration Planning Game
- Above we discussed how some features are key ones to begin with, and others are less core.
- In general, applications are a layered onion, with key features at the core and succesive layers of feature extensions on top ot that.
- Prioritizing features or use cases is a critical task
Iteration plans
- An interation plan maps features and/or use-cases on to which iteration they should be implemented in.
- Have a detailed iteration plan for the next iteration and a fuzzier one for more distant iterations
- Revise your iteration plan at the end of each iteration
- Maybe some things in the previous iteration proved too hard -- bump up to current iteration or divide into smaller problems over several future iterations.
- Make clear what the new set of features / use-cases you want to add in the next iteration is - take your fuzzy ideas from the previous iteration plan and refine them to a concrete plan.
- Again be fuzzy about the further iterations.
Making features or use cases that fit into iterations
- It can be hard to make use cases that are small enough to fit into an iteration.
- Also, some features you do not even want to take as a basis for implementation because they are too spread out
- In these cases, it is worth the effort to try to break them down into smaller, more manageable pieces
- Along model-GUI lines: two distinct use-cases for each part.
- Along local-distributed lines: implement a local version and extend it to a distributed or web-based version later.
- As mentioned previously, some single lines of use-cases are discovered to be complex and expand to use-cases in their own right.
- Make alternative paths in complex use-cases that can be
left out initiallty its own use-case for the purposes of
iteration planning (e.g. "implement the happy path of use-case 4
in iteration 4; do the alternative paths in iteration 5")
Implementation Principles
Here are some random good principles for implementation, many of which are from the XP and Agile schools of thought.Practice Collective Code Ownership
- Everyone can edit all code (use CVS, SVN, or other version control system)
- Why? allows refactoring to not be bounded--tweak others' code if you see a better way for it to interact with your code
- More eyes on any codebase is always a good thing for bug-finding and improving code
- Forces readability (who wants to be hated by everyone else)
- Another need for unit tests--if you tweak others' code, run their tests to make sure you didn't hose it.
Continuous Integration
Build the whole system every couple of hours.- Will make it more obvious when a recent change broke the system
- Works well with unit tests because its automatically apparent that the build failed.
- Implicitly means you are fixing small bugs and not big bugs.
Have a coding standard
This is a standard intro programming topic you probably know well.See the Information page Style section for guidelines for this course; here are a couple more:
- Follow the Java API formats for Java coding, both in terms of names of methods, etc, and in terms of usage of JavaDoc.
- If a method returns a boolean, make that clear in the method
name:
isWanted, etc. - Use active voice (imperative): i.e.: deleteInvoice()
- Use active voice for comments as well: deletes the Invoice not the invoice is deleted
- Don't re-use a temporary variable
Pair Programming
Pair programming is two people programming on one terminal.
- Pair consists of Driver & Partner
- Driver has the keyboards - ALL coding done in pairs
- Partner: corrects flaws in driver
- syntax
- conceptual
- asks questions of driver about code
- Driver:
- has final say (they have the keyboard)
- can ignore details
Is it good? Bad? Ugly?
- Advantages:
- One brain has attention to "concepts", the other to "details" -- specializing in a way that makes a "superbrain" better than two individual brains trying to hold both at once.
- You get the best qualities of two people - some are better at concepts and some better at details.
- Rapidly train new people (intense exposure)
- Disadvantages: With only one person typing, it can slow things down.
- Conclusion: may work with some personalities on some projects some or all of the time. Give it a test-drive if you have not tried it before.
Refactoring
Refactoring is an independent future lecture topic.
Testing
Testing is a major component of commercial software development.- A test suite is a set of tests which can be automatically run and the code either passes or fails.
- Programming without testing is analogous do doing science without experimental validation; just as experiments are at the basis of the scientific method, tests are at the basis of programming.
- More concretely, if you don't take a methodological approach to automated testing your applications can become so bug-infested as to be un-fixable.
Testing Hierarchies
From small fragments to whole system.Standard hierarchy from software engineering
- unit tests (intra-module / intra-package)
- integration tests - inter-package
- validation tests
- system tests
- Unit tests (low-level operations)
- Acceptance tests (level of requirements use-cases)
Unit Testing
Write small tests for each nontrivial operation- Test should be completely automatically executable.
- Each test returns either true (success) or false (fail) always.
- Re-run the complete unit test suite after any significant change, and then immediately debug to get test success to 100%
- By being rigorous about regular and thorough testing you
are stamping out the bugs before they get out of hand.
-- "a stitch in time saves nine" yet again.
Unit testing in Java with JUnit
- JUnit is a simple Java unit testing framework
- It is also built into Eclipse
- You are required to implement unit tests for your projects
- Learning JUnit is not hard and is a self-study topic; see the Course CASE tools page for pointers.
Test Coverage
- There is no need for tests that pretty much compltely overlap with each other.
- If there is some special case the code should work on, document this by writing a test for it.
- If you think an operation could fail, write a test to make
sure you are catching it
(e.g. is reading past the end of file caught?) - Add tests before refactoring to make sure you can verify the success of the refactoring afterwards
- Cover bugs with tests (i.e. add a test that would have failed given the bug you just found; prevents recurring bugs)
Test-first programing
Test-first programming is a major principle of XP.
Why write tests before code?
- Tests serve as initial specifications for the code: if you can't write the tests, you don't know what you are doing
- Related to this: while writing the tests you will need to make design decisions.
- It naturally leads to code easy to call: the tests are written from the viewpoint of the caller, so they will by definition give a caller-friendly view.
- A test specification gives a precise measure of success: the tests pass. An English specification doesn't offer this concrete answer.
- Its hard to write spaghetti code if you need to have clear interfaces for testing in the code.
Degrees of importance of thorough testing
The degree of commitment varies by the size, complexity, and need for correctness of the project. Here are some points on the spectrum.- Safety-critical systems require incredibly thorough
testing since failure could literally be catastrophic
-- need as much (or more) effort on testing as on actual development! - Nearly all projects need some degree of automated testing to keep bugs down.
- Very simple informal programs will get well "exercised" by random usage patterns so there may not be a great need for formal test suites.
Acceptance testing
- Acceptance tests are tests corresponding to use-cases: one per use-case.
- Acceptance tests often involve clicking on GUI elements so special tools may be needed to automate acceptance testing.
Specialized tests
- Parallel test: is the new version really better than the old one and worth releasing?
- Stress test: test under maximal load/minimal bandwidth.
- Monkey test: system works in the presence of nonsensical input.
Things that are harder to test automatically
You need to work harder to get some features automatically tested.- GUI's: its hard to automate the input. Either test via the underlying model, or display in a console a message for what the human tester needs to manually do at each point.
- Distributed systems: make sure you set up a particular initial configuration before running the test.
- Chicken-egg problems: if you have no code, you have no concrete objects to pass as parameters. Its OK to make some mock objects initially.
GUI Testing
This is an important topic since you cannot write automated tests for GUI's with just JUnit. Solutions include:- You should have carefully factored your model from your GUI, so most of the tests can be implemented directly on the model classes. Still, it is not an acceptance test until the GUI itself is tested.
- Write a JUnit test that pops up windows instructing what the tester should manually do at that point in the test.
- Use one of the new GUI testing tools. For use with JUnit, one such tool is Abbot.
Distributed systems testing
This is also a difficult kind of application to test. New systems are "in beta" on these topics now.- Cactus is a new JUnit extension for automated testing of distributed Java programs, servlets and Java beans in particular.
- HttpUnit is a new http page automated testing tool. With this you can automatically test whether fancy forms etc on your website are working.
- JunitEE, a tester for J2EE applications.
Programming Idioms
- There are many subtle programming idioms that you probably won't discover on your own
- You may discover them by seeing them in someone else's code
- These idioms often amount to some fixed pattern of syntax.
- Its a good idea to focus on the topic so you can build an explicit library of programming idioms.
- You already have a nice library of them; lets expand on your library.
Consider using static factory methods in place of class constructors
(Bloch Item 1)Why? Flexibility!
- Gives constructor a name for its purpose (Smalltalk always does this: all constructors are static methods!)
- Could opt to not make an object in special cases
- Could also make a subtype instead of current type
Implementing the Singleton Pattern in Java
(Bloch's Item 2)Its good to implement a singleton in a manner that guarantees the singleton behavior only.
- Use a static final field whose initializer creates the single instance
- Make a the default constructor private -- then outsiders can't make any more of the singleton.
Minimize Accessibility
(Item 13.)We've "talked the talk" about how good encapsulation is; this is one important way you need to "walk the walk" to implement encapsulation.
- Avoid public static fields like the plague: everyone can see them
- Don't forget to get the protected/private modifiers set appropriately on your code.
- Start out with things private and loosen up as needed. Yes those compile errors are annoying when you can't access the field/method you want, but there is a reward later.
- Loosen up in increments: private -> default -> protected ->
public
(default differs from public in that default only allows access within the package and protected offers access to subclasses outside the package)
Be Immutable Whenever Possible
(Bloch's Item 15)- An immutable object is one which doesn't change.
- Many objects are just naturally immutable, they don't change over time. Coffee cups. Pens. etc.
- Immutable = Static = Declarative = Mathematical = Good
Put another way, you always know what an immutable object is by reading its definition. No change could have altered its meaning. Just like a definition in a mathematics textbook. - Key Corollary: immutable objects can be shared freely without risk.. Thus they are always thread-safe.
- So, if possible be immutable. Even being mostly immutable is better than not being immutable at all.
- Related point: initialize the object in the constructor, not afterwards -- objects in the "limbo" state in-between are bug magnets.
There is strong precedence for this concept in programming languages.
- Functional programming languages (e.g. Haskell): all objects are immutable.
- Extended functional programming langauges (ML): objects are
immutable unless you declare them mutable via
ref. - Smalltalk et al: method arguments are always immutable.
- Java: make only the fields that
will change mutable, make the rest
final. Recall thatfinalfields cannot change after initialization. - In general, use as many
finalfield declarations as you can, a part-immutable object is better than a fully mutable one.
- Make all fields private and final, and not providing any mutating methods.
- Be careful about how shallow/deep the immutability is: a Java
finalfield itself can't change, but objects in the field can have components change if they have non-finalfields (thats the point of the example above).
Deep immutability is the one with good mathematical properties.
When Using Inheritance be Clear and Robust or ... Don't Inherit
(Bloch item 17)Since inheritance breaks class encapsulation, it can be a very good rope to hang yourself. This is related to the Open-Closed Principle (OCP).
- Inheritance breaks the normal class encapsulation boundary: you are mucking with the superclass' code when you override methods. (Think of it this way: the superclass is in effect importing the overriden method from the subclass to use it)
- Only use inheritance when there is really an is-a relationship
between the sub and super-class.
Example: don't subclass fromHashSetwhen your class is e.g. a set of car parts. Car parts aren't hash sets! - Be explicit on what methods depend on methods that can can be
overridden via putting
finalor not on methods.
Example:removefrom collection iterates over the collection so ifiteratorwas overridden, its behavior will change. - When overriding a method, don't change its original purpose.
- Constructors shouldn't invoke overridable methods: the subclass' constructor will not have been run before the overriding method would get run and things could blow up. Bloch example.
- If you aren't keeping the above things in mind, make your class
final---if you need to inherit you can later clean up the class and remove final.
Favor Composition Over Inheritance
(Bloch Item 16)
This is also very much related to The Open-Closed Principle (OCP).
- Composition is a whole-object holding on to (in a field) a part-object.
- Observe that by delegating some methods sent to the whole to the part (forwarding them) we achieve something similar to inheritance minus ability of overriding to change part's behavior.
- Composition is a weaker relationship than inheritance which doesn't violate class encapsulation -- it obeys the OCP
- You can explicitly pick what parts of the part-object that outsiders can see, by forwarding only some messages sent to the whole to the part.
- Composition is an "advanced" OO style -- several design patterns use composition in their implementation, including Composite, Decorator, State, ...
Avoid switch whenever possible
(same as Bloch item 20, replace unions with class hierarchies)
- Switches branch on an aspect of an object or objects: the object is passive, and the switch is active -- this is data-centric and opposed to the principle of active objects.
- Let the object do the work instead, as a method
- Any union type can be replaced with an inheritance hierarchy, with an abstract superclass (or interface) and a subclass for each discriminant in the union.
- We are going to cover this one in detail in the Refactoring lectures.
And Many More!
The Bloch book has many more. Here is a highlight.- (Items 8 and 9) Overriding
equalscan be good, but you need to make sure your method satisfies all the things equals needs - See the API documentation for what equals needs to obey: it
must for instance be transitive, reflexive, symmetric, and
have a faithful
hashCodeimplemented for it. - (Item 30) Use enums instead of
intconstants intconstants are for low-level C programmers only- (Item 38) Explicitly check method parameters for validity.
- For instance, a Scrabble play where an empty set of tiles was passed should be caught by the method. Raise an exception.
- (Item 45) Minimize the scope of local variables.
- If a variable is declared only in the neighborhood where its used, it won't be abused elsewhere in the method. But, your methods should be small enough anyway that this should not be a problem.
- (Item 58) Use checked exceptions for things the caller can recover from and unchecked exceptions for things tey can't recover from.
- (unchecked exceptions are the ones subclassing RuntimeException which
you don't have to declare
throwson in method headers). - (Item 60) Reuse existing exceptions built into Java whenever possible.
- Example: for a bad parameter passed to your method use
IllegalArgumentException. If its null useNullPointerException. - (Item 61) Throw exceptions understandable by the caller only
- Thus, if there is some low-level exception arising
(
SocketClosedException), catch it and rethrow it as (PlayerOfflineException) to keep the grubby low-level thing from leaking up. - (Item 65) Don't just catch and ignore exceptions
- This sort of code is "on the edge" and will easily fall apart--the exception indicates a problem and you need to track it down to the source. (In Fowler's terms, this is a bad smell in code)