Chapter 6: Working Classes

6.2 Good Class Interfaces

  • Create good abstraction for classes;
  • If you think of the class’s public routines as an air lock that keeps water from getting into a submarine, inconsistent public routines are leaky panels in the class. The leaky panels might not let water in as quickly as an open air lock, but if you give them enough time, they’ll still sink the boat. In practice, this is what happens when you mix levels of abstraction. As the program is modified, the mixed levels of abstraction make the program harder and harder to understand, and it gradually degrades until it becomes unmaintainable;
  • Each time you add a routine to a class interface, ask, “Is this routine consistent with the abstraction provided by the existing interface?” If not, find a different way to make the modification, and preserve the integrity of the abstraction;
  • The ideas of abstraction and cohesion are closely related—a class interface that presents a good abstraction usually has strong cohesion. Classes with strong cohesion tend to present good abstractions, although that relationship is not as strong. Focusing on the abstraction presented by the class interface tends to provide more insight into class design than focusing on class cohesion. If you see that a class has weak cohesion and aren’t sure how to correct it, ask yourself whether the class presents a consistent abstraction instead;

Good Encapsulation

  • Minimizing accessibility is one of several rules that are designed to encourage encapsulation. If you’re wondering whether a specific routine should be public, private, or protected, one school of thought is that you should favor the strictest level of privacy that’s workable. I think that’s a fine guideline, but I think the more important guideline is, “What best preserves the integrity of the interface abstraction?” If exposing the routine is consistent with the abstraction, it’s probably fine to expose it. If you’re not sure, hiding more is generally better than hiding less;
  • Don’t expose member data in public;
  • Don’t make assumptions about the class’s users. A class should be designed and implemented to adhere to the contract implied by the class interface. It shouldn’t make any assumptions about how that interface will or won’t be used, other than what’s documented in the interface. Comments like this are an indication that a class is more aware of its users than it should be:
    • initialize x, y, and z to 1.0 because DerivedClass blows up if they’re initialized to 0.0.
  • Don’t put a routine into the public interface just because it uses only public routines. The fact that a routine uses only public routines is not a very significant consideration. Instead, ask whether exposing the routine would be consistent with the abstraction presented by the interface;
  • Favor read-time convenience to write-time convenience. Code is read far more times than it’s written, even during initial development. Favoring a technique that speeds write-time convenience at the expense of read time convenience is a false economy. This is especially applicable to creation of class interfaces. Even if a routine doesn’t quite fit the interface’s abstraction, sometimes it’s tempting to add a routine to an interface that would be convenient for the particular client of a class that you’re working on at the time. But adding that routine is the first step down a slippery slope, and it’s better not to take even the first step;
  • Be very, very wary of semantic violations of encapsulation. The difficulty of semantic encapsulation compared to syntactic encapsulation is similar. Syntactically, it’s relatively easy to avoid poking your nose into the internal workings of another class just by declaring the class’s internal routines and data private. Achieving semantic encapsulation is another matter entirely. Here are some examples of the ways that a user of a class can break encapsulation semantically:
    • Not calling Class A’s Initialize() routine because you know that Class A’s PerformFirstOperation() routine calls it automatically;
    • Not calling the database.Connect() routine before you call employee.Retrieve( database ) because you know that the employee.Retrieve() function will connect to the database if there isn’t already a connection;
    • Not calling Class A’s Terminate() routine because you know that Class A’s PerformFinalOperation() routine has already called it;
    • Using a pointer or reference to ObjectB created by ObjectA even after ObjectA has gone out of scope, because you know that ObjectA keeps ObjectB in static storage, and ObjectB will still be valid;
    • Using ClassB’s MAXIMUM_ELEMENTS constant instead of using ClassA.MAXIMUM_ELEMENTS, because you know that they’re both equal to the same value.
  • The problem with each of these examples is that they make the client code dependent not on the class’s public interface, but on its private implementation. Anytime you find yourself looking at a class’s implementation to figure out how to use the class, you’re not programming to the interface; you’re programming through the interface to the implementation. If you’re programming through the interface, encapsulation is broken, and once encapsulation starts to break down, abstraction won’t be far behind;
  • If you can’t figure out how to use a class based solely on its interface documentation, the right response is not to pull up the source code and look at the implementation. That’s good initiative but bad judgment. The right response is to contact the author of the class and say, “I can’t figure out how to use this class”. The right response on the class-author’s part is not to answer your question face to face. The right response for the class author is to check out the class-interface file, modify the class-interface documentation, check the file back in, and then say, “See if you can understand how it works now”. You want this dialog to occur in the interface code itself so that it will be preserved for future programmers. You don’t want the dialog to occur solely in your own mind, which will bake subtle semantic dependencies into the client code that uses the class. And you don’t want the dialog to occur interpersonally so that it benefits only your code but no one else’s;
  • Watch for coupling that’s too tight. Coupling refers to how tight the connection is between two classes. In general, the looser the connection, the better. Several general guidelines flow from this concept:
    • Minimize accessibility of classes and members;
    • Avoid friend classes, because they’re tightly coupled;
    • Avoid making data protected in a base class because it allows derived classes to be more tightly coupled to the base class;
    • Avoid exposing member data in a class’s public interface;
    • Be wary of semantic violations of encapsulation;
    • Observe the Law of Demeter;
    • Coupling goes hand in glove with abstraction and encapsulation. Tight coupling occurs when an abstraction is leaky, or when encapsulation is broken. If a class offers an incomplete set of services, other routines might find they need to read or write its internal data directly. That opens up the class, making it a glass box instead of a black box, and virtually eliminates the class’s encapsulation.

6.3 Design and Implementation Issues

