package fitnesse.wikitext.widgets;
import java.uitl.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGXP = "'''.+?'''";
private static final Patten patten = Patten.compile("'''(.+?)'''",
Patten.MULTILINE + Patten.DOTALL
);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = patten.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml().append("<b>");
return html.toString();
}
}
package fitnesse.wikitext.widgets;
import java.uitl.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGXP = "'''.+?'''";
private static final Patten patten = Patten.compile("'''(.+?)'''",
Patten.MULTILINE + Patten.DOTALL
);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = patten.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml().append("<b>");
return html.toString();
}
}
위와 아래는 동일한 코드인데, 빈 행 유무의 차이만 있다.
빈 행이 없어지면, 코드 가독성이 현저하게 떨어져 암호처럼 보인다.
public class ReporterConfig {
/**
* 리포터 리스너의 클래스 이름
*/
private String m_className;
/**
* 리포터 리스너의 속성
*/
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}
}
public class ReporterConfig
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}
}
위에 주석이 있는 코드보다, 아래 코드가 '한 눈'에 들어온다.
변수는 사용하는 위치에 최대한 가까이 선언한다.
루프를 제어하는 변수는 흔히 루프 문 내부에 선언한다.
인스턴스 변수는 클래스 맨 처음에 선언한다. 또한, 변수 간에 세로로 거리를 두지 않는다.
한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를
호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽힌다.
친화도가 높을수록 코드를 가까이 배치한다. 친화도가 높은 요인은 여러 가지다. 한 함수가 다른 함수를 호출해 생기는 직접적인 종속성이 한 예이고, 변수와 그 변수를 사용하는 함수도 한 예이다.
아래 예시는 명명법이 같고 기본 기능이 유사하고 간단하다. 종속적인 관계가 없더라도 가까이 배치해야할 함수들이다.
public class Assert {
static public void assertTrue(String message, boolean condition) {
if(!condition)
fail(message);
}
static public void assertTrue(boolean condition) {
assertTrue(null, condition);
}
static public void assertFalse(String message, boolean condition) {
assertTrue(message, !condition);
}
static public void assertFalse(boolean condition) {
assertFalse(null, condition);
}
}
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWithHistogram.addLine(lineSize, lineCount);
recordWidgetLine(lineSize);
}
할당 연산자를 강조하려고 앞뒤에 공백을 줬다. 할당문은 왼쪽 요소와 오른쪽 요소가 분명히 나뉜다. 공백을 넣으면 두 가지 주요 요소가 확실히 나뉜다는 사실이 더욱 분명해진다.
함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않았다. 함수와 인수는 서로 밀접하기 때문이다. 공백을 넣으면 한 개념이 아닌 별개로 보인다.
함수를 호출하는 인수는 쉼표를 강조해 인수가 별개라는 사실을 보여주기 위해 가로 공백을 준다.
연산자 우선 순위를 강조하기 위해서도 공백을 사용한다.
public class Quadratic {
public static double root1(double a, double b, double c) {
double determinant = determinant(a, b, c);
return (-b + Math.sqrt(determinant)) / (2*a);
}
public static double root2(int a, int b, int c) {
double determinant = determinant(a, b, c);
return (-b - Math.sqrt(determinant)) / (2*a);
}
private static double determinant(double a, double b, double c) {
return b*b - 4*a*c;
}
}
public class CommentWidget extends TextWidget
{
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r?";
public CommentWidget(ParentWidget parent, String text) {super(parent, text);}
public String render() throws Exception{ return "";}
}
public class CommentWidget extends TextWidget {
public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r?";
public CommentWidget(ParentWidget parent, String text) {
super(parent, text);
}
public String render() throws Exception{
return "";
}
}
public class CodeAnalyzer implements JavaFileAnalysis {
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;
public CodeAnalyzer() {
lineWidthHistogram = new LineWidthHistogram();
}
public static List<File> findJavaFiles(File parentDirectory) {
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}
private static void findJavaFiles(File parentDirectory, List<File> files) {
for (File file : parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}
public void analyzeFile(File javaFile) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(javaFile));
String line;
while ((line = br.readLine()) != null)
measureLine(line);
}
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
private void recordWidestLine(int lineSize) {
if (lineSize > maxLineWidth) {
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}
public int getLineCount() {
return lineCount;
}
public int getMaxLineWidth() {
return maxLineWidth;
}
public int getWidestLineNumber() {
return widestLineNumber;
}
public LineWidthHistogram getLineWidthHistogram() {
return lineWidthHistogram;
}
public double getMeanLineWidth() {
return (double)totalChars/lineCount;
}
public int getMedianLineWidth() {
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount/2)
return width;
}
throw new Error("Cannot get here");
}
private int lineCountForWidth(int width) {
return lineWidthHistogram.getLinesforWidth(width).size();
}
private Integer[] getSortedWidths() {
Set<Integer> widths = lineWidthHistogram.getWidths();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}
변수를 비공개로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶어서다. 하지만 수많은 프로그래머가 조회함수와 설정함수(getter, setter)를 당연하게 공개해 비공개 변수를 외부에 노출할까?
public class Point {
public double x;
public double y;
}
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
public interface Vehicle {
double getPercentFuelRemaining();
}
상위 인터페이스는 자동차 연료 상태를 구체적인 숫자로 알려주지만, 하위 인터페이스는 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려준다.
상위 인터페이스는 변수값을 읽어 반환할 뿐이라는 사실이 거의 확실하지만, 하위 인터페이스는 정보가 어디서 오는지 전혀 드러나지 않는다.
절차적인 도형
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
public double width;
}
public class Geometry {
public final double PI = 3.141592653585793;
public double area(Object shape) throws NoSuchShapeException {
if(shape instanceOf Square) {
Square s = (Square)shape;
return s.side * s.side;
}
else if(shape instanceOf Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
}
else if(shape instanceOf Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius
}
}
throw new NoSuchShapeException();
}
Geometry 클래스는 세 가지 도형 클래스를 다루고, 각 도형 클래스는 간단한 자료 구조이다. 즉, 자료구조인 각 도형 클래스에서는 아무 메소드를 제공하지 않고, 도형이 동작하는 방식은 Geometry라는 객체에서 구현한다.
다형적인 도형(객체지향적 도형)
public class Square implements Shape {
public Point topLeft;
public double side;
public double area() {
return side*side;
}
}
public class Rectangle implements Shape {
public Point topLeft;
public double height;
public double width;
public double area() {
return height*width;
}
}
public class Circle implements Shape {
public Point center;
public double radius;
public double width;
public double area() {
return PI*radius*radius;
}
}
area 메소드는 다형 메소드로, Geometry 클래스는 필요하지 않다. 새 도형을 추가하고 싶더라도 기존 함수에 아무런 영향을 주지 않지만, 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.
(자료 구조를 사용하는)절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다. 반면, 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
절차적인 코든느 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다. 객체 지향 코든느 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.
객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉽고, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.
클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.
클래스 C
f가 생성한 객체
f 인수로 넘어온 객체
C 인스턴스 변수에 저장된 객체
쉽게 말해, 낯선 사람은 경계하고 친구랑만 놀라는 의미다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
Options opts = ctxt.getOption();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
기차 충돌 코드는 이처럼 수정하는 것이 좋지만, getter를 사용하는 바람에 디미터 법칙을 위반한 것인지 혼란을 일으킨다.
final String outputDir = ctxt.options.scratchDir.absolutPath;
위 코드처럼 구현한다면, 디미터 법칙을 거론할 필요가 없어진다.
만약 ctxt, options, scratchDir이 진짜 객체라면, 기차 충돌 코드를 사용하면 안된다.
// 1번
ctxt.getAbsolutePathOfScratchDirectoryOption();
// 2번
ctxt.getScratchDirectoryOption().getAbsolutePath()
// 3번
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
첫 번째 방법은 ctxt 객체에 공개해야 하는 메서드가 너무 많아지고,
두 번째 방법은 getScratchDirectoryOption()이 객체가 아닌 자료 구조를 반환한다고 가정한다.
세 번째 방법은 ctxt 객체에 임시 파일을 생성하도록 하는 것으로, 객체에서 맡기기 적당한 임무로 보인다.
또한, ctxt 내부 구조를 드러내지 않으며, 모듈에서 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없기에 디미터 법칙을 위반하지 않는다.
class Address {
private final String postalCode;
private final String city;
private final String street;
private final String streetNumber;
private final String apartmentNumber;
public Address(String postalCode, String city, String street, String streetNumber, String apartmentNumber) {
this.postalCode = postalCode;
this.city = city;
this.street = street;
this.streetNumber = streetNumber;
this.apartmentNumber = apartmentNumber;
}
public String getPostalCode() {
return postalCode;
}
public String getCity() {
return city;
}
public String street() {
return street;
}
public String streetNumber() {
return streetNumber;
}
public String apartmentNumber() {
return apartmentNumber;
}
}