- Use Intention-Revealing Names
- Avoid Disinformation
- Make Meaningful Distinction
- If names must be different, then they should also mean something different.
- Production, ProductionInfo, ProducionData are not distinctable.
- Use Pronounceable Names
- Use Searchable Names
- Avoid Encodings
- Hungarian Notation
- Membder Prefix
- Interfaces and Implementations
- Avoid Mental Mapping
- Readers shouldn’t have to mentally translate your names into other names they already know.
- Class Names: noun or noun phrase
- Method Names: verb or verb phrase
- Don't be cute
- Pick One Word per Concept
- Don't Pun
- Use Solution Domain Names
- Use computer science (CS) terms, algorithm names, pattern names, math terms, and so forth.
- Use Problem Domain Names
- When there is no “programmer-eese” for what you’re doing, use the name from the prob- lem domain.
- Add Meaningful Context
- Don't Add Gratuitous Context
- Small
- Do One Thing
- One Level of Abstraction per Function
- Reading Code from Top to Bottom: The Stepdown Rule
- Switch Statements
- Use Descriptive Names
- Function Arguments
- Common Monadic Forms
- Dyadic Functions
- Triads
- Argument Objects
- Argument Lists
- Verbs and Keywords
- Have No Side Effects
- Command Query Seperation
- Prefer Exceptions to Returning Error Codes
- Extract Try/Catch Blocks
- Error Handling Is One Thing
- Don't Repeat Yourself
- Structured Programming
Single-entry, single-exit rule
: Dijkstra said that every function, and every block within a function, should have one entry and one exit.- There should only be one return statement in a func- tion, no break or continue statements in a loop, and never, ever, any goto statements.
- Data Abstraction
- Data/Object Anti-Symmetry
Objects
hide their data behind abstractions and expose functions that operate on that data.Data structure
expose their data and have no meaningful functions.Procedural
code makes it hard to add new data structures because all the functions must change.OO
code makes it hard to add new functions because all the classes must change.
- The Law of Demeter
Law of Demeter
that says a module should not know about the innards of the objects it manipulates- Train Wrecks
- Hybrids
- Hiding Structure
- Data Transfer Objects(DTO)
- The quintessential form of a data structure is a class with public variables and no functions
- Use Exceptions Rather Than Return Codes
- Write Your Try-Catch-Finally Statement First
- Use Unchecked Exceptions
- Provide Context with Exceptions
- Define Exception Classes in Terms of a Caller's Needs
- Define the Normal Flow
Special Case Pattern
: create a class or configure an object so that it handles a special case.
try { MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); } catch(MealExpensesNotFound e) { m_total += getMealPerDiem(); }
/* ExpenseReportDAO always returns a MealExpense object. * If there are no meal expenses, it returns a MealExpense object that returns the per diem as its total. */ MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal();
- Don't Return Null
- throw an exception or return a special case object instead
- Don't Pass Null
- forbid passing null by default
If you use a boundary interface like Map, keep it inside the class, or close family of classes, where it is used. Avoid returning it from, or accepting it as an argument to, public APIs.
We manage third-party boundaries by having very few places in the code that refer to them. We may wrap them as we did with Map, or we may use an ADAPTER
to convert from our perfect interface to the provided interface. Either way our code speaks to us better, promotes internally consistent usage across the boundary, and has fewer maintenance points when the third-party code changes.
With functions we measured size by counting physical lines. With classes we use a different measure. We count responsibilities
.
The name of a class should describe what responsibilities it fulfills. In fact, naming is probably the first way of helping determine class size.
The more variables a method manipulates the more cohesive that method is to its class.
Breaking a large function into many smaller functions often gives us the opportunity to split several smaller classes out as well.
class Sql {
public:
Sql(const string& table, const Column&... column);
string create();
string insert(cosnt string&... fields);
string selectAll();
string findByKey(cosnt string& keyColumn, cosnt string& keyValue);
};
class Sql {
public:
Sql(const string& table, const Column&... column);
virtual ~Sql() = default;
virtual string generate() = 0;
};
class CreateSql : public Sql {
public:
CreateSql(const string& table, const Column&... column);
virtual string generate() override;
};
class UpdateSql : public Sql {
public:
UpdateSql(const string& table, const Column&... column);
virtual string generate() override;
};
In an ideal system, we incorporate new features by extending the system, not by making modifications to existing code.
A client class depending upon concrete details is at risk when those details change. We can introduce interfaces and abstract classes to help isolate the impact of those details.
Dependency Inversion Principle (DIP)
: classes should depend upon abstractions, not on concrete details.
/* The system has an external dependency: ToykoStoackExchange,
* use DIP and DI(Dependency Injection) to isolate the change of the external dependency */
class StockExchange {
public:
virtual Money currentPrice(cosnt string& symbol) = 0;
};
class Portfolio {
public:
Portfolio(const StockExchange& exchange) : mExchange(exchange) { }
private:
const StockExchange& mExchagne;
};
Software systems should separate the startup process, when the application objects are constructed and the dependencies are “wired” together, from the runtime logic that takes over after startup.
Service getServcie() {
if (mServcie == nullptr) {
mService = std::make_shared<ServcieImpl>(...);
}
return mService;
}
- The code has hard-coded dependencies
ServiceImpl
and its contructor paramters - Testing can be a problem
We should have a global, consistent strategy for resolving our major dependencies.
One way to separate construction from use is simply to move all aspects of construction to main, or modules called by main, and to design the rest of the system assuming that all objects have been constructed and wired up appropriately.
Sometimes we need to make the application responsible for when an object gets created.
In this case we can use the ABSTRACT FACTORY
pattern to give the application control of when to build the object, but keep the details of that construction separate from the application code.
A powerful mechanism for separating construction from use is Dependency Injection (DI)
, the application of Inversion of Control (IoC)
to dependency management.
IoC moves secondary responsibilities from an object to other objects that are dedicated to the purpose, thereby supporting the Single Responsibility Principle.
Software systems are unique compared to physical systems. Their architectures can grow incrementally, if we maintain the proper separation of concerns.
An optimal system architecture consists of modularized domains of concern, each of which is implemented with Plain Old Java (or other) Objects. The different domains are integrated together with minimally invasive Aspects or Aspect-like tools. This architecture can be test-driven.
Modularity and separation of concerns make decentralized management and decision making possible.
It is best to give responsibilities to the most qualified persons. It is also best to postpone decisions until the last possible moment.
The agility provided by a POJO system with modularized concerns allows us to make optimal, just-in-time decisions, based on the most recent knowledge. The complexity of these decisions is also reduced.
Standards make it easier to reuse ideas and components, recruit people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards lose touch with the real needs of the adopters they are intended to serve.
Domain-Specific Languages allow all levels of abstraction and all domains in the application to be expressed as POJOs, from high-level policy to low-level details.
- Runs all the tests
- Contains no duplication
- Expresses the intent of the programmer
- Minimizes the number of classes and methods
During refactoring step, we can apply anything from the entire body of knowledge about good software design. We can increase cohesion, decrease coupling, separate concerns, modularize system concerns, shrink our functions and classes, choose better names, and so on.
The TEMPLATE METHOD
pattern is a common technique for removing higher-level duplication.
class VacationPolicy {
public:
void accrueUSDivisionVacation() {
// calcualte vacation based on hours worked to data
// ensure vacation meets US minimums
// apply vacation to payroll record
}
void accrueEUDivisionVacation() {
// calcualte vacation based on hours worked to data
// ensure vacation meets EU minimums
// apply vacation to payroll record
}
};
class VacationPolicy {
public:
void accrueVacation() {
calcualteBaseVocationHours();
alterForLegalMiniumus();
applyToPayroll();
}
protected:
virtual void alterForLegalMiniumus() = 0;
private:
void calcualteBaseVacationHours();
void applyToPayroll();
};
class USVacationPolicy : public VacationPolicy {
protected:
void alterForLegalMiniumus() override {
}
};
class EUVacationPolicy : public VacationPolicy {
protected:
void alterForLegalMiniumus() override {
}
};
Express yourself by choosing good names, keeping your functions and classes small, using standard nomenclature and design pattern.
Even concepts as fundamental as elimination of duplication, code expressiveness, and the SRP can be taken too far.
Concurrency is a decoupling strategy. It helps us decouple what gets done from when it gets done.
In single-threaded applications what
and when
are so strongly coupled that the state of the entire application can often be determined by looking at the stack backtrace.
Decoupling what from when can dramatically improve both the throughput and structures of an application. From a structural point of view the application looks like many little collaborating computers rather than one big main loop. This can make the system easier to understand and offers some powerful ways to separate concerns.
- Single Responsibility Principle
- Keep your concurrency-related code separate from other code
- Corollary: Limit the Scope of Data
- Take data encapsulation to heart; severely limit the access of any data that may be shared
- Corollary: Use Copies of Data
- Corollary: Threads Should Be as Independent as Possible
- Attempt to partition data into independent subsets than can be operated on by independent threads, possibly in different processors
Concept | Definition |
---|---|
Bound Resources | Resources of a fixed size or number used in a concurrent environment. Examples include database connections and fixed-size read/ write buffers. |
Mutual Exclusion | Only one thread can access shared data or a shared resource at a time. |
Starvation | One thread or a group of threads is prohibited from proceeding for an excessively long time or forever. For example, always letting fast-running threads through first could starve out longer running threads if there is no end to the fast-running threads. |
Deadlock | Two or more threads waiting for each other to finish. Each thread has a resource that the other thread requires and neither can finish until it gets the other resource. |
Livelock | Threads in lockstep, each trying to do work but finding another “in the way.” Due to resonance, threads continue trying to make progress but are unable to for an excessively long time— or forever. |
- Producer-Consumer
- Readers-Writers
- Dining Philosophers
Think about shut-down early and get it working early. It’s going to take longer than you expect. Review existing algorithms because this is probably harder than you think.
Write tests that have the potential to expose problems and then run them frequently, with different programatic configurations and system configurations and load. If tests ever fail, track down the failure. Don’t ignore a failure just because the tests pass on a subsequent run.
- Treat spurious failures as candidate threading issues.
- Get your nonthreaded code working first.
- Do not try to chase down nonthreading bugs and threading bugs at the same time. Make sure your code works outside of threads.
- Make your threaded code pluggable.
- Make your threaded code tunable.
- Run with more threads than processors.
- Run on different platforms.
- Instrument your code to try and force failures.
- wait(), sleep(), yield(), and priority()
- Inappropriate Information
- Obsolete Comment
- Redundant Comment
- Poorly Written Comment
- Commented-Out Code
- Build Requires More Than One Step
- Tests Require More Than One Step
- Too Many Arguments
- Output Arguments
- Flag Arguments
- Dead Function
-
Multiple Languages in One Source File
-
Obvious Behavior Is Unimplemented
-
Incorrect Behavior at the Boundaries
-
Overridden Safeties
-
Duplication
- Every time you see duplication in the code, it represents a missed opportunity for abstraction.
-
Code at Wrong Level of Abstraction
- creating abstract classes to hold the higher level concepts and derivatives to hold the lower level concepts
- Isolating abstractions is one of the hardest things that software developers do, and there is no quick fix when you get it wrong.
-
Base Classes Depending on Their Derivatives
- The most common reason for partitioning concepts into base and derivative classes is so that the higher level base class concepts can be independent of the lower level derivative class concepts.
-
Too Much Information
-
Dead Code
-
Vertical Separation
- Variables and function should be defined close to where they are used.
-
Inconsistency
- If you do something a certain way, do all similar things in the same way.
-
Clutter
- Variables that aren’t used, functions that are never called, comments that add no information..., all these things are clutter and should be removed.
-
Artificial Coupling
- Things that don’t depend upon each other should not be artificially coupled.
- e.g., general enums should not be contained within more specific classes
-
Feature Envy
- The methods of a class should be interested in the variables and functions of the class they belong to, and not the variables and functions of other classes.
-
Selector Arguments
- Not only is the purpose of a selector argument difficult to remember, each selector argument combines many functions into one.
- Selector arguments are just a lazy way to avoid splitting a large function into several smaller functions.
-
Obscured Intent
- We want code to be as expressive as possible. Run-on expressions, Hungarian notation, and magic numbers all obscure the author’s intent.
-
Misplaced Responsibility
- One of the most important decisions a software developer can make is where to put code.
-
Inapproprivate Static
- When static a function, make sure that there is no chance that you’ll want it to behave polymorphically.
-
Use Explanatory Variables
- One of the more powerful ways to make a program readable is to break the calculations up into intermediate values that are held in variables with meaningful names.
-
Function Names Should Say What They Do
-
Understand the Algorithm
-
Make Logical Dependencies Physical
- If one module depends upon another, that dependency should be physical, not just logical. The dependent module should not make assumptions (in other words, logical dependencies) about the module it depends upon. Rather it should explicitly ask that module for all the information it depends upon.
-
Prefer Polymorphism to If/Else or Switch/Case
-
Follow Standard Conventions
-
Replace Magic Numbers with Named Constants
-
Be Precise
-
Structure over Convention
-
Encapsulate Conditionals
-
Avoid Negative Conditionals
- Boolean logic is hard enough to understand without having to see it in the context of an if or while statement.
- Extract functions that explain the intent of the conditional.
-
Functions Should Do One Thing
-
Hidden Temporal Couplings
class MoogDiver { public: /* The order of the three functions is important. * Unfortunately, the code does not enforce this temporal coupling. * Another programmer could call reticulateSplines before saturateGradient * was called, leading to an UnsaturatedGradientException. */ void dive(String reason) { saturateGradient(); reticulateSplines(); diveForMoog(reason); } private: Gradient gradient; List<Spline> splines; };
class MoogDiver { public: /* This exposes the temporal coupling by creating a bucket brigade. */ void dive(String reason) { Gradient gradient = saturateGradient(); List<Spline> splines = reticulateSplines(gradient); diveForMoog(splines, reason); } private: Gradient gradient; List<Spline> splines; };
-
Don’t Be Arbitrary
-
Encapsulate Boundary Conditions
-
Functions Should Descend Only One Level of Abstraction
- The statements within a function should all be written at the same level of abstraction, which should be one level below the operation described by the name of the function.
-
Keep Configurable Data at High Levels
- Expose configurable data as an argument to that low-level function called from the high-level function.
- The configuration constants reside at a very high level and are easy to change.
public static void main(String[] args) throws Exception { Arguments arguments = parseCommandLine(args); } public class Arguments { public static final String DEFAULT_PATH = "."; public static final String DEFAULT_ROOT = "FitNesseRoot"; public static final int DEFAULT_PORT = 80; public static final int DEFAULT_VERSION_DAYS = 14; }
-
Avoid Transitive Navigation
a.getB().getC().doSomething(); myCollaborator.doSomething();
- Choose Descriptive Names
- Choose Names at the Appropriate Level of Abstraction
- Don’t pick names that communicate implementation; choose names the reflect the level of abstraction of the class or function you are working in.
- Use Standard Nomenclature Where Possible
- Unambiguous Names
- Use Long Names for Long Scopes
- Avoid Encodings
- Names Should Describe Side-Effects
/* createOrReturnOos is better */ public ObjectOutputStream getOos() throws IOException { if (m_oos == null) { m_oos = new ObjectOutputStream(m_socket.getOutputStream()); } return m_oos; }
- Insufficient Tests
- The tests are insufficient so long as there are conditions that have not been explored by the tests or calculations that have not been validated.
- Use a Coverage Tool!
- Don’t Skip Trivial Tests
- An Ignored Test Is a Question about an Ambiguity
- Test Boundary Conditions
- Exhaustively Test Near Bugs
- Bugs tend to congregate. When you find a bug in a function, it is wise to do an exhaustive test of that function.
- Patterns of Failure Are Revealing
- Test Coverage Patterns Can Be Revealing
- Tests Should Be Fast