Containment (“has a” relationships)

  • Containment is the simple idea that a class contains a primitive data element or object;
  • A lot more is written about inheritance than about containment, but that’s because inheritance is more tricky and error prone, not because it’s better;
  • Implement “has a” through containment:
    • One way of thinking of containment is as a “has a” relationship. For example, an employee “has a” name, “has a” phone number, “has a” tax ID, and so on. You can usually accomplish this by making the name, phone number, or tax ID member data of the Employee class.
  • Implement “has a” through private inheritance as a last resort:
    • In some instances you might find that you can’t achieve containment through making one object a member of another. In that case, some experts suggest privately inheriting from the contained object. The main reason you would do that is to set up the containing class to access protected member functions or data of the class that’s contained. In practice, this approach creates an overly cozy relationship with the ancestor class and violates encapsulation. It tends to point to design errors that should be resolved some way other than through private inheritance.
  • Be critical of classes that contain more than about seven members:
    • The number 7+/-2 has been found to be a number of discrete items a person can remember while performing other tasks. If a class contains more than about seven data members, consider whether the class should be decomposed into multiple smaller classes. You might err more toward the high end of 7+/-2 if the data members are primitive data types like integers and strings; more toward the lower end of 7+/-2 if the data members are complex objects.

Inheritance (“is a” relationships)

  • Inheritance is the complex idea that one class is a specialization of another class;
  • The purpose of inheritance is to create simpler code by defining a base class that specifies common elements of two or more derived classes;
  • A great many of the problems in modern programming arise from overly enthusiastic use of inheritance;
  • When you decide to use inheritance, you have to make several decisions:
    • For each member routine, will the routine be visible to derived classes? Will it have a default implementation? Will the default implementation be overridable?
    • For each data member (including variables, named constants, enumerations, and so on), will the data member be visible to derived classes?

Implement “is a” through public inheritance

  • When a programmer decides to create a new class by inheriting from an existing class, that programmer is saying that the new class “is a” more specialized version of the older class. The base class sets expectations about how the derived class will operate;
  • If the derived class isn’t going to adhere completely to the same interface contract defined by the base class, inheritance is not the right implementation technique. Consider containment or making a change further up the inheritance hierarchy.

Design and document for inheritance or prohibit it

  • Inheritance adds complexity to a program, and, as such, it is a dangerous technique;
  • If a class isn’t designed to be inherited from, make it sealed so that you can’t inherit from it.

Adhere to the Liskov Substitution Principle

  • You shouldn’t inherit from a base class unless the derived class truly “is a” more specific version of the base class;
  • Subclasses must be usable through the base class interface without the need for the user to know the difference. In other words, all the routines defined in the base class should mean the same thing when they’re used in each of the derived classes;
  • If you have a base class of Account, and derived classes of CheckingAccount, SavingsAccount, and AutoLoanAccount, a programmer should be able to invoke any of the routines derived from Account on any of Account’s subtypes without caring about which subtype a specific account object is;
  • If a program has been written so that the Liskov Substitution Principle is true, inheritance is a powerful tool for reducing complexity because a programmer can focus on the generic attributes of an object without worrying about the details;
  • If a programmer must be constantly thinking about semantic differences in subclass implementations, then inheritance is increasing complexity rather than reducing it. Suppose a programmer has to think, “If I call the InterestRate() routine on CheckingAccount or SavingsAccount, it returns the interest the bank pays, but if I call InterestRate() on AutoLoanAccount I have to change the sign because it returns the interest the consumer pays to the bank.” According to Liskov, the InterestRate() routine should not be inherited because its semantics aren’t the same for all derived classes.

Be sure to inherit only what you want to inherit

  • A derived class can inherit member routine interfaces, implementations, or both;
  • Inherited routines come in three basic flavors:
    • An abstract overridable routine means that the derived class inherits the routine’s interface but not its implementation;
    • An overridable routine means that the derived class inherits the routine’s interface and a default implementation, and it is allowed to override the default implementation;
    • A non-overridable routine means that the derived class inherits the routine’s interface and its default implementation, and it is not allowed to override the routine’s implementation;
  • When you choose to implement a new class through inheritance, think through the kind of inheritance you want for each member routine. Beware of inheriting implementation just because you’re inheriting an interface, and beware of inheriting an interface just because you want to inherit an implementation.

Move common interfaces, data, and behavior as high as possible in the inheritance tree

  • The higher you move interfaces, data, and behavior, the more easily derived classes can use them;
  • How high is too high? Let abstraction be your guide. If you find that moving a routine higher would break the higher object’s abstraction, don’t do it.

Be suspicious of classes of which there is only one instance

  • A single instance might indicate that the design confuses objects with classes. Consider whether you could just create an object instead of a new class. Can the variation of the derived class be represented in data rather than as a distinct class?

Be suspicious of base classes of which there is only one derived class

  • When I see a base class that has only one derived class, I suspect that some programmer has been “designing ahead”—trying to anticipate future needs, usually without fully understanding what those future needs are;
  • The best way to prepare for future work is not to design extra layers of base classes that “might be needed someday,” it’s to make current work as clear, straightforward, and simple as possible;
  • That means not creating any more inheritance structure than is absolutely necessary.

Be suspicious of classes that override a routine and do nothing inside the derived routine

  • This typically indicates an error in the design of the base class. For instance, suppose you have a class Cat and a routine Scratch() and suppose that you eventually find out that some cats are declawed and can’t scratch. You might be tempted to create a class derived from Cat named ScratchlessCat and override the Scratch() routine to do nothing. There are several problems with this approach:
    • It violates the abstraction (interface contract) presented in the Cat class by changing the semantics of its interface;
    • This approach quickly gets out of control when you extend it to other derived classes. What happens when you find a cat without a tail? Or a cat that doesn’t catch mice? Or a cat that doesn’t drink milk? Eventually you’ll end up with derived classes like ScratchlessTaillessMicelessMilklessCat;
    • Over time, this approach gives rise to code that’s confusing to maintain because the interfaces and behavior of the ancestor classes imply little or nothing about the behavior of their descendents.
  • The place to fix this problem is not in the base class, but in the original Cat class. Create a Claws class and contain that within the Cats class, or build a constructor for the class that includes whether the cat scratches;
  • The root problem was the assumption that all cats scratch, so fix that problem at the source, rather than just bandaging it at the destination.

