如何避免编写一个糟糕的代码
在用于数据导入的批处理作业中,我们有许多类似的类用于保存要导入的数据。 从技术上讲,它们都是不同的,具有不同的领域,但从概念上讲,它们是相同的。 我发现这种概念上的重复令人不快,并编写了一个更通用的类来替换所有这些。
重构的灵感来自Clojure及其对通用结构的偏爱,例如具有许多功能的地图,而不是针对特定案例的数据结构(即类)的OO方式,例如在本次Rich Hickey访谈中解释的那样,“ OO可以严重阻碍重用”。
原始代码:
public interface LogEntry {
Date getTimestamp();
/** For dumping into a .tsv file to be imported into Hadoop */
String toTabDelimitedString();
String getSearchTerms();
boolean hasSearchTerms();
String[][] getColumns();
String getKey();
void mergeWith(LogEntry entry);
}
// Another impl. of LogEntry, to showcase the conceptual and,
// to a degree, factual duplication
public class PlayerEventLogEntry implements LogEntry {
public static final String[][] COLUMNS = { ... }
private String userId;
private String contentId;
private String sessionId;
...
public PlayerEventLogEntry(...) { /* assign all fields ... */ }
@Override
public String[][] getColumns() {
return COLUMNS;
}
...
@Override
public String toTabDelimitedString() {
StringBuilder sb=new StringBuilder();
sb.append(contentId);
sb.append(FIELD_DELIMITER);
sb.append(userId);
sb.append(FIELD_DELIMITER);
sb.append(sessionId);
sb.append(FIELD_DELIMITER);
...
return sb.toString();
}
}
// Called from a data import job to process individual log lines.
// Some time later, the job will call toTabDelimitedString on it.
public class PlayerEventsParser ... {
@Override
public LogEntry parse(String logLine) throws LogParseException {
... // tokenizing, processing etc. of the data ...
return new PlayerEventLogEntry(userId, contentId, sessionId, ...);
}
}
// One of 15 implementations of LogEntry for import of right granted event logs
public class RightGrantedLogEntry implements LogEntry {
private static final char FIELD_DELIMITER = '\t';
public static final String[][] COLUMNS = { { "messageId", "STRING" }
{ "userId", "STRING" },{ "rightId", "STRING" }, ... };
private String messageId;
private Date timestamp;
private String userId;
private String rightId;
...
public RightGrantedLogEntry(String messageId, Date timestamp, String userId, String rightId, ...) {
this.messageId=messageId;
this.timestamp=timestamp;
this.userId=userId;
this.rightId=rightId;
...
}
@Override
public String[][] getColumns() {
return RightGrantedLogEntry.COLUMNS;
}
@Override
public String getKey() {
return messageId;
}
@Override
public String getSearchTerms() {
return null;
}
@Override
public Date getTimestamp() {
return timestamp;
}
@Override
public boolean hasSearchTerms() {
return false;
}
@Override
public void mergeWith(LogEntry arg0) {}
@Override
public String toTabDelimitedString() {
StringBuilder sb=new StringBuilder();
sb.append(messageId);
sb.append(FIELD_DELIMITER);
sb.append(userId);
sb.append(FIELD_DELIMITER);
sb.append(rightId);
sb.append(FIELD_DELIMITER);
...
return sb.toString();
}
}
重构的代码,其中所有LogEntry实现都已被MapLogEntry代替:
public interface LogEntry {
// same as before
}
// The generic LogEntry implementation
// JavaDoc removed for the sake of brevity
public class MapLogEntry implements LogEntry {
private static final char FIELD_SEPARATOR = '\t';
private Map<String, String> fields = new HashMap<String, String>();
private final String[][] columns;
private final StringBuilder tabString = new StringBuilder();
private final Date timestamp;
private String key;
public MapLogEntry(Date timestamp, String[][] columns) {
this.timestamp = checkNotNull(timestamp, "timestamp");
this.columns = checkNotNull(columns, "columns");
}
@Override
public String toTabDelimitedString() {
return tabString.toString();
}
public MapLogEntry addInOrder(String column, String value) {
checkAndStoreColumnValue(column, value);
appendToTabString(value);
return this;
}
public MapLogEntry validated() throws IllegalStateException {
if (fields.size() != columns.length) {
throw new IllegalStateException("This entry doesn't contain values for all the columns " +
"expected (" + columns.length + "). Actual values (" + fields.size() + "): " + toTabDelimitedString());
}
return this;
}
private void checkAndStoreColumnValue(String column, String value) {
final int addedColumnIndex = fields.size();
checkElementIndex(addedColumnIndex, columns.length, "Cannot add more values, all " + columns.length +
" columns already provided; column being added: " + column);
String expectedColumn = columns[addedColumnIndex][0];
if (!column.equals(expectedColumn)) {
throw new IllegalArgumentException("Cannot store value for the column '" +
column + "', the column expected at the current position " + addedColumnIndex +
" is '" + expectedColumn + "'");
}
fields.put(column, value);
}
private void appendToTabString(String value) {
if (tabString.length() > 0) {
tabString.append(FIELD_SEPARATOR);
}
tabString.append(valOrNull(replaceFildSeparators(value)));
}
/** Encode value for outputting into a tab-delimited dump. */
Object valOrNull(Object columnValue) {
if (columnValue == null) {
return HiveConstants.NULL_MARKER;
}
return columnValue;
}
@Override
public Date getTimestamp() {
return timestamp;
}
@Override
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public MapLogEntry withKey(String key) {
setKey(key);
return this;
}
/**Utility method to simplify testing. */
public Map<String, String> asFieldsMap() {
return fields;
}
...
}
// Called from a data import job to process individual log lines.
// Some time later, the job will call toTabDelimitedString on it.
public class PlayerEventsParser ... {
@Override
public LogEntry parse(String logLine) throws LogParseException {
... // tokenizing, processing etc. of the data ...
return new MapLogEntry(timestamp, getColumns())
.addInOrder("userid", userId)
.addInOrder("contentid", contentId)
.addInOrder("sessionid", sessionId)
...
.validated();
}
}
改进我们用一类替换了大约15个类,使可以在单个位置(DRY)更改将数据转换为制表符分隔的字符串的方式,并提供了一种美观,流畅的API,其用法在每个位置都非常相似。 新的MapLogEntry更加易于测试(修改所有现有类以支持MLE所做的工作将是一场噩梦)。
异议有人可能认为许多原始POJO类比一个泛型类更简单。 一个泛型类肯定比原始数据结构更复杂,但是总的来说解决方案不那么复杂,因为有更少的片段,并且单个片段在所有地方都以相同的方式使用,因此产生的认知负担较小。 前者的代码更“容易理解”,而后者则更“简单”。
干燥原理
如何避免编写一个糟糕的代码