- some people might argue that design isn’t really a construction activity, but usually it is and the sooner it’s recognized as an explicit activity, the sooner will maximize the benefit received from it.
5.1 Design Challenges
- Software design is a conception/invention/scheme for turning a specification for a computer program into an operational program;
- Design links requirements to coding and debugging;
- A good top-level design provides a structure which can safely contain multiple lower-level designs;
- A good design is useful on small projects and indispensable on large ones;
- Design also implies many challenges which will be described below.
Design is a Wicked Problem
- A “wicked” problem is defined as one which can be clearly defined only by solving it or by solving a part of it.
Design is a Sloppy Process
- A finished design should look clean and organized but the process to get the design to this form rarely is.
Design is sloppy because:
- you eventually blindly follow many false paths to find out why it’s not working, so basically you make a lot of mistakes. However, mistakes are welcome at this stage, this is the point of the design. It’s way cheaper to make mistakes and correct them now then after some or most of the implementation is finished and you’ll have to revert/change the code again;
- a good solution is only slightly better than a poor one;
- it’s hard to know when the design is good enough;
- how do you know if more detail should be added?
- how do you know there is too much detail?
- when are you finished*?
* Design is usually over when you’re out of time due to its open-ended concept.
Design is About Trade-Offs and Priorities
In an ideal world every system could:
- run forever;
- consume zero resources (storage, network bandwidth);
- contain no errors;
- free to build.
However, in a real world a designer’s job is thoroughly think about the design characteristics and come up with a balanced solution for the design. If a fast response time is more important than minimizing the development time, the designer will choose one design. If minimizing the development time is more important, another design will be chosen;
Design Involves Restrictions
The point of design is to create and restrict possibilities altogether because he resources are not infinite.
Design is Non-Deterministic
There are many ways a computer program can be designed and work as expected.
Design is a Heuristic Process
- are usually heuristic;
- are not a repeatable process which is guaranteed to produce predictable results;
- involve trial and error;
A design tool or technique which worked well on one job, does not necessarily perform in the same way in for another job. No tool is right for everything.
Design is Emergent
Designs don’t just come up from someone’s mind in the final form. They evolve and improve through design reviews, informal discussions, experience with working and revising the code itself. All designs will be at least slightly changed during the initial phase and the changes usually extend at a later point in time when a new version is required. The degree to which change is beneficial or acceptable depends entirely on the type of software being developed.
5.2 Key Design Concepts
Accidental and Essential Difficulties
There are two types of problems:
- Essential – the ones which define a thing to be a thing (a car must have an engine and wheels to move);
- Accidental – the ones which are not so important, but they exist (the color of the car).
In software, most accidental problems consisted of poor programming languages, environments, resources etc. Most of these kind of issues are already solved nowadays with the modern programming languages, IDEs and resources.
So all difficulty resides in the essential problems which tend to be slower. Why? Because we try to:
- interface with the complex, disorderly real-world;
- accurately and completely identify exceptions and dependencies;
- design solutions which are not approximately correct, they must be exactly correct.
So we can say that the root of all these difficulties is complexity – both accidental and essential.
Importance of Managing Complexity
Most projects fail due to several reasons:
- Poor requirements;
- Poor planning;
- Poor management.
However, when projects fail due to technical reasons, usually is because of uncontrolled complexity. This happens when the software is allowed to grow so complex that no one really knows what it does. And because of this, no one really understands how some changes in one area will impact others in another one, thus resulting in progress stagnation.
Managing complexity is the most important technical topic in software development.
What we can do to avoid this complexity is to organize our programs in such a way that we can safely focus on a small part at a time. The goal is to minimize the amount of a program you must think about at any one time. This can be thought of as mental juggling – the more balls you have in the air to juggle with, the more likely you are to drop one (or more), leading to a design or coding error.
At the architecture level, complexity is managed by breaking the system into subsystems. Humans can easier understand several small, simple pieces of information rather than one complex piece. So the goal of software design is to break a complex problem into smaller, easier pieces. The more independent they are, the easier it is to understand and focus on, thus working on it. Carefully defined objects separate concerns so you can focus on one thing at a time. Packages/namespaces provide the same benefit at a higher level of abstraction. Small methods helps reduce mental workload. Writing programs in terms of the problem domain rather than in low-level implementation details and working at the highest level of abstraction reduce the load on your brain.
How to Attack Complexity
Below are some ineffective, overly costly designs:
- A complex solution to a simple problem (over-engineering);
- A simple, incorrect solution to a complex problem;
- An inappropriate, complex solution to a complex problem.
What we can do about it:
- Minimize the amount of essential complexity one’s brain has to deal with at any one time;
- Keep accidental complexity under control;
- Understand that all other technical goals are secondary to managing complexity.
Desirable Characteristics of a Design
A high-quality design has several general characteristics. Some goals will contradict others, but this is the design’s challenge: creating a good set of trade-offs from competing objectives. Some characteristics of design quality are also characteristics of the program: reliability, performance, and so on. Others are internal characteristics of the design. Below are some internal characteristics.
- Minimize complexity;
- Avoid “clever” clever designs, instead focus on “simple” and “easy-to-understand” designs;
- If design does not simply allow to ignore other parts of the code, then the design is faulty.
Ease of Maintenance
- Design solutions like you will be the one responsible for maintaining it;
- Always ask yourself questions the maintenance developer would ask the code you’re writing. Imagine the maintenance developer as your audience.
- Keep connections between parts of your program to a minimum;
- Use principles of strong cohesion, loose coupling and information hiding to design classes with as few connections as possible;
- Minimal connectedness keeps integration, testing and maintenance work to a minimum.
- Make sure you can change a piece of the system without affecting the others.
- Design the system so that you can reuse parts of it at a later point in time;.
- Refers to having a high number of classes use a certain class;
- Implies that a system was designed to make good use of utility classes at the lower levels in the system.
- Refers to having a certain class to use a low-to-medium number of other classes;
- High fan-out (more than about seven) indicates a class uses a large number of other classes and therefore will be overly complex;
- This principle can be applied to both classes or methods (having a method calling too many other methods).
- Designing a system which can be easily moved to another environment.
- Designing the system so that it has no extra parts. So, nothing can be added or removed;
- Future versions of the program must remain backward-compatible with the extra code;
- Fatal question: What does it hurt if we add this here and that there?
- Stratified design is keeping the levels of decomposition stratified so you can view the system at any single level and get a consistent view;
- Design the system to view it at one level without needing go in deeper levels;
- When working with old, bad designed legacy code, make sure to write a layer in the new system that’s responsible for interfacing with the old code, presenting a consistent set of services to the newer layers. This approach leads to the following advantages:
- The messiness of the bad code is contained in one layer;
- If at some point in time you can throw the old code away, you’ll have to work only on the interface layer.
- The more a system relies on exotic pieces, the more intimidating will be for someone to understand it the first time;
- Try to give the whole system a familiar feeling by using standardized, common techniques;
- Code for the average developer.
Levels of Design
Level 1: Software System
- First level is the entire system;
- It’s usually beneficial to think through higher level combinations of classes, such as subsystems or packages.
Level 2: Division into Subsystems or Packages
- Common subsystems:
- Business logic;
- User interface;
- Database access;
- System dependencies;
Level 3: Division Into Classes
- Details regarding the ways the classes interact with each other are defined at this stage;
- Usually the class interface is also defined;
- The major design activity at this level is making sure that all the subsystems have been decomposed to a level of detail fine enough that you can implement their parts as individual classes;
Level 4: Division Into Routines
- Divide each class into routines. The class interface as level 3 defines some routines. Design at level 4 will detail the class’s private routines.
Level 5: Internal Routine Design
- Consists of laying out the detailed functionality of the individual routines;
- Internal routine design is typically left to the individual programmer;
- The design consists of activities such as writing pseudocode, looking up algorithms in reference books, deciding how to organize the paragraphs of code in a routine, and writing programming language code.
5.3 Design Building Blocks: Heuristics
Find Real-World Objects
- The first and most popular approach to identifying design alternatives is the “by the book” object-oriented approach, which focuses on identifying real-world and synthetic objects;
- The steps in designing with objects are:
- Identify the objects and their attributes (methods and data);
- Determine what can be done to each object;
- Determine what each object can do to other objects;
- Determine the parts of each object that will be visible to other objects – which parts will be public and which will be private;
- Define each object’s public interface.
Form Consistent Abstractions
- Abstraction is the ability to engage with a concept while safely ignoring some of its details— handling different details at different levels. Any time you work with an aggregate, you’re working with an abstraction. If you refer to an object as a “house” rather than a combination of glass, wood, and nails, you’re making an abstraction. If you refer to a collection of houses as a “town,” you’re making another abstraction;
- A good class interface is an abstraction that allows you to focus on the interface without needing to worry about the internal workings of the class.
- The interface to a well-designed routine provides the same benefit at a lower level of detail, and the interface to a well-designed package or subsystem provides that benefit at a higher level of detail;
- From a complexity point of view, the principal benefit of abstraction is that it allows you to ignore irrelevant details;
- Most real-world objects are already abstractions of some kind. A house is an abstraction of windows, doors, siding, wiring, plumbing, insulation, and a particular way of organizing them. A door is in turn an abstraction of a particular arrangement of a rectangular piece of material with hinges and a doorknob. And the doorknob is an abstraction of a particular formation of brass, nickel, iron, or steel;
- Good programmers create abstractions at the routine-interface level, class-interface level, package-interface level—in other words, the doorknob level, door level, and house level—and that supports faster and safer programming.
Encapsulate Implementation Details
- Encapsulation picks up where abstraction leaves off. Abstraction says, “You’re allowed to look at an object at a high level of detail.” Encapsulation says, “Furthermore, you aren’t allowed to look at an object at any other level of detail”;
- To continue the housing-materials analogy: Encapsulation is a way of saying that you can look at the outside of the house, but you can’t get close enough to make out the door’s details. You are allowed to know that there’s a door, and you’re allowed to know whether the door is open or closed, but you’re not allowed to know whether the door is made of wood, fiberglass, steel, or some other material, and you’re certainly not allowed to look at each individual wood fiber.
• The road to programming hell is paved with global variables;
Identify Areas Likely to Change
- Accommodating changes is one of the most challenging aspects of good program design;
- The goal is to isolate unstable areas so that the effect of a change will be limited to one class;
- Steps to follow in preparing for such perturbations:
- Identify items that seem likely to change. If the requirements have been done well, they include a list of potential changes and the likelihood of each change. In such a case, identifying the likely changes is easy. If the requirements don’t cover potential changes, see the discussion that follows of areas that are likely to change on any project;
- Separate items that are likely to change. Compartmentalize each volatile component identified in step 1 into its own class, or into a class with other volatile components that are likely to change at the same time;
- Isolate items that seem likely to change. Design the interclass interfaces to be insensitive to the potential changes. Design the interfaces so that changes are limited to the inside of the class and the outside remains unaffected. Any other class using the changed class should be unaware that the change has occurred. The class’s interface should protect its secrets.
- Areas likely to change:
- Business logic;
- Hardware dependencies
- Examples of hardware dependencies include interfaces to screens, printers, keyboards, mice, disk drives, sound facilities, and communications devices. Isolate hardware dependencies in their own subsystem or class. Isolating such dependencies helps when you move the program to a new hardware environment;
- It also helps initially when you’re developing a program for volatile hardware. You can write software that simulates interaction with specific hardware, have the hardware-interface subsystem use the simulator as long as the hardware is unstable or unavailable, and then unplug the hardware-interface subsystem from the simulator and plug the subsystem into the hardware when it’s ready to use.
- Input and output;
- Nonstandard language features;
- Difficult design and construction areas:
- It’s a good idea to hide difficult design and construction areas because they might be done poorly and you might need to do them again. Compartmentalize them and minimize the impact their bad design or construction might have on the rest of the system.
- Status variables:
- Status variables indicate the state of a program and tend to be changed more frequently than most other data. In a typical scenario, you might originally define an error-status variable as a boolean variable and decide later that it would be better implemented as an enumerated type with the values ErrorType_None, ErrorType_Warning, and ErrorType_Fatal;
- You can add at least two levels of flexibility and readability to your use of status variables:
- Don’t use a boolean variable as a status variable. Use an enumerated type instead. It’s common to add a new state to a status variable, and adding a new type to an enumerated type requires a mere recompilation rather than a major revision of every line of code that checks the variable;
- Use access routines rather than checking the variable directly. By checking the access routine rather than the variable, you allow for the possibility of more sophisticated state detection. For example, if you wanted to check combinations of an error-state variable and a current-function-state variable, it would be easy to do if the test were hidden in a routine and hard to do if it were a complicated test hard-coded throughout the program.
- Data type constraints
- Use variables and reference them anywhere are needed. Don’t rely on hard coded magic numbers.
Anticipating Different Degrees of Change
- If a change is likely, make sure that the system can accommodate it easily;
- Only extremely unlikely changes should be allowed to have drastic consequences for more than one class in a system;
- Good designers also factor in the cost of anticipating change. If a change is not terribly likely, but easy to plan for, you should think harder about anticipating it than if it isn’t very likely and is difficult to plan for.
Keep Coupling Loose
- Coupling describes how tightly a class or routine is related to other classes or routines;
- The goal is to create classes and routines with small, direct, visible, and flexible relations to other classes and routines (loose coupling);
- The concept of coupling applies equally to classes and routines, so for the rest of this discussion I’ll use the word “module” to refer to both classes and routines;
- Good coupling between modules is loose enough that one module can easily be used by other modules;
- Model railroad cars are coupled by opposing hooks that latch when pushed together. Connecting two cars is easy—you just push the cars together. Imagine how much more difficult it would be if you had to screw things together, or connect a set of wires, or if you could connect only certain kinds of cars to certain other kinds of cars. The coupling of model railroad cars works because it’s as simple as possible;
- In software, make the connections among modules as simple as possible;
- Try to create modules that depend little on other modules. Make them detached, as business associates are, rather than attached, as Siamese twins are;
- refers to the number of connections between modules. A routine that takes one parameter is more loosely coupled to modules that call it than a routine that takes six parameters. A class with four well-defined public methods is more loosely coupled to modules that use it than a class that exposes 37 public methods;
- Refers to the prominence of the connection between two modules;
- Passing data in a parameter list is making an obvious connection and is therefore good. Modifying global data so that another module can use that data is a sneaky connection and is therefore bad.
- Refers to how easily you can change the connections between modules. Ideally, you want something more like the USB connector on your computer than like bare wire and a soldering gun.
Kinds of Coupling
- Simple data parameter coupling
- Two modules are simple-data-parameter coupled if all the data passed between them are of primitive data types and all the data is passed through parameter lists. This kind of coupling is normal and acceptable.
- Simple object coupling
- A module is simple-object coupled to an object if it instantiates that object. This kind of coupling is fine.
- Object-parameter coupling
- Two modules are object-parameter coupled to each other if Object1 requires Object2 to pass it an Object3. This kind of coupling is tighter than Object1 requiring Object2 to pass it only primitive data types.
- Semantic coupling
- The most insidious kind of coupling occurs when one module makes use, not of some syntactic element of another module, but of some semantic knowledge of another module’s inner workings. Here are some examples:
- Module1 passes a control flag to Module2 that tells Module2 what to do. This approach requires Module1 to make assumptions about the internal workings of Module2, namely, what Module2 is going to with the control flag. If Module2 defines a specific data type for the control flag (enumerated type or object), this usage is probably OK;
- Module2 uses global data after the global data has been modified by Module1. This approach requires Module2 to assume that Module1 has modified the data in the ways Module2 needs it to be modified, and that Module1 has been called at the right time;
- Module1’s interface states that its Module1.Initialize() routine should be called before its Module1.Routine1() is called. Module2 knows that Module1.Routine1() calls Module1.Initialize() anyway, so it just instantiates Module1 and calls Module1.Routine1() without calling Module1.Initialize() first;
- Module1 passes Object to Module2. Because Module1 knows that Module2 uses only three of Object’s seven methods, it only initializes Object only partially—with the specific data those three methods need;
- Module1 passes BaseObject to Module2. Because Module2 knows that Module2 is really passing it DerivedObject, it casts BaseObject to DerivedObject and calls methods that are specific to DerivedObject;
- DerivedClass modifies BaseClass’s protected member data directly.
- The most insidious kind of coupling occurs when one module makes use, not of some syntactic element of another module, but of some semantic knowledge of another module’s inner workings. Here are some examples:
- Semantic coupling is dangerous because changing code in the used module can break code in the using module in ways that are completely undetectable by the compiler. When code like this breaks, it breaks in subtle ways that seem unrelated to the change made in the used module, which turns debugging into a Sisyphean task;
Design Heuristics Recap:
- Find Real-World Objects;
- Form Consistent Abstractions;
- Encapsulate Implementation Details;
- Inherit When Possible;
- Hide Secrets (Information Hiding);
- Identify Areas Likely to Change;
- Keep Coupling Loose;
- Look for Common Design.
- Aim for Strong Cohesion;
- Build Hierarchies;
- Formalize Class Contracts;
- Assign Responsibilities;
- Design for Test;
- Avoid Failure;
- Choose Binding Time Consciously;
- Make Central Points of Control;
- Consider Using Brute Force;
- Draw a Diagram;
- Keep Your Design Modular.
Guidelines for Heuristics – it details the steps in solving any problem – read the whole chapter from the book, all is interesting.
5.4 Design Practices
- Design is an iterative process;
- As you cycle through candidate designs and try different approaches, you’ll look at both high-level and low-level views. The big picture you get from working with high-level issues will help you to put the low-level details in perspective;
- The details you get from working with low-level issues will provide a foundation in solid reality for the high-level decisions. The tug and pull between top-level and bottom-level considerations is a healthy dynamic; it creates a stressed structure that’s more stable than one built wholly from the top down or the bottom up;
- Many programmers—many people, for that matter—have trouble ranging between high-level and low-level considerations. Switching from one view of a system to another is mentally strenuous, but it’s essential to effective design;
- When you come up with a first design attempt that seems good enough, don’t stop! The second attempt is nearly always better than the first, and you learn things on each attempt that can improve your overall design;
- Even if you end up searching for other approaches with no result, it’s not wasted time. You found things which did not work and these are just as helpful as the ones which do work because you know you can’t go a certain path;
Divide and Conquer
- Divide the program into different areas of concern, and then tackle each of those areas individually;
- If you run into a dead end in one of the areas, iterate;
- Incremental refinement is a powerful tool for managing complexity;
- Understand the problem, then devise a plan, then carry out the plan, then look back to see how you did.
Top-Down and Bottom-Up Approaches
- Is a decomposition strategy;
- When you start with general classes and decompose them into more specialized classes step by step, your brain isn’t forced to deal with too many details at once;
- At various points in the decomposition, you’ll have choices about which way to partition the subsystems, lay out the inheritance tree, and form compositions of objects. You make a choice and see what happens. Then you start over and decompose it another way and see whether that works better. After several attempts, you’ll have a good idea of what will work and why;
- How far do you decompose a program? Continue decomposing until it seems as if it would be easier to code the next level than to decompose it. Work until you become somewhat impatient at how obvious and easy the design seems. At that point, you’re done. If it’s not clear, work some more;
- The strength of top-down design is that it’s easy. People are good at breaking something big into smaller components, and programmers are especially good at it;
- Another strength of top-down design is that you can defer construction details. Since systems are often perturbed by changes in construction details (for example, changes in a file structure or a report format), it’s useful to know early on that those details should be hidden in classes at the bottom of the hierarchy.
- Is a composition strategy;
- Sometimes the top-down approach is so abstract that it’s hard to get started. If you need to work with something more tangible, try the bottom-up design approach. Ask yourself, “What do I know this system needs to do?”;
- You might identify a few low-level responsibilities that you can assign to concrete classes. After you identify several low-level responsibilities, you’ll usually start to feel comfortable enough to look at the top again;
- Things to keep in mind as you do bottom-up composition:
- Ask yourself what you know the system needs to do;
- Identify concrete objects and responsibilities from that question;
- Identify common objects and group them using subsystem organization, packages, composition within objects, or inheritance, whichever is appropriate;
- Continue with the next level up, or go back to the top and try again to work down.
- One strength of the bottom-up approach is that it typically results in early identification of needed utility functionality, which results in a compact, well factored design. If similar systems have already been built, the bottom-up approach allows you to start the design of the new system by looking at pieces of the old system and asking, “What can I reuse?”;
- A weakness of the bottom-up composition approach is that it’s hard to use exclusively. It’s like the old assemble-it-yourself problem: I thought I was done, so why does the box still have parts in it? Fortunately, you don’t have to use the bottom-up composition approach exclusively;
- Another weakness of the bottom-up design strategy is that sometimes you find that you can’t build a program from the pieces you’ve started with. You can’t build an airplane from bricks, and you might have to work at the top before you know what kinds of pieces you need at the bottom.
- To summarize, top down tends to start simple, but sometimes low-level complexity ripples back to the top, and those ripples can make things more complex than they really needed to be. Bottom up tends to start complex, but identifying that complexity early on leads to better design of the higher-level classes—if the complexity doesn’t torpedo the whole system first;
- In the final analysis, top-down and bottom-up design aren’t competing strategies—they’re mutually beneficial. Design is a heuristic process, which means that no solution is guaranteed to work every time; design contains elements of trial and error. Try a variety of approaches until you find one that works well.
- Sometimes you can’t really know whether a design will work until you better understand some implementation detail;
- You might not know if a particular database organization will work until you know whether it will meet your performance goals;
- A general technique for addressing these questions at low cost is experimental prototyping. Prototyping works poorly when developers aren’t disciplined about writing the absolute minimum of code needed to answer a question;
- In case of database performance prototyping for example, you can then write very simple prototyping code that uses tables with names like Table1, Table2, and Column1, and Column2, populate the tables with junk data, and do your performance testing;
- Prototyping also works poorly when the design question is not specific enough.
- A design question like, “Will this database framework work?” does not provide enough direction for prototyping;
- A design question like, “Will this database framework support 1,000 transactions per second under assumptions X, Y, and Z” provides a more solid basis for prototyping.
- A final risk of prototyping arises when developers do not treat the code as throwaway code. I have found that it is not possible for people to write the absolute minimum amount of code to answer a question if they believe that the code will eventually end up in the production system. They end up implementing the system instead of prototyping. A technique which can be used in these cases is prefixing the classes with Prototype, this way, the developers will think twice before extending such a class.
- In design, two heads are often better than one, whether those two heads are organized formally or informally;
- Collaboration can take any of several forms:
- You informally walk over to a co-worker’s desk and ask to bounce some ideas around;
- You and your co-worker sit together in a conference room and draw design alternatives on a whiteboard;
- You and your co-worker sit together at the keyboard and do detailed design in the programming language you’re using;
- You schedule a meeting to walk through your design ideas with one or more co-workers;
- You schedule a formal inspection with all the structured described in Chapter 21;
- You don’t work with anyone who can review your work, so you do some initial work, put it into a drawer, and come back to it a week later. You will have forgotten enough that you should be able to give yourself a fairly good review.
How Much Design is Enough?
- Check table 5.2 for design formality levels depending on the project and team experience;
- If I can’t decide how deeply to investigate a design before I begin coding, I tend to err on the side of going into more detail. The biggest design errors are those in which I thought I went far enough, but it later turns out that I didn’t go far enough to realize there were additional design challenges. In other words, the biggest design problems tend to arise not from areas I knew were difficult and created bad designs for, but from areas I thought were easy and didn’t create any designs for at all;
- I rarely encounter projects that are suffering from having done too much design work;
- On the other hand, occasionally I have seen projects that are suffering from too much design documentation. I would rather see 80 percent of the design effort go into creating and exploring numerous design alternatives and 20 percent go into creating less polished documentation than to have 20 percent go into creating mediocre design alternatives and 80 percent go into polishing documentation of designs that are not very good.
Capturing Your Design Work
- The traditional approach to capturing design work is to write up the designs in a formal design document;
- However, there are numerous alternative ways to capture designs that can work well on small projects, informal projects, or projects that are otherwise looking for a lightweight way to capture a design:
- Insert design documentation into the code itself;
- Capture design discussions and decisions on a Wiki;
- Write email summaries;
- Use a digital camera and take pictures of diagrams on whiteboards;
- Save design flipcharts;
- Use CRC cards;
- Create UML diagrams at appropriate levels of detail.
5.5 Comments on Popular Methodologies
- Big Design Up Front – BDUF is bad. You’re better off not doing any design before you begin coding;
- In 10 years the pendulum has swung from “design everything” to “design nothing.” But the alternative to BDUF isn’t no design up front, it’s a Little Design Up Front (LDUF) or Enough Design Up Front—ENUF;
- How do you tell how much is enough? That’s a judgment call, and no one can make that call perfectly. But while you can’t know the exact right amount of design with any confidence, there are two amounts of design that are guaranteed to be wrong every time: designing every last detail and not designing anything at all;
- Treat design as a wicked, sloppy, heuristic process. Don’t settle for the first design that occurs to you. Collaborate. Strive for simplicity. Prototype when you need to. Iterate, iterate, and iterate again. You’ll be happy with your designs.
Make sure to read the Checklist and Key Points from the end of the chapter.