Avoid deep inheritance trees

  • Object oriented programming provides a large number of techniques for managing complexity;
  • But every powerful tool has its hazards, and some object oriented techniques have a tendency to increase complexity rather than reduce it;
  • Most people have trouble juggling more than two or three levels of inheritance in their brains at once;
  • Deep inheritance trees have been found to be significantly associated with increased fault rates;
  • Deep inheritance trees increase complexity, which is exactly the opposite of what inheritance should be used to accomplish. Make sure you’re using inheritance to minimize complexity.

Prefer inheritance to extensive type checking

  • Frequently repeated case statements sometimes suggest that inheritance might be a better design choice, although this is not always true;
//code example 1:
switch ( shape.type ) 
{
 case Shape_Circle:
 shape.DrawCircle();
 break;
 case Shape_Square:
 shape.DrawSquare();
 break;
}
  • In this example, the calls to shape.DrawCircle() and shape.DrawSquare() should be replaced by a single routine named shape.Draw(), which can be called regardless of whether the shape is a circle or a square;
//code example 2
switch ( ui.Command() ) 
{
 case Command_OpenFile:
 OpenFile();
 break;

case Command_Print:
 Print();
 break;

 case Command_Save:
 Save();
 break;

 case Command_Exit:
 ShutDown();
 break;
}

 

  • In this case, it would be possible to create a base class with derived classes and a polymorphic DoCommand() routine for each command. But the meaning of DoCommand() would be so diluted as to be meaningless, and the case statement is the more understandable solution.

Avoid using a base class’s protected data in a derived class (or make that data private instead of protected in the first place)

  • Inheritance breaks encapsulation;
  • When you inherit from an object, you obtain privileged access to that object’s protected routines and data;
  • If the derived class really needs access to the base class’s attributes, provide protected accessor functions instead.

Why Are There So Many Rules for Inheritance?

  • Inheritance tends to work against the primary technical imperative you have as a programmer, which is to manage complexity;
  • For the sake of controlling complexity you should maintain a heavy bias against inheritance;
  • Here’s a summary of when to use inheritance and when to use containment:
    • If multiple classes share common data but not behavior, then create a common object that those classes can contain;
    • If multiple classes share common behavior but not data, then derive them from a common base class that defines the common routines;
    • If multiple classes share common data and behavior, then inherit from a common base class that defines the common data and routines;
    • Inherit when you want the base class to control your interface; contain when you want to control your interface.

Member Functions and Data

Keep the number of routines in a class as small as possible

  • Statistically, programs found that higher numbers of routines per class were associated with higher fault rates;
  • Other competing factors were found to be more significant, including deep inheritance trees, large number of routines called by a routine, and strong coupling between classes;
  • Evaluate the tradeoff between minimizing the number of routines and these other factors.

Minimize direct routine calls to other classes

  • One study found that the number of faults in a class was statistically correlated with the total number of routines that were called from within a class;
  • The same study found that the more classes a class used, the higher its fault rate tended to be.

Minimize indirect routine calls to other classes

  • Direct connections are hazardous enough. Indirect connections—such as account.ContactPerson().DaytimeContactInfo().PhoneNumber()—tend to be even more hazardous
  • Law of Demeter
    • Object A can call any of its own routines;
    • If Object A instantiates an Object B, it can call any of Object B’s routines. But it should avoid calling routines on objects provided by Object B;
    • In the account example above, that means account.ContactPerson() is OK, but account.ContactPerson().DaytimeContactInfo() is not;
    • Depending on how classes are arranged, it might be acceptable to see an expression like account.ContactPerson().DaytimeContactInfo().

In general, minimize the extent to which a class collaborates with other classes

  • Try to minimize all of the following:
    • Number of kinds of objects instantiated;
    • Number of different direct routine calls on instantiated objects;
    • Number of routine calls on objects returned by other instantiated objects.

Constructors

Initialize all member data in all constructors, if possible

  • Initializing all data members in all constructors is an inexpensive defensive programming practice.

Initialize data members in the order in which they’re declared

  • Using the same order in both places also provides consistency that makes the code easier to read.

Prefer deep copies to shallow copies until proven otherwise

  • One of the major decisions you’ll make about complex objects is whether to implement deep copies or shallow copies of the object;
  • A deep copy of an object is a member-wise copy of the object’s member data
    • //code example
       A ob1 = new A();
       ob1.a = 10;
       A ob2 = new A();
       ob2.a = ob1.a;
       ob1.a = 5; // If you see value of ob2.a after this line, it will be 10.
      
  • A shallow copy typically just points to or refers to a single reference copy
    • //code example
       A ob1 = new A();
       ob1.a = 10;
       A ob2 = new A();
       ob2 = ob1;
       ob1.a = 5; // If you see value of ob2.a after this line, it will be 5.
      
  • Deep copies are simpler to code and maintain than shallow copies;
  • In addition to the code either kind of object would contain, shallow copies add code to count references, ensure safe object copies, safe comparisons, safe deletes, and so on;
  • This code tends to be error prone, and it should be avoided unless there’s a compelling reason to create it;
  • The motivation for creating shallow copies is typically to improve performance;
  • Although creating multiple copies of large objects might be aesthetically offensive, it rarely causes any measurable performance impact;
  • A small number of objects might cause performance issues, but programmers are notoriously poor at guessing which code really causes problems;
  • Because it’s a poor tradeoff to add complexity for dubious performance gains, a good approach to deep vs. shallow copies is to prefer deep copies until proven otherwise.

