We’ll set our goal by presenting several elements that make up a complete problem statement: a context in which the problem arises, the problem, the forces that influence the choice of solution, the solution that balances the forces, and some consequences of the chosen solution.
Based on the problem statement, we’ll present the high-level use case that this software implements. The use case is almost too trivial to bother defining. However, we have seen many projects run aground because they lacked even the most rudimentary description of the actor, the system and how the system helps the actor create value.
We will summarize the approach to the solution, describing the overall strategy that we will follow. This is a kind of overall design pattern that we’ll use to establish some areas of responsibility.
We will also describe the technical foundations. In this case, they are not terribly complex, but this is an important part of describing any software solution, no matter how simple.
We will dance around the methodology issue. Our intent is not to sell a particular methodology, but to provide some perspective on how we broke the work into manageable pieces.
Finally, we’ll present some important parts of getting started on the solution. These are more specific, technical considerations that define common aspects of our approach.
We’ll start with a big-picture overview of our problem. We’ll present the context in which the problem arises, a summary of the problem, and a “business use case”. This will show how our application is used.
We can then dig into the details of our application.
Fools Rush In
It’s important not to rush in to programming.
Here’s the rule: The first person to cut code loses.
Be sure you understand the problem being solved and how software solves that problem.
Context. Our context is the “classic” casino table games played against the house, including Roulette, Craps and Blackjack. We want to explore the consequences of various betting strategies for these casino games.
Questions include “How well does the Cancellation strategy work?” “How well does the Martingale strategy works for the Come Line odds bet in Craps?” “How well does this Blackjack strategy I found on the Internet compare with the strategy card I bought in the gift shop?”
A close parallel to this is exploring variations in rules and how these different rules have an influence on outcomes. Questions include “What should we do with the 2x and 10x odds offers in Craps?” “How should we modify our play for a single-deck Blackjack game with 6:5 blackjack odds?”
Our context does not include exploring or designing new casino games. Our context also excludes multi-player games like poker. We would like to be able to include additional against-the-house games like Pai Gow Poker, Caribbean Stud Poker, and Baccarat.
Problem. Our problem is to answer the following question: For a given game, what player strategies produce the best results?
Forces. There are a number of forces that influence our choice of solution. First, we want an application that is relatively simple to build. Instead of producing an interactive user interface, we will produce raw data and statistical summaries. If we have little interaction, a command-line interface will work perfectly. We can have the user specify a player strategy and the application respond with a presentation of the results. If the results are tab-delimited, they can be pasted into a spreadsheet for further analysis.
Another force that influences our choice of solution is the need to be platform and language agnostic. In this case, we have selected an approach that works well on POSIX-compliant operating systems (i.e., Linux, MacOS, and all of the proprietary UNIX variants), and also works on non-compliant operating systems (i.e., all of the Windows versions). We have chosen two OO languages that work identically on both platform families: Java and Python.
We also need to strike a balance between interesting programming, probability theory and statistics. On one hand, the simplicity of these games means that complete analyses have been done using probability theory. However, that’s not a very interesting programming exercise, so we will ignore the pure probability theory route in favor of learning OO design and programming.
Another force is the desire to reflect actual game play. While a long-running simulation of thousands of invidual cycles of play will approach the theoretical results, people typically don’t spend more than a few hours at a table game. If, for example, a Roulette wheel is spun once each minute, a player is unlikely to see more that 480 spins in an eight-hour evening at a casino. Additionally, many players have a fixed budget, and the betting is confined by table limits. Finally, we need to address the subject of “money management”: a player may elect to stop playing when they are ahead. This structures our statistical analysis: we must simulate sessions of play that are limited in time, the amount lost and the amount won.
Use Case. The high-level use case is an overall cycle of investigation . From this overall view, the actor’s goal is to find an optimal strategy for a given game.
Here’s the scenario we’re imagining.
Business Use Case
Consequences. We’re going build the simulator application that supports this high-level (or “business”) use case.
We’re not going to build the IDE to build the new classes. Any IDE should work.
Additionally, we won’t address how to analyze the results.
One of the most important consequences of our solution is that we will build an application into which new player betting strategies can be inserted. Clever gamblers invent new strategies all the time.
We will not know all of the available strategies in advance, so we will not be able to fully specify all of the various design details in advance. Instead, we will find ourselves reworking some parts of the solution, to support a new player betting strategy. This forces us to take an Agile approach to the design and implementation.
The previous section was a fluffy overview of what we’re trying to accomplish. It sets some goals and provides a detailed context for who’s using this application and why.
Armed with that informatin, we can look at the simulation application we’re going to write.
Our simulation application will allow a programmer to experiment with different casino game betting strategies. We’ll build a simple, command-line simulator that provides a reliable, accurate model of the game. We need to be able to easily pick one of a variety of player betting strategies, play a number of simulated rounds of the game, and produce a statistical summary of the results of that betting strategy.
This leads us to a small essential use case. There is a single actor, the “investigator”. The actor’s goal is to see the expected results of using a particular strategy for a particular game. The typical scenario is the following.
Essential Use Case
Actor. Specifies which game and betting strategy to test.
The game may require additional parameters, like betting limits.
The strategy may need additional parameters, like an initial budget, or stake.
System. Responds with a statistical summary of the outcomes after a fixed number of cycles (spins, or throws or hands). The number of cycles needs to be small (on the order of 200, to reflect only a few hours of play).
On Simplicity. Yes, this use case is very simple. It’s a command-line application: it’s supposed to be simple.
The point is to explore OO design, not development of a fancy GUI or web application.
Simplicity is a virtue. You can add a fancy GUI or web presentation of the results later. First, create some results.
We feel that the use case technique is badly abused by some IT organizations. Quoting from [Jacobson95]. “A use case is a sequence of transactions in a system whose task is to yield a result of measurable value to an individual actor of the system.”
A use case will clearly identify an actor, define the value created, and define a sequence of transactions. A use case will be a kind of system test specification. A use case will define the system’s behavior, and define why an actor bothers to interact with it.
A use case is not a specification, and does not replace ordinary design. We have had experiences with customers who simply retitle their traditional procedural programming specifications as “use cases”. We hypothesize that this comes from an unwillingness to separate problem definition from solution definition. The consequence is a conflation of use case, technical background, design and programming specifications into gargantuan documents that defy the ability of programmers or users to comprehend them.
There are a number of common problems with use cases that will make the design job more difficult. Each of these defects should lead to review of the use case with the authors to see what, if anything, they can do to rework the use case to be more complete.
- No Actor. Without an actor, it’s impossible to tell who is getting value from the interaction. A catch-all title like “the user” indicates a use case written from the point of view of the database or the application software, not an actual person. An actor can be an interface with other software, in which case, the actual software needs to be named. Without knowing the actor, you will have trouble deciding which classes are clients and which classes provide the lower-level services of the application.
- No Value Proposition. There are two basic kinds of value: information for decision-making or actions taken as the result of decision-making. People interact with software because there are decisions the software cannot make or there are actions the actor cannot make. Some use cases include value-less activities like logging in, or committing a transaction, or clicking “Okay” to continue. These are parts of operating scenarios, not statements of value that show how the actor is happier or more successful. Without a value proposition, you will have no clue as to what problem the software solves, or what it eventually does for the actor.
- No Interactions. If the entire body of the use case is a series of steps the application performs, we are suspicious of the focus. We prefer a use case to emphasize interaction with the actor. Complex algorithms or interface specifications should be part of an appendix or supplemental document. Without any interaction, it isn’t clear how the actor uses the software.
We also try to make a distinction between detailed operating scenarios and use cases. We have seen customers write documents they call “detailed use cases” that describe the behavior of individual graphical user interface widgets or panels. We prefer to call these scenarios, since they don’t describe measuable business value, but instead describe technical interactions.
From reading the problem and use case information, we can identify at least the following four general elements to our application.
When we look at common design patterns, the Model-View-Control pattern often helps to structure applications. A more sophisticated, transactional application may require a more complex structure. However, in this case, the game, the player, and the statistics are the model. The command line selection of player and the reporting of raw data is the view. The overall control component creates the various objects to start the simulation.
While interesting, we will not pursue the design of a general-purpose simulation framework. Nor will we use any of the available general frameworks. While these are handy and powerful tools, we want to focus on developing application software “from scratch” (or de novo) as a learning exercise.
Our solution will depend heavily on desktop integration: the actor will use their IDE to create a strategy and build a new version of the application program. Once the application is built, the actor can run the application from the command line, collecting the output file. The statistical results file can be analyzed using a spreadsheet application. There are at least three separate application programs involved: the IDE (including editor and compiler), the simulator, the spreadsheet used for analysis.
A typical execution of the simulator will look like the following example.
python -m casino.craps --Dplayer.name="Player1326" >details.log
We are intentionally limiting our approach to a simple command-line application using the default language libraries. Avoiding additional libraries assures a “lowest-common denominator” multi-platform application. For Java, this standard is the J2SE set of libraries; we won’t use any J2EE extensions. For Python, it is the base installation.
There are a number of more technical considerations that we will expand in Deliverables. These include the use of an overall simulation framework and an approach for unit testing.
Among the topics this book deals with in a casual – possibly misleading – manner are probability and statitics. Experts will spot a number of gaps in our exposition. For example, there isn’t a compelling need for simulation of the simpler games of Craps and Roulette, since they can be completely analyzed. However, our primary objective is to study programming, not casino games, therefore we don’t mind solving known problems again. We are aware that our statistical analysis has a number of deficiencies. We will avoid any deeper investigation into statistics.
We want to focus on technical skills; we won’t follow any particular software development methodology too closely. We hesitate to endorse a specific methodology; doing so inevitably alienates readers who embrace a different methodology. To continue this rant, we find that almost everyone has an existing notion of the proper way to organize software development work. This leads to the common practice of customizing methodologies, in most cases without a deep background in the methodology or the changes being made.
We prefer to lift up a few techniques which have a great deal of benefit.
The exercises are presented as if we are doing a kind of iterative design with very, very small deliverables. We present the exercises like this for a number of reasons.
First, we find that beginning designers work best with immediate feedback on their design decisions. While we present the design in considerable detail, we do not present the final code. Programmers new to OO design will benefit from repeated exposure to the transformation of problem statement through design to code.
Second, for iterative or agile methodologies, this presentation parallels the way software is developed. A project manager may use larger collections of deliverables. However, the actual creation of functional source eventually decomposes into classes, fields and methods. For project managers, this exposition will help them see where and how rework can occur; giving them a chance to plan for the kind of learning that occur in most projects.
Third, for project teams using a strict waterfall methodology – with all design work completed before any programming work – the book can be read in a slightly different order. From each exercise chapter, read only the overview and design sections. From that information, integrate the complete design. Then proceed through the deliverables sections of each chapter, removing duplicates and building only the final form of the deliverables based on the complete design. This will show how design rework arises as part of a waterfall methodology.
This section addresses a number of methodology or process topics:
Our approach to overall quality assurance is relatively simple. We feel that a focus on unit testing and documetation covers most of the generally accepted quality factors. The Software Engineering Institute (SEI) published a quality measures taxonomy. While officially “legacy”, it still provides an exhaustive list of quality attributes. These are broadly grouped into five categories. Our approach covers most of those five categories reasonably well.
Need Satisfaction. Does the software meet the need? We start with a problem statement, define the use case, and then write software which is narrowly focused on the actor’s needs. By developing our application in small increments, we can ask ourself at each step, “Does this meet the actor’s needs?” It’s fairly easy to keep a software development project focused when we have use cases to describe our goals.
Performance. We don’t address this specifically in this book. However, the presence of extensive unit tests allows us to alter the implemention of classes to change the overall performance of our application. As long as the resulting class still passes the unit tests, we can develop numerous alternative implementations to optimize speed, memory use, input/output, or any other resource.
Maintenance. Software is something that is frequently changed. It changes when we uncover bugs. More commonly, it changes when our understanding of the problem, the actor or the use case changes. In many cases, our initial solution merely clarifies the actor’s thinking, and we have to alter the software to reflect a deeper understanding of the problem.
Maintenance is just another cycle of the iterative approach we’ve chosen in this book. We pick a feature, create or modify classes, and then create or modify the unit tests. In the case of bug fixing, we often add unit tests to demonstrate the bug, and then fix our classes to pass the revised unit tests.
Adaptation. Adaptation refers to our need to adapt our software to changes in the environment. The environment includes interfaces, the operating system or platform, even the number of users is part of the environment. When we address issues of interoperability with other software, portability to new operating systems, scalability for more users, we are addressing adaptation issues.
We chose Python and Java to avoid having interoperability and portability issues; these platforms give admirable support for many scalability issues. Generally, a well-written piece of software can be reused. While this book doesn’t focus on reuse, Java and Python are biased toward writing reusable software.
Organizational. There are some organizational quality factors: cost of ownership and productivity of the developers creating it. We don’t address these directly. Our approach, however, of developing software incrementally often leads to good developer productivity.
Our approach (Incremental, Unit Testing, Embedded Documentation) assures high quality in four of the five quality areas. Incremental development is a way to focus on need satisfaction. Unit testing helps us optimize resource use, and do maintenance well. Our choices of tools and platforms help us address adaptation.
The organizational impact of these techniques isn’t so clear. It is easy to mis-manage a team and turn incremental development into a quagmire of too much planning for too little delivered software. It is all too common to declare that the effort spent writing unit test code is “wasted”.
Ultimately, this is a book on OO design. How people organize themselves to build software is beyond our scope.
In Problem Statement, we described the problem. In Solution Approach, we provided an overview of the solution. The following parts will guide you through an incremental design process; a process that involves learning and exploring. This means that we will coach you to build classes and then modify those classes based on lessons learned during later steps in the design process. See our Soapbox on Rework for an opinion on the absolute necessity for design rework.
We don’t simply present a completed design. We feel that it is very important follow a realistic problem-solving trajectory so that beginning designers are exposed to the decisions involved in creating a complete design. In our experience, all problems involve a considerable amount of “learn as you go”.
We want to reflect this in our series of exercises. In many respects, a successful OO design is one that respects the degrees of ignorance that people have when starting to build software. We will try to present the exercises in a way that teaches the reader how to manage ignorance and still develop valuable software.
For some, the word rework has a negative connotation. If you find the word distasteful, please replace every occurance with any of the synonyms: adaptation, evolution, enhancement, mutation. We prefer the slightly negative connotation of the word rework because it helps managers realize the importance of incremental learning and how it changes the requirements, the design and the resulting software.
Since learning will involve mistakes, good management plans for the costs and risks of those mistakes. Generally, our approach is to manage our ignorance; we try to create a design such that correcting a mistake only fixes a few classes.
We often observe denial of the amount of ignorance involved in creating IT solutions. It is sometimes very difficult to make it clear that if the problem was well-understood, or the solution was well-defined there would be immediately applicable off-the-shelf or open-source solutions. The absence of a ready-to-hand solution generally means the problem is hard. It also means that there are several degrees of ignorance: ignorance of the problem, solution and technology; not to mention ignorance of the amount of ignorance involved in each of these areas.
We see a number of consequences of denying the degrees of ignorance.
We find that any initial “high-level” design can miss details of the problem domain, and this leads to rework. Forbidding rework amounts to mandating a full understanding of the problem. In most cases, our users do not fully understand their problem any more than our developers understand our users. Generally, it is very hard to understand the problem, or the solution. We find that hands-on use of preliminary versions of software can help more than endless conversations about what could be built.
In the programming arena, we find that Java and Python (and their associated libraries) are so powerful that detailed design is done at the level of individual language statements. This leads us to write the program either in English prose or UML diagrams (sometimes both) before writing the program in the final programming language. We often develop a strong commitment to the initial design, and subsequent revisions are merely transliterations with no time permitted for substantial revisions. While we have written the program two or three times over, the additional quality isn’t worth doubling or tripling the workload. We feel that the original workload should be managed as planned cycles of work and rework.
Many of the chapters will include some lengthy design decisions that appear to be little more than hand-wringning over nuances. While this is true to an extent, we need to emphasize our technique for doing appropriate hand-wringing over OO design. We call it “Looking For The Big Simple”, and find that managers don’t often permit the careful enumeration of all the alternatives and the itemization of the pros and cons of each choice. We have worked with managers who capriciously play their “schedule” or “budget” trump cards, stopping useful discussion of alternatives. This may stem from a fundamental discomfort with the technology, and a consequent discomfort of appearing lost in front of team members and direct reports. Our suggestion in this book can be summarized as follows:
Good OO design comes from a good process for technical decision-making.
First, admit what we don’t know, and then take steps to reduce our degrees of ignorance.
Which means not saying “work smarter not harder” unless we also provide the time and budget to actually get smarter. The learning process, as with all things, must be planned and managed. Our lesson learned from Blaise Pascal is that a little more time spent on design can result in considerable simplification, which will reduce development and maintenance costs.
It’s also important to note that no one in the real world is omniscient. Some of the exercises include intentional dead-ends. As a practical matter, we can rarely foresee all of the consequences of a design decision.
While there is a great deal of commonality among the three games, the exercises do not start with an emphasis on constructing a general framework. We find that too much generalization and too much emphasis on reuse is not appropriate for beginning object designers. See Soapbox on Reuse for an opinion on reuse. Additionally, we find that projects that begin with too-lofty reuse goals often fail to deliver valuable solutions in a timely fashion. We prefer not to start out with a goal that amounts to boiling the ocean to make a pot of tea.
These exercises will refer to several of the “Gang of Four” design patterns in [Gamma95]. The Design Patterns book is not a prerequisite; we use it as reference material to provide additional insight into the design patterns used here. We feel that use of common design patterns significantly expands the programmer’s repertoire of techniques. We note where they are appropriate, and provide some guidance in their implementation.
In addition, we reference several other design patterns which are not as well documented. These are, in some cases, patterns of bad design more than patterns of good design.
Each chapter defines the classes to be built and the unit testing that is expected. A third deliverable is merely implied. The purpose of each chapter is to write the source files for one or more classes, the source files for one or more unit tests, and assure that a minimal set of API documentation is available.
Source Files. The source files are the most important deliverable. In effect, this is the working application program. Generally, you will be running this application from within your Integrated Development Environment (IDE). You may want to create a stand-alone program.
In the case of Java, we might also deliver the collection of class files. Additionally, we might bundle the class files into an executable JAR file. The source is the principle deliverable; anyone should be able to produce class and JAR files using readily available tools.
In the case of Python, it’s the packages of .py files. There really isn’t much more to deliver. The interested student might want to look at the Python distutils and setuptools to create a distribution kit, or possibly a Python .egg file.
Unit Test Files. The deliverables section of each chapter summarizes the unit testing that is expected, in addition to the classes to be built. We feel that unit testing is a critical skill, and emphasize it throughout the inividual exercises. We don’t endorse a particular technology for implementing the unit tests. There are several approaches to unit testing that are in common use.
For formal testing of some class, X, we create a separate class, TestX, which creates instances of X and exercises those instances to be sure they work. In Java, this is often done with JUnit. In Python, the unittest module is the mechanism for doing formal unit tests. Additionally, many Python developers also use the doctest module to assure that the sample code in the docstrings is actually correct. We cover these technologies in the appendices.
Documentation. The job isn’t over until the paperwork is done. In the case of Java and Python, the internal documentation is generally built from specially formatted blocks of comments within the source itself. Java programmers can use the javadoc tool to create documentation from the program source. Python programmers can use Epydoc (or sphinx) to create similar documentation.