|
This page last changed on Jul 18, 2005 by jflowers.
Good Design
Practices
- Test First
This is the practice of writing unit tests before writing production code.
What is a unit test?
Where can I find how to do this?
- Payback on Code Debt
As a project progresses life happens and shortcuts must be taken. The shortcuts or debts accumulate. At some point they need to be paid off. This can happen by refactoring during the project or on a great walup at the end of the project.
This is from that paperback in the Martin Fowler signature series.
- Coding by Intention
Coding by intention is the practice of pretending a piece of function you need already exists and is
in the form you want it to be in. In other words, if you find that you have to write some function,
instead of coding it in the place you need it run, put in a call to method you pretend exists. In our
example in the "Write Tests First" section, this would mean writing the method we described as:
s= getStatus();
s= formatStatus(s);
s= encryptString(s);
transmit(s);
This is much better than writing one long method that is all implementation. This gives us an easy
to read method that describes what is happening. Note that each of these methods has strong
cohesion and should be correctly coupled because no knowledge of how it is being implemented is
being taken advantage. Since each method has a well-defined interface with a clear contract, each
of these should also be relatively easy to test.
Coding by intention assists:
Testability: because a well-defined intention is easier to test.
Cohesion: because the intention should be about one thing, resulting in method cohesion
Encapsulation: because it encapsulates the implementation of the method being defined
Correct coupling: because the calling routine is written only to the interface
Readability: because functions are contained in well-named methods.
It may not be quite as clear that it also helps eliminate redundancy, but in practice it does. This is
because coding by intention results in stronger cohesion which makes it easier to identify when a
redundancy has been introduced.
Code Qualities and Practices
- Refactoring
- Up Front Design
- Don't do more than you need
Don't do more than you need is our version of XP's "Doing the simplest thing possible". We
prefer to state it this way because it isn't always clear what simple is. Don't do more than you need
springs from too many projects doing much more than needed. Common examples include:
- features that are implemented that are never used
- bells and whistles that are added because developers were "sure" they were useful
- frameworks that handle more than is ever required of them
We're not suggesting architecture isn't important or that you shouldn't think ahead. The difficulty
is that developers often think much too far ahead (or not ahead at all). Doing something you need
now is a good idea. Doing something you may need in the future is often a waste of time. What's
worse, is that it often causes the design problem to greatly increase in magnitude. Over- designed
software can be just as problematic as under- designed software.
The bottom line is, if you follow this mandate, you may improve the quality of your code because:
1. You might not actually need it (YAGNI), thereby saving time to work on things you do
need.
2. If you do need it in the future, you may be smarter at that point when you have to
implement it.
3. Not doing things you don't need to do now allows you to focus on those things you do need
to do now
4. If you do need it later, more of the system will be built that should make writing the
function easier.
Code Qualities and Practices
There is an old saying: "Fool me once, shame on you. Fool me twice, shame on me." This is a powerful attitude in software design. To keep from loading our software with Needless Complexity, we may permit ourselves to be fooled once. This means we initially write our code expecting it not to change. When a change occurs, we implement the abstractions that protect us from future changes of that kind. In short, we take the first bullet, and then we make sure we are protected from any more bullets coming from that gun.
From Robert Martin's "Agile Software Development"
- Continuous Integration
Principles
- Single Responsibility
THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A
CLASS TO CHANGE.
Why is it important to separate responsibilities into separate classes?
Because each responsibility is an axis of change. When the requirements change, that
change will be manifest through a change in responsibility amongst the classes. If a class
assumes more than one responsibility, then there will be more than one reason for it to
change.
If a class has more then one responsibility, then the responsibilities become coupled.
Changes to one responsibility may impair or inhibit the class' ability to meet the others.
This kind of coupling leads to fragile designs that break in unexpected ways when
changed.
http://www.objectmentor.com/resources/articles/srp
- Open Close
A module should be open for extension but closed for modification.
Of all the principles of object oriented design, this is the most important. It originated
from the work of Bertrand Meyer2. It means simply this: We should write our modules
so that they can be extended, without requiring them to be modified. In other
words, we want to be able to change what the modules do, without changing the
source code of the modules.
This may sound contradictory, but there are several techniques for achieving the OCP
on a large scale. All of these techniques are based upon abstraction. Indeed, abstraction
is the key to the OCP.
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
- Inversion of Dependency
Depend upon Abstractions. Do not depend upon concretions.
If the OCP states the goal of OO architecture, the DIP states the primary mechanism.
Dependency Inversion is the strategy of depending upon interfaces or abstract functions
and classes, rather than upon concrete functions and classes. This principle is the
enabling force behind component design, COM, CORBA, EJB, etc.
Procedural designs exhibit a particular kind of dependency structure. As Figure 2-17
shows, this structure starts at the top and points down towards details. High level
modules depend upon lower level modules, which depend upon yet lower level modules,
etc..
A little thought should expose this dependency structure as intrinsically weak. The
high level modules deal with the high level policies of the application. These policies
generally care little about the details that implement them. Why then, must these high
level modules directly depend upon those implementation modules?
An object oriented architecture shows a very different dependency structure, one in
which the majority of dependencies point towards abstractions. Morevoer, the modules
that contain detailed implementation are no longer depended upon, rather they
_depend themselves _upon abstractions. Thus the dependency upon them has been
inverted.
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
- Substitution
Subclasses should be substitutable for their base classes.
This principle was coined by Barbar Liskov2 in her work regarding data abstraction
and type theory. It also derives from the concept of Design by Contract (DBC) by
Bertrand Meyer3.
The concept, as stated above, is depicted in Figure 2-14. Derived classes should be
substitutable for their base classes. That is, a user of a base class should continue to
function properly if a derivative of that base class is passed to it.
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
- Interface Segregation
Many client specific interfaces are better than one general purpose interface
The ISP is another one of the enabling technologies supporting component substrates
such as COM. Without it, components and classes would be much less useful and portable.
The essence of the principle is quite simple. If you have a class that has several clients,
rather than loading the class with all the methods that the clients need, create
specific interfaces for each client and multiply inherit them into the class.
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
Object Oriented Design
- Inheritance
Used for Abstraction
Used to Encapsulate Types/Implementations
Follows the Substitution Principle
Follows the Single Responsibility Principle
- Polymorphism
- Encapsulation
http://www.netobjectives.com/emergentdesign/download/paying\_attention\_to\_qualities.pdf
Follows the Coding by Intention Practice
Values
- Strong Cohesion
http://www.netobjectives.com/emergentdesign/download/paying\_attention\_to\_qualities.pdf
- Loose coupling
http://www.netobjectives.com/emergentdesign/download/paying\_attention\_to\_qualities.pdf
- Reuse
http://www.netobjectives.com/emergentdesign/download/paying\_attention\_to\_qualities.pdf
- Easy to Maintain
http://www.netobjectives.com/emergentdesign/download/paying\_attention\_to\_qualities.pdf
- Easy to Test
http://www.netobjectives.com/emergentdesign/download/paying\_attention\_to\_qualities.pdf
- Easy to Understand Design (Low Viscosity)
http://www.netobjectives.com/emergentdesign/download/paying\_attention\_to\_qualities.pdf
Bad Design
Values
- Needless Complexity
Code that is not used, or code that does more than it needs to.
-
- Redundancy
The same or similar code blocks scattered through out the product.
- Fragility
Closely related to rigidity is fragility. Fragility is the tendency of the
software to break in many places every time it is changed. Often the breakage occurs
in areas that have no conceptual relationship with the area that was changed. Such
errors fill the hearts of managers with foreboding. Every time they authorize a fix,
they fear that the software will break in some unexpected way.
As the fragility becomes worse, the probability of breakage increases with time,
asymptotically approaching 1. Such software is impossible to maintain. Every fix
makes it worse, introducing more problems than are solved.
Such software causes managers and customers to suspect that the developers have lost
control of their software. Distrust reigns, and credibility is lost.
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
-
-
- Rigidity
Rigidity is the tendency for software to be difficult to change, even in
simple ways. Every change causes a cascade of subsequent changes in dependent
modules. What begins as a simple two day change to one module grows into a multiweek
marathon of change in module after module as the engineers chase the thread of
the change through the application.
When software behaves this way, managers fear to allow engineers to fix non-critical
problems. This reluctance derives from the fact that they don't know, with any reliability,
when the engineers will be finished. If the managers turn the engineers loose
on such problems, they may disappear for long periods of time. The software design
begins to take on some characteristics of a roach motel -- engineers check in, but they
don't check out.
When the manager's fears become so acute that they refuse to allow changes to software,
official rigidity sets in. Thus, what starts as a design deficiency, winds up being
adverse management policy.
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
-
-
- Immobility
Immobility is the inability to reuse software from other projects or
from parts of the same project. It often happens that one engineer will discover that he
needs a module that is similar to one that another engineer wrote. However, it also
often happens that the module in question has too much baggage that it depends upon.
After much work, the engineers discover that the work and risk required to separate
the desirable parts of the software from the undesirable parts are too great to tolerate.
And so the software is simply rewritten instead of reused.
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
-
-
- Viscosity
Viscosity comes in two forms: viscosity of the design, and viscosity of
the environment. When faced with a change, engineers usually find more than one
way to make the change. Some of the ways preserve the design, others do not (i.e.
they are hacks.) When the design preserving methods are harder to employ than the
hacks, then the viscosity of the design is high. It is easy to do the wrong thing, but
hard to do the right thing.
Viscosity of environment comes about when the development environment is slow
and inefficient. For example, if compile times are very long, engineers will be
tempted to make changes that don't force large recompiles, even though those
changes are not optiimal from a design point of view. If the source code control system
requires hours to check in just a few files, then engineers will be tempted to make
changes that require as few check-ins as possible, regardless of whether the design is
preserved.
These four symptoms are the tell-tale signs of poor architecture. Any application that
exhibits them is suffering from a design that is rotting from the inside out. But what
causes that rot to take place?
http://www.objectmentor.com/resources/articles/Principles\_and\_Patterns.PDF
-
-
- Opacity
Opacity is the tendency of a module to be difficult to understand. Code can be written in a clear and expressive manner, or it can be written in an opaque and convoluted manner. Code that evloves over time tends to become more and more opaque with age. A constant effort to keep the code clear and expressive is required in order to keep opacity to a minimum.
When developers first write a module, the code may seem clear to them. That is because they have immersed themselves within it, and the understand it at an intimate level. Latter, after tha intamicy has worn off, they may return to the module and wonder how the could have written anything so awful. To prevent this, developers need to put themselves in thier readers shoes and make a concerted effort to refactor thier code so that thier readers can understand it. They also need to have code reviews by others.
This is a quote from "Agile Software Development" Robert Martin.
-
- Practices
- Programing for possibilities
This is when you antisipate need for some functionality or capability in the future. You include code to support these antisipations in the code for the known needs. In the future these antispation may or may not manifest. When they do not there is unnessicary complexity to understand and maintain. When they do mainifest, they rarly do so in the way you expected.
- Test Last or Not at All
Testing last is difficult. It is generally imposilble to write true unit tests. Integration tests are the most granular tests that can be written inmost cases. Even integration tests in environment are difficult to setup. Because they are so difficult to create and maintain excusses are made to not create them at all. This leads to not testing at all. In this case developement has no way to know the health of the code they deliver to the test team.
- Never Addressing Impact of Shortcuts
As the code debt builds it is never payed of. This will increase the viscosity of the design. It will become increasingly difficult to event find what the design is. At that point maintenance is extreemly difficult. Making changes to the code have unknowable affects.
- No Formal Design
Just start coding.
- Coding by Purpose
Starts writting code after thinking about what the code needs to acomplish. The focus is on what the code does not on the interface, how to use it. This will cause the code to be difficult to test, and clents will have difficulty in understanding how to use it. The possibility of reuse is reduced as well, who wants to reuse something that has so many difficulties.
- Object Oriented Design
- Inheritance
Used for Specialization
I have a concrete class that is used by at least one client. It does not offer a capability that I need. I need some of if not all of the functionality that it does offer. I will subclass it adding the responsibility, creating a specialization. There are many problems with this. One the concrete class was not designed to be inherited from, that is why it is conrete. We have added a resposibility breaking the Single Responsibility Principle.
There is a page in Design Patterns Expalined second edition that talks about how and why to use inheritance.
-
-
- Encapsulation
Hides information needed to understand how to use it.
An example of this is an Enum the represents many descret sets of information. The name of the Enum can not convey all the sets of infromation it is resposible for.
Responsibility
- Indicators Of
- Inheritance
Misuse of
Adding new responsibilities
Subclassing to add new responsibilities is a misuse of inheritance. Many times the solution to this is to implement the new responsility in a new class or even be hind a new abstraction.
Client casting so as to treat this subclass differently
This is a sign that the substitution principle has been broken. The clent needs to check what the actual type is so that it can be treated differently.
Need for
- Polymorphism
Misuse of
Need for
Type checking in a case statement
This can be a sign that polymorphism should be used. The resposiblity for what needs to happen needs to shift from the class implementing the case statement to polymorphism.
- Encapsulation
Misuse of
Need for
|