6.4 Reasons to Create a Class

  • Model real-world objects;
  • Model abstract objects;
  • Reduce complexity;
  • Isolate complexity;
  • Hide implementation details;
  • Limit effects of changes;
  • Hide global data;
  • Streamline parameter passing:
    • If you’re passing a parameter among several routines, that might indicate a need to factor those routines into a class that share the parameter as class data;
    • Streamlining parameter passing isn’t a goal, per se, but passing lots of data around suggests that a different class organization might work better.
  • Make central points of control:
    • Using one class to read from and write to a database is a form of centralized control. If the database needs to be converted to a flat file or to in-memory data, the changes will affect only the one class.
  • Facilitate reusable code;
  • Plan for a family of programs:
    • If you expect a program to be modified, it’s a good idea to isolate the parts that you expect to change by putting them into their own classes. You can then modify the classes without affecting the rest of the program, or you can put in completely new classes instead;
    • Several years ago I managed a team that wrote a series of programs used by our clients to sell insurance. We had to tailor each program to the specific client’s insurance rates, quote-report format, and so on. But many parts of the programs were similar: the classes that input information about potential customers, that stored information in a customer database, that looked up rates, that computed total rates for a group, and so on. The team factored the program so that each part that varied from client to client was in its own class. The initial programming might have taken three months or so, but when we got a new client, we merely wrote a handful of new classes for the new client and dropped them into the rest of the code. A few days’ work, and voila! Custom software!
  • Package related operations;
  • Accomplish a specific refactoring.

Classes to avoid:

  • God classes;
  • Eliminate irrelevant classes:
    • If a class consists only of data but no behavior, ask yourself whether it’s really a class and consider demoting it to become an attribute of another class.
  • Avoid classes named after verbs:
    • A class that has only behavior but no data is generally not really a class. Consider turning a class like DatabaseInitialization() or StringBuilder() into a routine on some other class.

Read checklist and Key Points at the end of the chapter

Advertisements

Chapter 5: Design in Construction

  • 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

Design techniques:

  • 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.

Minimal Complexity

  • 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.

Minimal Connectedness

  • 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.

Extensibility

  • Make sure you can change a piece of the system without affecting the others.

Reusability

  • Design the system so that you can reuse parts of it at a later point in time;.

High fan-in

  • 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.

Low-to-medium fan-out

  • 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).

Portability

  • Designing a system which can be easily moved to another environment.

Leanness

  • 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?

Stratification

  • 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.

Standard Techniques

  • 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;

Coupling Criteria

  • Size
    • 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;
  • Visibility
    • 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.
  • Flexibility
    • 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.
  • 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.

Other Heuristics:

  • 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

Iterate

  • 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

Top-Down

  • 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.

Bottom-Up

  • 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.

Summary

  • 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.

Experimental Prototyping

  • 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.

Collaborative Design

  • 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.

Chapter 4: Key Constructing Decisions

Choice of Programming

  • the programming language affects the productivity and code quality in several ways;
  • programmers working in a language they’ve used for three years or more are about 30% more productive than programmers with equivalent experience who are new to a language;
  • programmers with extensive experience with a programming language were more than three times as productive as those with minimal experience;
  • programmers working with high level languages achieve better productivity and quality than those working with lower level languages. Languages such as C++, Java, Smalltalk and Visual Basic have been credited with improving productivity, reliability, simplicity and comprehensibility by factors 5 to 15 over low lever languages such as assembly and C;
  • higher level languages are more expressive than lower level languages. Each line of code says more;
  • some languages are better at expressing programming concepts than others. The words available in a programming language for expressing your programming thoughts certainly determine how you express your thoughts and might even determine what thoughts you can express;
  • Check table 4-2. The Best and Worst Languages for Particular Kinds of Programs.

Programming Conventions

  • in high-quality software, you can see a relationship between the conceptual integrity of the architecture and its low-level implementation. The implementation must be consistent with the architecture that guides it and consistent internally. That’s the point of construction guidelines for variable names, class names, routine names, formatting conventions, and commenting conventions;
  • any large program requires a controlling structure that unifies its programming-language details. Part of the beauty of a large structure is the way in which its detailed parts bear out the implications of its architecture. Without a unifying discipline, your creation will be a jumble of poorly coordinated classes and sloppy variations in style;
  • before construction begins, spell out the programming conventions you’ll use. They’re at such a low level of detail that they’re nearly impossible to retrofit into software after it’s written.

Your Location on the Technology Wave

  • technology cycles, or waves, imply different programming practices depending on where you find yourself on the wave;
  • in mature technology environments – the end of the wave, you will benefit from a rich software development infrastructure. Advantages: multiple programming language choices, comprehensive error checking, powerful debugging tools, automatic performance optimization, almost bug free components, clear documentation, various books on subject etc;
  • in early wave environments, the situation is the opposite: few programming language choices, buggy components, poor documentation, you are most likely to be the first one to encounter an issue etc;
  • when working in a primitive environment try not to let your poor programming tools determine how to think about programming. Programmers who program “in” a language limit their thoughts to constructs that the language directly supports (if language tools are primitive, the programmers thoughts will be also primitive). Programmers who program “into” a language, first decide what thoughts they want to express and then they determine how to express those thoughts using the tools provided by their specific language;
  • if your language lacks constructs that you want to use or is prone to other kinds of problems, try to compensate for them. Invent your own coding conventions, standards, class libraries, and other augmentations.

Selection of Major Construction Practices

  • Part of preparing for construction is deciding which of the many available good practices you’ll emphasize. Some projects use pair programming and test-first development, while others use solo development and formal inspections. Either technique can work well depending on specific circumstances of the project;
  • See checklist;

