Believe it or not, there’s still more. Yes, with over 550 pages under your belt, there are still things we couldn’t cram in. Even though these last ten topics don’t deserve more than a mention, we didn’t want to let you out of Objectville without a little more information on each one of them. But hey, now you’ve got just a little bit more to talk about during commercials of CATASTROPHE... and who doesn’t love some stimulating OOA&D talk every now and then?
Besides, once you’re done here, all that’s left is another appendix... and the index... and maybe some ads... and then you’re really done. We promise!
Lots of times in OO programming circles, you’ll hear someone talk about the IS-A and HAS-A relationships.
Usually, IS-A relates to inheritance, for example: “A Sword IS-A Weapon, so Sword should extend Weapon.”
The reason we haven’t covered IS-A and HAS-A much is that they tend to break down in certain situations. For example, consider the situation where you’re modeling shapes, like Circle
, Rectangle
, and Diamond
.
If you think about a Square
object, you can apply the IS-A relationship: Square
IS-A Rectangle
. So you should make Square
extend Rectangle
, right?
But remember LSP, and that subtypes should be substitutable for their base types. So Square
should be a direct substitute for Rectangle
. But what happens with code like this:
The problem here is that when you set the width in setWidth()
on Square
, the square is going to have to set its height, too, since squares have equal width and height. So even though Square
IS-A Rectangle
, it doesn’t behave like a rectangle. Calling getHeight()
above will return 5, not 10, which means that squares behave differently than rectangles, and aren’t substitutable for them—that’s a violation of the LSP.
Use inheritance when one object behaves like another, rather than just when the IS-A relationship applies.
Even though there’s a pretty standard definition for what a use case is, there’s not a standard way for writing use cases. Here are just a few of the different ways you can write up your use cases:
This format is a little more focused on separating out what is in a system, and how the actors outside of the system interact with your software.
All of these use cases say the same thing... it’s up to you (and probably your boss) to decide which format works best for you.
We’ve talked a lot in this book about design patterns, and described them this way:
Design Patterns
Design patterns are proven solutions to particular types of problems, and help us structure our own applications in ways that are easier to understand, more maintainable, and more flexible.
Design patterns help you recognize and implement GOOD solutions to common problems.
But there’s another type of pattern you should know about, called an anti-pattern:
Anti patterns are about recognizing and avoiding BAD solutions to common problems.
Anti Patterns
Anti-patterns are the reverse of design patterns: they are common BAD solutions to problems. These dangerous pitfalls should be recognized and avoided.
Anti patterns turn up when you see the same problem get solved the same way, but the solution turns out to be a BAD one. For example, one common anti pattern is called “Gas Factory”, and refers to designs that are overly complex, and therefore not very maintainable. So you want to work to avoid the Gas Factory in your own code.
CRC stands for Class, Responsibility, Collaborator. These cards are used to take a class and figure out what its responsibility should be, and what other classes it collaborates with.
CRC cards are typically just 3x5 index cards, with each individual card representing a class. The card has two columns: one for the responsibilities of the class, and another for other classes that are collaborators, and used to fulfill those responsibilities.
You can use CRC cards to make sure your classes follow the Single Responsibility Principle. These go hand in hand with your SRP Analysis, as well:
Sometimes it’s hard to tell how solid your design really is, because design is such a subjective thing. That’s where metrics can help out: while they don’t provide a complete picture of your system, they can be helpful in pointing out strengths, weaknesses, and potential problems. You usually use software tools to take as input your class’s source code, and those tools then generate metrics based on your code and its design.
These metrics are more than just numbers, though. For example, just counting the number of lines of code in your application is almost a total waste of time. It’s nothing but a number, and has no context (and also depends a lot on how you’re writing your code, something else we’ll talk about in this appendix). But if you count the number of defects per 1000 lines of code, then that becomes a useful metric.
You can also use metric to measure things like how well you’re using abstraction in your code. Good design will use abstract classes and interfaces, so that other classes can program to those interfaces rather than specific implementation classes. So abstraction keeps one part of your code independent from changes to other parts of your code, at least to the degree that it’s possible in your system. You can use something called the abstractness metric to measure this:
Packages that have lots of abstractions will have a higher value for A, and packages with less abstractions have a lower value for A. In general, you want to have each package in your software only depend on packages with a higher value for A. That means that your packages are always depending on packages that are more abstract; the result should be software that can easily respond to change.
When we were working on the dog door for Todd and Gina, we developed several alternate paths (and one alternate path actually had an alternate path itself). To really get a feel for how your system handles these different paths, it’s helpful to use a UML sequence diagram. A sequence diagram is just what it sounds like: a visual way to show the things that happen in a particular interaction between an actor and your system.
You’ve already seen class diagrams and sequence diagrams. UML also contains a diagram called a state machine diagram or statechart diagram, which is usually just referred to as a state diagram. This diagram describes a part of your system by showing its various states, and the actions that cause that state to change. These diagrams are great for describing complex behaviors visually.
State diagrams really come into play when you have multiple actions and events that are all going on at the same time. On the right page, we’ve taken just such a situation, and drawn a state diagram for how a game designer might use Gary’s Game System Framework. If game designers were going to use the framework, they might write a game that behaves a lot like this state diagram demonstrates.
In each chapter that we’re worked on an application, we’ve built “driver” programs to test the code, like SubwayTester
and DogDoorSimulator
. These are all a form of unit testing. We test each class with a certain set of input data, or with a particular sequence of method calls.
While this is a great way to get a sense of how your application works when used by the customer, it does have some drawbacks:
You have to write a complete program for each usage of the software.
You need to produce some kind of output, either to the console or a file, to verify the software is working correctly.
You have to manually look over the output of the test, each time its run, to make sure things are working correctly.
Your tests will eventually test such large pieces of functionality that you’re no longer testing all the smaller features of your app.
Fortunately, there are testing frameworks that will not only allow you to test very small pieces of functionality, but will also automate much of that testing for you. In Java, the most popular framework is called JUnit (http://www.junit.org), and integrates with lots of the popular Java development environments, like Eclipse.
A test case has a test method for each and every piece of functionality in the class that it’s testing. So for a class like DogDoor
, we’d test opening the door, and closing the door. JUnit would generate a test class that looked something like this:
Notice that instead of directly testing the DogDoor
’s open()
and close()
methods, this test uses the Remote
class, which is how the door would work in the real world. That ensures that the tests are simulating real usage, even though they are testing just a single piece of functionality at a time.
The same thing is done in testCloseDoor()
. Instead of calling the close()
method, the test opens the door with the remote, waits beyond the time it should take for the door to close automatically, and then tests to see if the door is closed. That’s how the door will be used, so that’s what should be tested.
Reading source code should be a lot like reading a book. You should be able to tell what’s going on, and even if you have a few questions, it shouldn’t be too hard to figure out the answers to those questions if you just keep reading. Good developers and designers should be willing to spend a little extra time writing readable code, because it improves the ability to maintain and reuse that code.
Here’s an example of a commented and readable version of the DogDoor
class we wrote back in Chapter 2 and Chapter 3.
Many developers will tell you that code standards and formatting are a big pain, but take a look at what happens when you don’t spend any time making your code readable:
From a purely functional point of view, this version of DogDoor
works just as well as the one on the last page. But by now you should know that great software is more than just working code—it’s code that is maintainable, and can be reused. And most developers will not want to maintain or reuse this second version of DogDoor
; it’s a pain to figure out what it does, or where things might go wrong—now imagine if there were 10,000 lines of code like this, and not just 25 or so.
Writing readable code makes that code easier to maintain and reuse, for you and other developers.
Refactoring is the process of modifying the structure of your code without modifying its behavior. Refactoring is done to increase the cleanness, flexibility, and extensibility of your code, and usually is related to a specific improvement in your design.
Most refactorings are fairly simple, and focus on one specific design aspect of your code. For example:
public double getDisabilityAmount() { // Check for eligibility if (seniority < 2) return 0; if (monthsDisabled > 12) return 0; if (isPartTime) return 0; // Calculate disability amount and return it }
While there’s nothing particularly wrong with this code, it’s not as maintainable as it could be. The getDisabilityAmount()
method is really doing two things: checking the eligibility for disability, and then calculating the amount.
By now, you should know that violates the Single Responsibility Principle. We really should separate the code that handles eligibility requirements from the code that does disability calculations. So we can refactor this code to look more like this:
Now, if the eligibility requirements for disability change, only the isEligibleForDisability()
methods needs to change—and the method responsible for calculating the disability amount doesn’t.
Think of refactoring as a checkup for your code. It should be an ongoing process, as code that is left alone tends to become harder and harder to reuse. Go back to old code, and refactor it to take advantage of new design techniques you’ve learned. The programmers who have to maintain and reuse your code will thank you for it.
Refactoring changes the internal structure of your code WITHOUT affecting your code’s behavior.