Classes
Robert C. Martin
Book Notes
Best Practices
Extracted from the book Clean Code page 136
@see Meaningful Names
Following the standard Java convention, a class should begin with a list of variables. Public static constants, if any, should come first. Then private static variables, followed by private instance variables. There is seldom a good reason to have a public variable.
Public functions should follow the list of variables. We like to put the private utilities called by a public function right after the public function itself. This follows the stepdown rule and helps the program read like a newspaper article.
Cohesion
Extracted from the book Clean Code pages 140 -141
Classes should have a small number of instance variables. Each of the methods of a class should manipulate one or more of those variables. In general the more variables a method manipulates the more cohesive that method is to its class. A class in which each variables is used by each method is maximally cohesive.
In general it is neither advisable not possible to create such maximally cohesive classes; on the other hand, we would like cohesion to be high. When cohesion is high, it means that the methods and variables of the class are co-dependent and hang together as a logical whole.
The strategy of keeping functions small and keeping parameters lists short can sometimes lead toa proliferation of instance variables that are used by a subset of methods. When this happens, it almost always means that there is at least one other class trying to get out of the larger class.
Consider a large function with many variables declared within it. Let’s say you want to extract one small part of that function into a separate function. However, the code you want to extract uses four of the variables declared in the function. Must you pass all four of those variables into the new function as arguments?
Not at all! If we promote those four variables to instance variables of the class, then we could extract the code without passing any variables at all. It would be easy to break the function up small pieces.
Unfortunately, this also means that our classes lose cohesion because they accumulate more and more instance variables that exist solely to allow a few functions to share them. But wait! If there are a few functions that want to share certain variables, doesn’t that make them a class in their own right? Of course it does. When classes lose cohesion, split them!
So breaking a large function into many smaller functions often gives us the opportunity to split several smaller classes as well. This gives our program a much better organization and more transparent structure.
Organizing for Change
Extracted from the book Clean Code pages 147 - 149
For most systems change is continual. Every change subjects us to the risk that the remainder of the system no longer works as intended. In a clean system we organize our classes so as to reduce the risk of change.
The following Sql
class is used to generate properly formatted SQL strings given appropriate metadata. It’s a work in progress and, as such, doesn’t yet support SQL functionality like update
statements. When the time comes for the Sql
class to support an update statement, we’ll have to “open up” this class to make modifications. The problem with opening a class is that it introduces risk. Any modifications to the class have the potential of breaking other code in the class. It must be fully retested.
public class Sql {
public Sql(String table, Column[] columns);
public String create();
public String insert(Object[] fields);
public String selectAll();
public String findByKey(String keyColumn, String keyValue);
public String select(Column column, String pattern);
public String select(Criteria criteria);
public String preparedInsert();
private String columnList(Column[] columns);
private String valuesList(Object[] fields, final Column[] columns);
private String selectWithCriteria(String criteria);
private String placeholderList(Column[] columns);
}
What if we consider a solution like the following? Each public interface method defined in the previous Sql
class is refactored out to its own derivative of the Sql
class. Note that the private methods, such as valuesList
, move directly where they are needed. The common private behavior is isolated to a pair of utility classes Where
and ColumnList
.
abstract public class Sql {
public Sql(String table, Column[] columns)
abstract public String generate()
}
public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns)
@Override public String generate()
}
public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns)
@Override public String generate()
}
public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields)
@Override public String generate()
private String valuesList(Object[] fields, final Column[] columns)
}
public class SelectWithCriteriaSql extends Sql {
public SelectWithCriteriaSql(String table, Column[] columns, Criteria criteria)
@Override public String generate()
}
public class SelectWithMatchSql extends Sql {
public SelectWithMatchSql(
String table, Column[] columns, Column column, String pattern)
@Override public String generate()
}
public class FindByKeySql extends Sql {
public FindByKeySql(
String table, Column[] columns, String keyColumn, String keyValue)
@Override public String generate()
}
public class PreparedInsertSql extends Sql {
public PreparedInsertSql(String table, Column[] columns)
@Override public String generate()
private String placeholderList(Column[] columns)
}
public class Where {
public Where(String criteria)
public String generate()
}
public class ColumnList {
public ColumnList(Column[] columns)
public String generate()
}
The code is each class becomes excruciatingly simple. Our required comprehension time to understand any class decreases to almost nothing. The risk that one function could break another becomes vanishingly small. From a test standpoint, it becomes an easier task to prove all bits of logic in this solution, as the classes are all isolated from one another.
Equally important, when it’s time to add the update
statements, none of the existing classes need change! We code the logic to build update
statements in a new subclass of Sql
named UpdateSql
. No other code in the system will break because of this change.
Isolating for Change
Extracted from the book Clean Code pages 149 - 150
Needs will change, therefore code will change. We learned in OO 101 that there are concrete classes, which contain implementation details (code), and abstract classes, which represent concepts only. 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.
Dependencies upon concrete details create challenges for testing our system. If we are building a Portfolio
class and it depends upon an external TokyoStockExchange
API to derive the portfolio’s value, our test cases are impacted by the volatility of such a lookup. It’s hard to write a test when we get a different answer every five minutes!
Instead of designing Portfolio
so that it directly depends upon TokyoStockExchange
we create an interface, StockExchange
, that declares a single method. We design TokyoStockExchange
to implement this interface. We also make sure that the constructor of Portfolio
takes a StockExchange
reference as argument.
Now our test can create a testable implementation of the StockExchange
interface that emulates the TokyoStockExchange
. This test implementation will fix the current value for any symbol we use in testing. If our test demonstrates purchasing five shares of Microsoft for our portfolio, we code the test implementation to always return $100 per share of Microsoft. Our test implementation of the StockExchange
interfaces reduces to a simple table lookup. We can then write a test that expects $500 for our overall portfolio value.
public interface StockExchange {
Money currentPrice(String symbol);
}
public Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
}
public class PortfolioTest {
private FixedStockExchangeStub exchange;
private Portfolio portfolio;
@Before
protected void setUp() throws Exception {
exchange = new FixedStockExchangeStub();
exchange.fix("MSFT", 100);
portfolio = new Portfolio();
}
@Test
public void givenFiveMSFTTotalShouldBe500() throws Exception {
portfolio.add(5, "MSFT");
Assert.assertEquals(500, portfolio.value());
}
}
If a system is decoupled enough to be tested this way, it will also be more flexible and promote more reuse. The lack of coupling means that the elements of our system are better isolated from each other and from change. This isolation makes it easier to understand each element of the system.