Key Points:

  • every programming language has strengths and weaknesses. Be aware of the specific strengths and weaknesses of the language you’re using;
  • establish programming conventions before you begin programming. It’s nearly impossible to change code to match them later;
  • more construction practices exist than you can use on any single project. Consciously choose the practices that are best suited to your project;
  • your position on the technology wave determines what approaches will be effective—or even possible. Identify where you are on the technology wave and adjust your plans and expectations accordingly.

Chapter 3: Measure Twice, Cut Once: Upstream Prerequisites

  • A focus on prerequisites can reduce costs regardless of whether you use an iterative or sequential approach;
  • Iterative approaches are usually a better option for many reasons, but an iterative approach that ignores prerequisites can end up costing significantly more than a sequential project that pays close attention to prerequisites;
  • One realistic approach is to plan to specify about 80 percent of the requirements up front, allocate time for additional requirements to be specified later, and then practice systematic change control to accept only the most valuable new requirements as the project progresses;
  • Another alternative is to specify only the most important 20 percent of the requirements up front and plan to develop the rest of the software in small increments, specifying additional requirements and designs as you go;
  • You might choose a more sequential (up-front) approach when:
    • The requirements are fairly stable;
    • The design is straightforward and fairly well understood;
    • The development team is familiar with the applications area;
    • The project contains little risk;
    • Long-term predictability is important;
    • The cost of changing requirements, design, and code downstream is likely to be high.
  • You might choose a more iterative (as-you-go) approach when:
    • The requirements are not well understood or you expect them to be unstable for other reasons;
    • The design is complex, challenging, or both;
    • The development team is unfamiliar with the applications area;
    • The project contains a lot of risk;
    • Long-term predictability is not important;
    • The cost of changing requirements, design, and code downstream is likely to be low.

3.3 Problem Definition Prerequisite

  • first prerequisite before beginning construction is a clear statement of the problem that the system is supposed to solve. This is sometimes called “product vision”, “mission statement”, and “product definition”. But it really is a  “problem definition”;
  • a problem definition defines what the problem is without any reference to possible solutions. It’s a simple statement, maybe one or two pages, and it should sound like a problem. The statement “We can’t keep up with orders for the X” sounds like a problem and is a good problem definition. The statement “We need to optimize our automated data-entry system to keep up with orders for the X” is a poor problem definition. It doesn’t sound like a problem; it sounds like a solution;
  • a problem definition comes before detailed requirements work, which is a more in depth investigation of the problem;
  • the programming process should be composed on the following layers:
    • problem definition;
    • requirements;
    • architecture;
    • construction;
    • system testing;
    • future improvements.
  • the problem definition should be in user language and the problem should be described from a user’s point of view. Avoid stating it in technical computer terms;
  • the exception to this rule applies when the problem is with the computer: compile times are too slow or the programming tools are buggy. In this case, it’s appropriate to state the problem in computer/programmer terms;
  • without a good problem definition you might put effort into solving the wrong problem. Be sure you know what you’re aiming for;
  • the penalty for failing to define the problem is that you can waste a lot of time solving the wrong problem. This is a double barreled penalty because you also don’t solve the right problem.

3.4 Requirements Prerequisite

  • requirements describe in detail what a software system is supposed to do;
  • requirements are the first step towards a solution;
  • requirements are also known as: requirements development, requirements analysis, analysis, requirements definition, software requirements, specification, functional spec, spec;

Why have official requirements?

  • ensure that the user rather than the programmer drives the system’s functionality. If requirements are explicit, the user can review and agree to them, otherwise the programmer usually ends up making requirements decisions during programming;
  • explicit requirements keep you from guessing what the user wants;
  • explicit requirements help avoid arguments between programmers. When disagreeing with another programmer about what is the program supposed to do, check the requirements;
  • paying attention to requirements helps to minimize changes to a system after development begins. If you find a coding error during coding, you change a few lines of code and work goes on. If you find a requirements error during coding you have to alter the design to meet the changed requirement. Part of the old design needs to be thrown so design will take longer. Some code might get discarded and with it a few test cases. New code and test cases must be added. Unaffected code will need to be retested to ensure everything works;
  • without good requirements, you can have the right general problem but miss the mark on specific aspects of the problem;
  • specifying requirements adequately is a key to project success, perhaps even more important than effective construction techniques;

The Myth of Stable Requirements

  • stable requirements are the holy grail of software development, but unfortunately it does not exist;
  • on a typical project, the customer can’t reliably describe what is needed before the code is written. The more you work with a project, the better you understand it so the more the customer works with it the better he understands his needs and this is a major source of requirements changes;
  • usually a project experiences about 25% change in requirements during development. This accounts for 70-85% of the rework on a typical project;
  • requirements WILL change. The best thing to do is to minimize the impact of requirement changes;

Handling Requirements Changes During Construction

  • if requirements aren’t good enough, stop and make them right before you proceed;
  • make sure everybody knows the cost of requirement changes. When clients get excited about a feature they tend to get over all meetings / discussions / requirements. In that case, a good way to handle the situation would be to point out that it’s not in the requirements document and you will schedule a meeting and cost estimate to decide whether they want it now or later. The schedule and cost keywords will usually transform most must haves into nice to haves;
  • if the organization is not sensitive to the importance of doing requirements first, point out that the changes at requirements time are much cheaper than changes later;
  • when the client’s excitement persists, consider establishing a formal change-control board to review such proposed changes. This way you can adapt to their needs in a controlled way;
  • use evolutionary prototyping approach to explore a system’s requirements before you send your forces in to build it. Build a little, get a little feedback, adjust the design, make a few changes, build a little more etc. The key is using short development cycles so that you can respond to your users quickly;
  • dump the project if the requirements are especially bad or volatile and none of the above suggestions work. If you can’t drop it, think about how it would be like to cancel it. Think how much worse it would have to get for you to dump it and check the difference between the two cases;
  • read requirements checklist.

