Chapter 10: Designing the Product

    1. 10A. General design tips
      1. 10.1 Abstraction is your friend
      2. 10.2 Separate concerns
      3. 10.3 Don't talk to strangers
      4. 10.4 Encapsulate
      5. 10.5 Preserve conceptual integrity
      6. 10.6 Standardise solutions
      7. 10.7 Use patterns
      8. 10.8 Do not overuse patterns
      9. 10.9 Value simplicity
      10. 10.10 Increase Fan-in, decrease Fan-out
      11. 10.11 Give brute force a chance
      12. 10.12 Less is more
      13. 10.13 Do not forget non-functional qualities
      14. 10.14 Beware of premature optimisation
      15. 10.15 Avoid analysis paralysis
      16. 10.16 Polish documents later
      17. 10.17 Remember the reason
    2. 10B. UI design tips
      1. 10.18 Design it for the user
      2. 10.19 Usability is king
      3. 10.20 Do a prototype
      4. 10.21 Be different, if you must
    3. 10C. API design tips
      1. 10.22 Talk to API clients
      2. 10.23 Choose descriptive names
      3. 10.24 Name consistently
      4. 10.25 Hide internal details
      5. 10.26 Complete primitive operations
      6. 10.27 Avoid bloat
      7. 10.28 Avoid 'Swiss Army Knife' operations
      8. 10.29 Make common things easy, rare things possible
      9. 10.30 Promise less
      10. 10.31 Specify abnormal behaviours
      11. 10.32 Choose atomic over multi-step
      12. 10.33 Match the abstraction
      13. 10.34 Keep to the same level of abstraction
      14. 10.35 Evolve as necessary
      15. 10.36 Document the API
      16. 10.37 Generate documentation from code

In a room full of top software designers, if two agree on the same thing, that's a majority. -- Bill Curtis

Most hard-core project courses put emphasis on the design aspect. Here are some things to keep in mind when designing your system.

10A. General design tips

10.1 Abstraction is your friend

A software design is a complex thing, especially, if you do not use abstraction to good effect. The design should be done at various levels of abstraction. At higher levels, you visualise the system as a small number of big components while abstracting away the details of what is inside each component. Once you have a good definition of what each of those components represent and how they interact with each other, you can move to the next lower level of abstraction. That is, you take each one of those components and design it using a small number of smaller components. This can go on until you reach a lower level of abstraction that is concrete enough to transform to an implementation.

10.2 Separate concerns

We should try to keep different 'concerns' of the system as separated from each other as possible. For example, parsing (and things related to the 'parsing' concern) should be done by the parser component, and everything that has to do with sorting should be done by the sorter component.

10.3 Don't talk to strangers

Otherwise known as the Law of Demeter, the 'don't talk to strangers' principle advocates keeping unrelated things independent of each other. For example, if the parser component can function without any knowledge of the sorter component, then the sorter is a stranger to the parser. That means the parser should not have any reference to the sorter (e.g. can you compile the parser without compiling the sorter?)

A classic example where this principle applies is when choosing between a central controller model and chain of controllers model. The former lets you keep strangers as strangers, while the latter forces strangers to talk to each other. Notice how one design below lets B and C remain strangers while the other forces them to know each other.