Architecture Prerequisite

  • software architecture is the high-level part of software design, the frame that holds the more detailed parts of the design;
  • architecture is also knows as: system architecture, high-level design, top-level design;
  • usually the architecture is described in a single document referred to as the architecture specification or top-level design;
  • some people make a distinction between architecture an high-level design – architecture refers to design constraints that apply system-wide, whereas high-level design refers to design constraints that apply at the subsystem or multiple-class level, but not necessarily system wide;
  • architecture should be a prerequisite because the quality of the architecture determines the conceptual integrity of the system. That in turn determines the ultimate quality of the system;
  • a well thought-out architecture provides the structure needed to maintain a system’s conceptual integrity from the top levels down the bottom. It provides guidance to programmers—at a level of detail appropriate to the skills of the programmers and to the job at hand. It partitions the work so that multiple developers or multiple development teams can work independently;
  • good architecture makes construction easy. Bad architecture makes construction almost impossible;
  • without good software architecture, you may have the right problem but the wrong solution. It may be impossible to have a successful construction;
  • like requirements changes, architectural changes are expensive to make during construction or later. So the earlier you can identify changes, the better.

Typical Architecture Components

Program Organization

  • a system architecture first needs an overview that describes the system in broad terms. Without this overview, you’ll have a hard time building a coherent picture from a thousand details or even a dozen individual classes;
  • architecture should provide evidence that alternatives to the final organization were considered and find the reasons the organization used was chosen over the alternatives. By describing organizational alternatives, the architecture provides the rationale for system organization and shows that each class has been carefully considered;
  • design rationale is at least as important for maintenance as the design itself;
  • the architecture should define the major building blocks in a program. Depending on the size of the program, each building block might be a simple class or a subsystem of many classes;
  • every feature listed in the requirements should be covered by at least one building block;
  • if a function is claimed by two or more building blocks, their claims should cooperate, not conflict;
  • a building block should have one area of responsibility and this responsibility should be well defined;
  • each building block should know as little as possible about other building blocks and their area of responsibility;
  • the communication rules for each building block should be well defined;
    the architecture should describe which other building blocks the building block can use directly, which it can use indirectly and which it shouldn’t use at all;

Major Classes

  • the architecture should specify the major classes to be used;
  • the architecture should identify the responsibilities of each major class and how the class will interact with other classes;
  • if the system is large enough, it should describe how classes are organized;
    the architecture should describe other class designs that were considered and give reasons for preferring the organization that was chosen;
  • the architecture doesn’t need to specify every class in the system, aim for the 80/20 rule – specify the 20 percent of the classes that make up 80 percent of the system’s behavior;

Data Design

  • the architecture should describe major files and table designs to be used;
  • it should describe alternatives that were considered and justify the choices that were made. During construction, such information gives you insight into the minds of the architects. During maintenance, the same insight is an invaluable aid. Without it you’re watching a foreign movie with no subtitles;
  • data should normally be accessed directly by only one of subsystem or classes, except through access classes or routines that allow access to the data in controlled and abstract ways;
  • the architecture should specify high-level organization and contents of databases used and explain why a single database is preferable to multiple databases and vice versa. Identify possible interactions with other programs that access the same data, explain what views have been created on the data and so on;

Business Rules

  • if the architecture depends on specific business rules, it should identify them and describe the impact the rules have on the system’s design;

User Interface Design

  • sometimes the user interface is specified at requirements time. If it’s not, it should be specified in the software architecture;
  • the architecture should specify major elements of the web page formats, GUIs, command line interfaces etc;
  • careful architecture of the user interface makes the difference between a well-liked program and one that’s never used;
  • the architecture should be modularized so that a new UI can be substituted without affecting the business rules and output parts of the program. For example, the architecture should make it fairly easy to remove a group of interactive interface classes and plug in a group of command line classes. This is useful for testing at the unit or subsystem level.

Input/Output

  • architecture should specify a look-ahead, look-behind or just in time reading scheme;
  • it should also describe the level at which I/O errors are detected: at field, record, stream or file level.

Resource Management

  • the architecture should describe a plan for managing limited resources such as database connections, threads and handles;
  • the architecture should estimate the resources used for nominal and extreme cases;

Security

  • architecture should describe the approach to design-level and code-level security. If a threat model has not previously been built, it should be built at architecture time;
  • coding guidelines should be developed with security implications in mind, including approaches to handling buffers; rules for handling untrusted data (data input from users, cookies, configuration data, other external interfaces); encryption; level of detail contained in error messages; protecting secret data that’s in memory; and other issues;

Performance

  • performance goals should be specified in the requirements;
  • performance goals should include both speed and memory use;
  • the architecture should provide estimates and explain why the architects believe the goals are achievable;
  • if certain areas are at risk of failing the use of specific algorithms or data types, the architecture should say so;
  • if certain areas require the use of specific algorithms or data types to meet their performance goals, the architecture should say so;
  • the architecture can also include space and time budgets for each class or object;

Scalability

  • is the ability of a system to grow to meet future demands;
  • the architecture should describe how the system will address growth in number of users, number of servers, number of network nodes, database size, transaction volume and so on;
  • if the system is not expected to grow and scalability is not an issue, the architecture should make the assumption explicit;

Interoperability

  • if the system is expected to share data or resources with other software or hardware, the architecture should describe how that will be accomplished;

Internationalization/Localization

  • internationalization is a technical activity of preparing a program to support multiple locales;
  • internationalization is often known as I18N because the first and last characters in internationalization are I and N and because there are 18 letters in the middle of the word;
  • localization is known as L10N for the same reason and is the activity of translating a program to support a specific local language;
  • most interactive systems contain dozens or hundreds of prompts, status displays, help messages, error messages and so on;
  • the architecture should show that the typical string and character set issues have been considered including character set used, kinds of strings used, maintaining the strings without changing code and translating the strings into foreign languages with minimal impact on the code and the UI;
  • the architecture can decide to use strings in line in code where they’re needed, keep strings in a class and reference them through the class interface or store the strings in a resource file. Either way, the architecture should explain which option has been chosen and why;

Error Processing

  • some people have estimated that as much as 90% of a program’s code is written for exceptional, error-processing cases  or housekeeping, implying that only 10% is written for the nominal cases. Therefore, a strategy for handling the errors consistently should be spelled out in the architecture;
  • error handling is often treated as a coding convention level issue, if it’s treated at all. But because it has system wide implications, it is best treated at the architectural level;
  • here are some questions to consider:
    • is error processing corrective or merely detective? If corrective, the program can attempt to recover from errors. If it’s merely detective, the program can continue processing as if nothing had happened, or it can quit. In either case, it should notify the user that it detected an error;
    • Is error detection active or passive? The system can actively anticipate errors—for example, by checking user input for validity—or it can passively respond to them only when it can’t avoid them—for example, when a combination of user input produces a numeric overflow. It can clear the way or clean up the mess. Again, in either case, the choice has user-interface implications;
    • How does the program propagate errors? Once it detects an error, it can immediately discard the data that caused the error, it can treat the error as an error and enter an error-processing state, or it can wait until all processing is complete and notify the user that errors were detected (somewhere);
    • What are the conventions for handling error messages? If the architecture doesn’t specify a single, consistent strategy, the user interface will appear to be a confusing macaroni-and-dried-bean collage of different interfaces in different parts of the program. To avoid such an appearance, the architecture should establish conventions for error messages;
    • Inside the program, at what level are errors handled? You can handle them at the point of detection, pass them off to an error-handling class, or pass them up the call chain;
    • What is the level of responsibility of each class for validating its input data? Is each class responsible for validating its own data, or is there a group of classes responsible for validating the system’s data? Can classes at any level assume that the data they’re receiving is clean?
    • Do you want to use your environment’s built-in exception handling mechanism, or build your own? The fact that an environment has a particular error handling approach doesn’t mean that it’s the best approach for your requirements;

Fault Tolerance

  • architecture should also indicate the kind of fault tolerance expected. Fault tolerance is a collection of techniques that increase a system’s reliability by detecting errors, recovering from them if possible, and containing their bad effects if not;

Architectural Feasibility

  • the architecture should demonstrate that the system is technically feasible;
  • if infeasibility in any area could render the project unworkable, the architecture should indicate how those issues have been investigated—through proof-of-concept prototypes, research, or other means;
  • these risks should be resolved before full-scale construction begins;

Over Engineering

  • robustness is the ability of a system to continue to run after it detects an error;
    often an architecture specifies a more robust system than that specified by the requirements. One reason is that system composed of many parts that are minimally robust might be less robust than is required overall;
  • in software, the chain isn’t as strong as its weakest link, it’s as weak as all the weak links multiplied together;
  • the architecture should clearly indicate whether programmers should err on the side of over engineering or on the side of doing the simplest thing that works;
    many programmers over engineer their classes automatically out of a sense of professional pride;
  • by setting expectations explicitly in the architecture, you can avoid the phenomenon in which some classes are exceptionally robust and others are barely adequate;

Buy vs Build Decisions

  • the most radical solution to building software is not to build it at all—to buy it instead;
  • you can buy GUI controls, database managers, image processors, graphics and charting components, Internet communications components, security and encryption components, spreadsheet tools, text processing tools—the list is nearly endless;
  • If the architecture isn’t using off-the-shelf components, it should explain the ways in which it expects custom-built components to surpass ready-made libraries and components;

Reuse Decisions

  • if the plan calls for using pre-existing software, the architecture should explain how the reused software will be made to conform to the other architectural goals—if it will be made to conform;

Change Strategy

  • building a software product is a learning process for both the programmers and the users, the product is likely to change throughout its development. The changes can be new capabilities likely to result from planned enhancements, or they can be capabilities that didn’t make it into the first version of the system. Consequently, one of the major challenges facing a software architect is making the architecture flexible enough to accommodate likely changes;
  • the architecture should clearly describe a strategy for handling changes. The architecture should show that possible enhancements have been considered and that the enhancements most likely are also the easiest to implement;

Global Architectural Quality

  • a good architecture specification is characterized by discussions of the classes in the system, of the information that’s hidden in each class, and of the rationales for including and excluding all possible design alternatives;
  • a good architecture should fit the problem. When you look at the architecture, you should be pleased by how natural and easy the solution seems. It shouldn’t look as if the problem and the architecture have been forced together with duct tape. You might know of ways in which the architecture was changed during its development. Each change should fit in cleanly with the overall concept;
  • the architecture’s objectives should be clearly stated. A design for a system with a primary goal of modifiability will be different from one with a goal of uncompromised performance, even if both systems have the same function;
  • the architecture should describe the motivations for all major decisions. Be wary of “we’ve always done it that way” justifications which are not good enough if arguments are not brought to the table. The reason why it’s always done in a certain way might be an unexpected and unrelated one;
  • good software architecture is largely machine and language independent. By being as independent of the environment as possible, you avoid the temptation to over-architect the system or to do a job that you can do better during construction;
  • the architecture should tread the line between under-specifying and over-specifying the system. No part of the architecture should receive more attention than it deserves, or be over-designed;
  • the architecture should address all requirements without gold-plating (without containing elements that are not required);
  • the architecture should explicitly identify risky areas. It should explain why they’re risky and what steps have been taken to minimize the risk;
  • finally, you shouldn’t feel uncomfortable about any parts of the architecture. It shouldn’t contain anything just to please the boss;
  • the architecture shouldn’t contain anything that’s hard for you to understand. You’re the one who’ll implement it; if it doesn’t make sense to you, how can you implement it?
  • be sure to check your architecture against the architecture check list to make sure you haven’t forgot anything.