Along the same vein, minimise communication between components. Avoid cyclic dependencies (e.g. A calls B, B calls C and C calls A

10.4 Encapsulate 

A component should reveal as little as possible about itself. This is also known as information hiding. For example, other components that interact with your component should not know in what format your component stores data and they should not be allowed to manipulate those data directly.

10.5 Preserve conceptual integrity

Fred Brooks contends that (in the book The Mythical Man-Month)

... the conceptual integrity is the most important consideration in system design. It is better for a system to omit certain anomalous features and improvements, but to reflect one set of design ideas, than to have one that contains many good but independent and uncoordinated ideas.
A student team is a team of peers that usually does not have a designated architect to dictate a design for others to follow. Everyone may want to have their say in the design. However, after discussing all alternative designs proposed, you should still choose one of them to follow, rather than devise a design that combines everyone's ideas. Combining ideas into one design has its merits, but do not do it just for the sake of achieving a compromise between competing ideas. If in doubt, get your supervisor's opinion as well.

10.6 Standardise solutions

Similar problems should be solved in a similar fashion. Sometimes, it pays to solve even slightly different yet largely similar problems in exactly the same way. It makes programs easier to understand. In other words, do not go out of your way to customise a solution to fit a problem precisely. It may be better to preserve the similarity of the solution instead.

10.7 Use patterns

Patterns (design patterns [http://tinyurl.com/wikipedia-patterns], as well as other types of patterns such as analysis patterns, testing patterns, etc.) embody tried-and-tested solutions to common problems. Learn patterns and use them where applicable.

10.8 Do not overuse patterns

Most patterns come with extra baggage. Do not use patterns for the sake of using them. For example, there is no need to apply the Singleton pattern to every class that will have only one instance in the system; use it when there is a danger of someone creating multiple instances of such a class.

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies,
and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.
--C.A.R. Hoare

10.9 Value simplicity

Try to make the design as simple as possible (but no simpler!). Simple yet elegant designs are much better than complex solutions: The former is much harder to achieve however, but that is what you should strive for. Given any design, try to see whether you can simplify it. Resist any change that makes it more complex.

10.10 Increase Fan-in, decrease Fan-out

When many other components use a given component (i.e. the component has high fan-in), this is a good thing because it increases reuse. When a given component uses many other components (i.e. it has high fan-out), this is not a good thing because it increases coupling.

10.11 Give brute force a chance

Some problems have an obvious and simple brute force solution. Do not dismiss this solution too quickly in your haste to look for a smarter solution. If you can afford it, give this brute force solution a chance; it may be all you need.

10.12 Less is more

Trim all parts of the design that are not immediately useful to the system. It does not matter how elegant they are, how proud you are of dreaming them up, and how hard you worked at building them. The same applies to code.

10.13 Do not forget non-functional qualities

Some non-functional qualities need to be incorporated from the design stage. One non-functional quality rarely mentioned in the specification and often forgotten in the design is the testability. Improving testability improves many other qualities of the design.

10.14 Beware of premature optimisation

Hoare wasn't kidding when he said "Premature optimization is the root of all evil in programming". Opt for a simple design. If it is fast enough, stick with it. If it is not, find the real bottlenecks (profiling tools [Chapter 9] can be used for this purpose) and optimise accordingly.

Caveat: This does not mean that you should start with a stupid design. Some designs are obviously inefficient and should be discarded immediately. Start with a design that is, in Einstein's words "as simple as possible, but no simpler".

10.15 Avoid analysis paralysis

During analysis and design, consider all the known facts but do not fret too much about unknowns. If you are given a concrete and stable specification (e.g. writing a parser for a given language) it would be stupid to start with a design that does not take the entire specification into account. Such short-sighted designs will eventually require change, causing rework that could have been avoided. On the other hand, if you are defining a first-of-a-kind exploratory system for an unspecified user base, go for a design with a reasonable degree of flexibility; do not worry about all the nitty-gritty issues that it might or might not have to face later.

10.16 Polish documents later

Documenting design often requires wrestling with UML editors and other diagramming tools. Therefore, it is very frustrating when we have to modify those documents as the design changes over time.

While designs should be documented as they are done, there is no need to start creating well-polished design documents right away. You can keep the documentation as low-maintenance rough sketches (with none of the important points missing) until the design is sufficiently stable. For example, you can take a photo of the whiteboard on which you drew the initial design, print it out, do your (minor) modifications on the hard copy, and convert it to a digitised UML diagram much later.

10.17 Remember the reason

As students, you are not expected to choose the best design in the first try.  Designs often evolve over time. Be sure to document such changes so that you get credit for the effort spent in the process. Make sure your design has a rationale. Often evaluators ask "why did you choose this design (over another)?" No design is perfect. You may be able to score extra credit by critically evaluating your final design, comparing it with an alternative design, and discussing ways of improving it further.

10B. UI design tips

The design of the UI is important because even the best functionality will not compensate for a poor user interface,
and people will not use the system unless they can successfully interact with it [adapted from the book Automated Defect Prevention: Best Practices in Software Management]

10.18 Design it for the user

The UI is for the user; design it for the user. It should be easy to learn, understand, remember, and navigate for the user (not you, the developer). A UI that makes the user wonders "where do I start?" the first time he looks at it is not an intuitive UI.

10.19 Usability is king

Do your best to improve the usability of the UI. Here are some things to consider:
...user's aren't stupid; they just simply are't programmers. Users are all smart and knowledgeable in their own way. They are doctors and lawyers and accountants, who are struggling to use the computer because programmers are too 'stupid' to understand law and medicine and accounting [http://tinyurl.com/stupidprogrammers].

10.20 Do a prototype

Do an early UI prototype/mockup and get feedback from teammates/instructor/client before implementing the whole system. UI prototypes are great in answering the "are we building the right system?" question. Note that a UI prototype need not have the final 'polished' look, and it need not be complete either. It can be drawn on paper, done using PowerPoint, drag-and-drop UI designers that come with IDEs such as Visual Studio, or using special prototyping tools such as Balsamiq.

10.21 Be different, if you must

There is no need to follow what everyone one else does, but do not do things differently just for the heck of it. Deviating from familiar interaction mechanisms might confuse the user. For example, most Windows users are familiar with the 'left-click to activate, right-click for context menu' way of interacting, and will be irritated if you use a different approach.

10C. API design tips

An API that isn't comprehensible isn't usable. --James Gosling

An API (Application Programming Interface) is a set of operations a software component (i.e. a system, a sub-system, class) provides to its clients. For example, here [http://tinyurl.com/stringAPI] is the API of the Java String class. 

A well-defined API is not only easy to use, but it gives the implementers (of that component) flexibility in implementation. As clients of the component write code against the API (i.e. they only know about the API), implementers have the freedom to change internal implementation details. As long as the API remains stable, client code will not have to change. For example, as clients of the String class, we only know its API, but we can use it without knowing how it is implemented internally.

When we design a system, we should use APIs to achieve a similar degree of freedom between components so that developers of those components can work independently of each other. For example, let us assume our system has two sub-systems: front-end and back-end. Let us also assume that the front-end uses the back-end. In this case we must first define the back-end API. Once that is defined, both the front-end team and the back-end team can start work in parallel instead of the front-end team waiting until the completed back-end is available. That is because the front-end team is assured that the back-end will support the agreed API irrespective of how it will be implemented. Therefore, defining APIs is an integral part of designing software.

10.22 Talk to API clients

To define the API of a component, we must clearly understand the needs of the component's clients. The best way to discover an API of a component, therefore, is to talk to those who will use the component. For example, clients for the back-end include front-end. Therefore, the back-end API should be defined based on a discussion between the front-end team and the back-end team. Similarly, if you are writing a utility class to be used by many other classes in the system, you must define the API of your class based on the requirement of those client classes.

10.23 Choose descriptive names

API operation names and parameter names should have intuitive meanings. E.g. setParent(parent, child) is more meaningful than setPrt(p,c). The chapter on 'implementation' has some more tips about naming.

10.24 Name consistently

If you have two operations addVariable() and removeVar(), you are not naming them consistently. Name the second operation removeVariable(), or the first operation addVar(). When using a consistently named API, users can simply deduce the correct operation to call without actually checking the API documentation.

10.25 Hide internal details

The API should not reveal clues about the internal structures. For example, addToVarHashtable() reveals that we are using a Hashtable to store variables; what if later we want to change it to a Vector?

10.26 Complete primitive operations

Make sure the component provides a complete set of primitive operations. You can add more specialised operations as necessary and implement them using the primitive operations. Even if you do not supply those specialised operations, clients will be able to accomplish such specialised tasks using the primitive operations. This avoids polluting the API with highly-specialised yet rarely-used operations.

10.27 Avoid bloat

If the API is becoming too long, may be the component is doing more than it should; consider splitting the component if appropriate. Or else, the component may be supporting too many operations to achieve the same thing. Strive to achieve a minimal yet complete API (refer tip 10.26).

10.28 Avoid 'Swiss Army Knife' operations

Having multi-purpose methods causes unnecessary complexities, performance penalties, and misunderstandings. Make each operation do one thing and only one thing. Rather than have a writeToFile(string text, boolean isAppend) where the second parameter indicates whether to overwrite or append, have two methods appendToFile(string text) and overwriteFile(string text).

Caveat: An over-enthusiastic application of this guideline could lead to a bloated API [see tip 10.27]

10.29 Make common things easy, rare things possible

This is another way to look at the previous two points. On the one hand the API should provide direct and easy-to-use ways to accomplish common tasks. On the other hand, rarely-used highly-specialised tasks need not be directly supported; instead, we can let clients implement these operations themselves, using a combination of existing primitive operations.

10.30 Promise less

An API is a contract (between the implementer of the component, and users of the component); just like any contract, promising less makes it easier to deliver. E.g. If an operation is for calculating the root of positive integers, it should be findRoot(PositiveInt i), not findRoot(int i)

10.31 Specify abnormal behaviours

An operation should throw exceptions to indicate exceptional situations (e.g. when an input file was not found). These should be clearly specified in the API. A common mistake is to specify only the typical behaviour.

10.32 Choose atomic over multi-step

When a set of steps is always done together in the same sequence, capture that as one atomic operation instead of having separate operations for each step. For example, imagine that your component has to parse a file that contains some program code, and create the corresponding abstract syntax tree (AST).

Option 1: Have one operation that takes the file name as the parameter and returns the resultant AST  parseFile(String fileName): AST
Option 2: Have three operations setFileName(String fileName), parse(), getAST(): AST

Clearly the first option is better; it makes the API shorter and reduces chances for error (what if we call parse method without calling the setFileName method first?).

10.33 Match the abstraction

Operations in the API should be in tune with the abstraction the API represents. For example, a Parser API should use terms related to Parsing. This is also applicable to the exceptions thrown by the API. For example, a Parser API should throw exceptions that are related to a Parser.

10.34 Keep to the same level of abstraction

Try to keep all operations of an API on the same level of abstraction. An API that has a mix of high-level and low-level operations is harder to understand. For example, the File API should not mix high-level operations such as open(), close(), append(String) with low-level operations such as those for direct manipulation of bits inside the file.

10.35 Evolve as necessary

While we try to keep APIs stable, it is not uncommon for changing client requirements to force changes in existing APIs. If you have multiple clients, make sure that a change requested by one client does not adversely affect the other clients.

Furthermore, we sometimes evolve the API as our understanding of the client requirements improves.

It is OK to specify the data types in the operation header using symbolic names, and later refine them into concrete types. For example, the return type can be specified as list-of-TreeNodes and later refined to TreeNode[] or ArrayList<TreeNode>.

10.36 Document the API

The API documentation should be precise, unambiguous, concise and complete. Do not leave room for the client to make assumptions. Instead, explicitly specify what a method does. You can use this API description of the Java String class as a guiding example when documenting APIs, although yours need not be as elaborate.

10.37 Generate documentation from code

Find a way to generate the API documentation from the code itself and comments therein. An example of this strategy in action is the Java API documentation generated by Javadoc comments embedded in the code. Doxygen is another tool that can help here.

 

Giving feedback

Any suggestions to improve this book? Any tips you would like to add? Any aspect of your project not covered by the book? Anything in the book that you don't agree with? Noticed any errors/omissions? Please use the link below to provide feedback, or send an email to damith[at]comp.nus.edu.sg

Sharing this book helps too!

 

---| This page is from the free online book Practical Tips for Software-Intensive Student Projects V3.0, Jul 2010, Author: Damith C. Rajapakse |---

free statistics free hit counter javascript