Amount of Time to Spend on Upstream Prerequisites

  • the amount of time to spend on problem definition, requirements, and software architecture varies according to the needs of your project. Generally, a well-run project devotes about 10 to 20 percent of its effort and about 20 to 30 percent of its schedule to requirements, architecture, and up-front planning. These figures don’t include time for detailed design that’s part of construction;
  • if requirements are unstable and you’re working on a large, formal project, you’ll probably have to work with a requirements analyst to resolve requirements problems that are identified early in construction. Allow time to consult with the requirements analyst and for the requirements analyst to revise the requirements before you’ll have a workable version of the requirements;
  • if requirements are unstable and you’re working on a small, informal project, allow time for defining the requirements well enough that their volatility will have a minimal impact on construction;
  • if the requirements are unstable on any project—formal or informal—treat requirements work as its own project. Estimate the time for the rest of the project after you’ve finished the requirements. This is a sensible approach since no one can reasonably expect you to estimate your schedule before you know what you’re building;
  • when allocating time for software architecture, use an approach similar to the one for requirements development. If the software is a kind that you haven’t worked with before, allow more time for the uncertainty of designing in a new area. Ensure that the time you need to create a good architecture won’t take away from the time you need for good work in other areas. If necessary, plan the architecture work as a separate project too;
  • check out upstream prerequisites check list;
  • check the additional resources (books) on requirements, architecture and development.

Key Points:

  • the overarching goal of preparing for construction is risk reduction. Be sure your preparation activities are reducing risks, not increasing them;
  • if you want to develop high-quality software, attention to quality must be part of the software-development process from the beginning to the end. Attention to quality at the beginning has a greater influence on product quality than attention at the end;
  • part of a programmer’s job is to educate bosses and coworkers about the software-development process, including the importance of adequate preparation before programming begins;
  • the kind of project you’re working significantly affects construction prerequisites—many projects should be highly iterative, and some should be more sequential.
    if a good problem definition hasn’t been specified, you might be solving the wrong problem during construction;
  • if a good requirements work hasn’t been done, you might have missed important details of the problem. Requirements changes cost 20 to 100 times as much in the stages following construction as they do earlier, so be sure the requirements are right before you start programming;
  • if a good architectural design hasn’t been done, you might be solving the right problem the wrong way during construction. The cost of architectural changes increases as more code is written for the wrong architecture, so be sure the architecture is right too;
  • understand what approach has been taken to the construction prerequisites on your project and choose your construction approach accordingly.

Chapter 5: Candy-Machine Interfaces

  • make it hard to ignore important details by making them explicit in the design;
  • create interface that are easy to use and understand;
  • return values must not carry error codes;
  • look for and eliminate flaws in your interfaces so others don’t follow your mistakes;
  • use single responsibility principle for methods, don’t write multi-purpose methods;
  • define explicit method arguments. Consider how others will call your method. Think about the method’s purpose and if it makes sense to call the method with an odd value. If it makes sense, the odd value should be treated differently in the method, otherwise don’t stress about it, just assert it;
  • write methods which cannot fail if they are given valid inputs;
  • examine your interfaces from the caller’s point of view;
  • make the code intelligible at the point of call by using proper arguments. Avoid using boolean and magic number arguments, this is usually a smell of the method having two purposes instead of one and usually ends up in splitting the method in two others;
  • comment code to emphasize a potential hazard if the hazard cannot be removed. Comment odd cases and what to be careful around, but again, only if the hazard cannot be removed. Comments should not be an excuse to write poor quality code;
  • the devil is in the details. Write everything as clear and obvious as possible.

Chapter 4: Step Through Your Code

  • the best place to look for newly introduced logic bugs is in the new or changed code;
  • it’s best to test new/changed code by setting a breakpoint in the new/changed code and stepping into it with the debugger. This way you can be sure the new code is actually executed (not skipped by some weird business logic for example);
  • every code path should be checked using the debugger and stepping into the code. Usually, the error handling cases have more bugs because that code path is rarely used;
  • a fast way to force an error and test the error handling cases is to step into the code with the debugger and forcing/setting the desired value to some variable to make sure the code will follow the path you desire to test. Even though it’s common sense how to do it, most people don’t (unless they hunt a bug);
  • danger zone: ||, &&, ??, ? operators. Why? Because the debugger will evaluate two conditions in a single step. Use debugger (immediate window) to analyze both conditions to check if they are as expected. Due to the short-circuit evaluation of the || operator for example, if the first condition is true, the second condition is not even evaluated. If in some cases, the code relies on the second condition to do something, and if the second condition was never tested, a bug might come up. So always check both code paths.

Chapter 3: Fortify Your Subsystems

  • look at your subsystems and ask yourself how likely are other programmers to miss-use them. Add assertions and validation checks to catch hard to spot and common bugs;
  • remove random behavior to force bugs to be reproducible;
  • destroy garbage data/objects in order for them not to be misused;
  • look for rare behaviors in the program and try to make them happen more often (even in the debug versions of the code with directives). A rare behavior might have a bug which is not easily noticed because the behavior itself is rare. This kind of bug can be very difficult to track down. If one were to make this behavior execute more often, there is a good chance that the bug will be discovered eventually;
  • all implementations should be well thought keeping in mind whether a possibility either generates or helps finding bugs;
  • nothing should be arbitrary. Analyze all options for as long as you need before taking a decision;
  • for each design you consider, ask yourself how can you thoroughly validate it. If the implementation is difficult or even impossible to test, seriously consider a different design even if that means trading speed/size for the ability to test the system;
  • the debug version is not shipped, you can do whatever tests you want in the debug version. Even if it’s slower, if there is a chance to catch a bug before the code reaches production, it’s a win-win situation for everybody;
  • if debug code is about to be tested by somebody else, warn them about the code being loaded with internal debug checks which affect performance;
  • don’t apply ship version constraints to the debug version. Trade size and speed for error detection.