"Clean Code"제3장 Function 학습 후 Code Review 실전 총결산
어젯밤에 클린코드-Function 장을 보고 배운 지식으로 코드 리뷰에서 내가 전에 쓴 작은 틀을 봤는데 순조롭게 진행되었다. 다음은 내가 새로 느낀 일, 그리고 폭로된 문제점을 정리해 보자.
원본 대조 (재구성 전후)
먼저 재구성 전후의 원본 대조를 붙인 다음에 총괄 분석하다
일단 에이프.java
재구성 전
package com.Albert.cache;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
/**
* @author Albert
* @create 2018-01-10 21:33
*/
public class App {
public static void main(String[] args) {
CacheResult cacheResult = BlockHardlyCacheResult.createNeedComputeFunction(get_A_TestComputeMethod());
final CountDownLatch firstComputesStartControl = new CountDownLatch(1);
final CountDownLatch firstComputesEndMark = new CountDownLatch(1);
final CountDownLatch secondComputesStartControl = new CountDownLatch(1);
final CountDownLatch secondComputesEndMark = new CountDownLatch(1);
final long computeFromOneUntilThisValue = 100000L;
final ExecutorService executor = Executors.newFixedThreadPool(1);
executor.execute(runManyComputeAndStartTimeIsControlled(cacheResult, firstComputesStartControl, firstComputesEndMark, computeFromOneUntilThisValue));
getFirstExecuteTime(firstComputesStartControl, firstComputesEndMark);
executor.execute(runManyComputeAndStartTimeIsControlled(cacheResult, secondComputesStartControl, secondComputesEndMark, computeFromOneUntilThisValue));
getSecondExecuteTime(secondComputesStartControl, secondComputesEndMark);
executor.shutdown();
}
private static Function get_A_TestComputeMethod() {
return a -> {
long result = 0;
for (long i = 1L; i <= a; i++) {
result += i;
}
return result;
};
}
private static void getSecondExecuteTime(CountDownLatch startGate2, CountDownLatch endGate2) {
long startTime2 = System.nanoTime();
startGate2.countDown();
try {
endGate2.await();
} catch (InterruptedException e) {
System.out.println("error........");
e.printStackTrace();
} finally {
long endTime2 = System.nanoTime();
System.out.println("This is use cache time: " + (endTime2 - startTime2) + " ns");
}
}
private static void getFirstExecuteTime(CountDownLatch startGate1, CountDownLatch endGate1) {
long startTime = System.nanoTime();
startGate1.countDown();
try {
endGate1.await();
} catch (InterruptedException e) {
System.out.println("error........");
e.printStackTrace();
} finally {
long endTime = System.nanoTime();
System.out.println("This is not use cache time: " + (endTime - startTime) + " ns");
}
}
private static Runnable runManyComputeAndStartTimeIsControlled(CacheResult cacheResult, CountDownLatch startGate2, CountDownLatch endGate2, long computeFromOneUntilThis) {
return () -> {
try {
startGate2.await();
for(long i = 1; i <= computeFromOneUntilThis; i++) {
cacheResult.compute(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate2.countDown();
}
};
}
}
재구성 후...
package com.Albert.cache;
import java.util.function.Function;
/**
* @author Albert
* @create 2018-01-10 21:33
*/
public class App {
public static void main(String[] args) {
Compute compute = EfficientCacheCompute.createNeedComputeFunction(get_A_TestComputeMethod());
System.out.println("--------start the first task of many compute--------");
long firstCostTime = getComputeTime(compute);
System.out.println("This is not use cache time: " + firstCostTime + " ns");
System.out.println("--------start the second task of many compute--------");
long secondCostTime = getComputeTime(compute);
System.out.println("This is use cache time: " + secondCostTime + " ns");
}
private static Function get_A_TestComputeMethod() {
return a -> {
long result = 0;
for (long i = 1L; i <= a; i++) {
result += i;
}
return result;
};
}
private static long getComputeTime(Compute compute) {
long startTime = System.nanoTime();
App.toComputeVeryMuch(compute);
long endTime = System.nanoTime();
return endTime - startTime;
}
private static void toComputeVeryMuch(Compute compute) {
long computeFromOneUntilThis = 100000L;
for(long i = 1; i <= computeFromOneUntilThis; i++) {
compute.compute(i);
}
}
}
App.java
재구성은 비교적 만족스럽다.먼저 App.java
의 기능은 EfficientCacheCompute
에 대한 실천적 비교로 캐시가 있는 것과 없는 상황에서의 시간 차이를 비교했다.(주의해야 할 것은 EfficientCacheCompute
는 병발 상황에서 잠금이 없는 라인 안전류이다)사상1: 재구성할 때 전체적인 각도에서 코드를 고려해야 한다
재구성할 때 처음에 나는 원래의 코드에 따라 원래의 뜻대로 재구성했다. 바로 원본 코드의 뜻에 따라 코드 생성 방법 등을 추출했다.나중에 보면 쓸수록 복잡해지고 간단한 앱 실천을 층층이 썼다
CountDownLatch
는 이종 라인에서 계산의 시작과 끝을 제어하는 데 사용되었기 때문이다).나중에 곰곰이 생각해 보니 좋은 코드는 한 번 보면 알 수 있고'멍청한'코드를 쓰는 사상을 견지해야 한다고 생각했다. 그리고 실천에서 이렇게 복잡한 병발류로 실현할 필요가 없다고 생각했다. 그러면 오히려 틀의 주된 부분을 돋보이게 하기 어렵다.그래서 비동기 계산을 취소하고 같은 주선으로 바꾸었다.이렇게 하면 코드가 더욱 이해하기 쉽고 EfficientCacheCompute
의 사용 방식을 뚜렷하게 볼 수 있다.사상2: 책에서 언급한 지식을 완전히 따르는 것이 아니라 사람들이 더 잘 이해할 수 있는 방식으로 코드를 고려해야 한다. 왜냐하면 이런 지식도 선인들이 다년간의 경험을 통해 얻은 것이다.
사상2에서 나는 재구성할 때의 두 가지 작은 점을 고려해서 얻은 것이다. 첫째,
private static Function get_A_TestComputeMethod() {
return a -> {
long result = 0;
for (long i = 1L; i <= a; i++) {
result += i;
}
return result;
};
}
이 방법명을 주의하세요. 처음에 저는
getATestComputeMethod
로 이름을 지었지만 두 대문자가 붙어 있으면 오해를 일으키기 쉽다고 생각해서 상술한 방식으로 바꿨습니다.영어 단어에는 짧은 단어가 많다. 예를 들어 Of는 이 단어들이 비교적 많은 명명에 섞이면 눈에 띄지 않기 때문에 ''를 사용할 수 있다.그것을 갈라놓다.""채택논쟁이 있는 관점이다. 아마도 어떤 사람들은 수정된 명칭이 매우 불편하다고 생각할 것이다. 나는 단지 사상2의 의미를 표현했을 뿐이다. 책의 규칙에 따라 코드를 쓰지 말고 자신의 사상을 가지고 가독성을 강화하는 것을 위주로 해야 한다.그 다음은 두 번째(코드는 아래 클래스EfficientCacheCompute
에서 나온다).private class Message {
public final Future putResult;
public final FutureTask willPut;
public Message(Future putResult, FutureTask willPut) {
this.putResult = putResult;
this.willPut = willPut;
}
}
이 종류의 이름은 처음에 나는
MessageOfPutToCacheResult
로 명명되었는데, 나중에 이렇게 사용하면 성명의 이 줄이 매우 길다는 것을 발견하였다. 아래와 같다.MessageOfPutToCacheResult messageOfPutToCacheResult;
그리고 나는 어쨌든 그것은 사유 내부 클래스이기 때문에 다른 곳에서는 틀림없이 사용되지 않을 것이다. 게다가 성명은 명칭이 그 용도를 설명해야 하기 때문에 읽기에 더욱 편하도록 클래스 이름을
Message
로 바꾼 후의 코드가 되었다.Message messageOfPutToCacheResult;
// :Message message_of_putToCacheResult; ,
변수 이름만으로도 그 뜻을 알 수 있어 코드가 더욱 간결해 보인다.
그 다음은 EfficientCacheCompute(주로 이 두 종류를 대상으로 재구성)
재구성 전...
package com.Albert.cache;
import java.util.concurrent.*;
import java.util.function.Function;
/**
* @author Albert
* @create 2018-01-10 19:44
*/
public class BlockHardlyCacheResult<ResultT, KeyT> implements CacheResult<ResultT, KeyT> {
private final boolean IS_NOT_RETURN = true;
private final ConcurrentHashMap> cacheResult;
private final Function computeMethod;
private BlockHardlyCacheResult(Function computeMethod) {
this.computeMethod = computeMethod;
this.cacheResult = new ConcurrentHashMap<>();
}
public static BlockHardlyCacheResult createNeedComputeFunction(Function computeMethod) {
return new BlockHardlyCacheResult<>(computeMethod);
}
@Override
public ResultT compute(final KeyT keyT) {
while (IS_NOT_RETURN) {
Future resultFuture = cacheResult.get(keyT);
if (isNotExitResult(resultFuture)) {
Callable putKeyComputeMethod = () -> computeMethod.apply(keyT);
FutureTask runWhenResultFutureNull = new FutureTask<>(putKeyComputeMethod);
resultFuture = cacheResult.putIfAbsent(keyT, runWhenResultFutureNull);
if (isNotExitResult(resultFuture)) {
resultFuture = runWhenResultFutureNull;
runWhenResultFutureNull.run();
}
}
try {
return resultFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
private boolean isNotExitResult(Future resultFuture) {
return resultFuture == null;
}
}
재구성 후...
package com.Albert.cache;
import java.util.concurrent.*;
import java.util.function.Function;
/**
* @author Albert
* @create 2018-01-10 19:44
*/
public class EfficientCacheCompute<ResultT, KeyT> implements Compute<ResultT, KeyT> {
private final boolean IS_NOT_RETURN = true;
private final ConcurrentHashMap> cacheResult;
private final Function computeMethod;
private EfficientCacheCompute(Function computeMethod) {
this.computeMethod = computeMethod;
this.cacheResult = new ConcurrentHashMap<>();
}
public static EfficientCacheCompute createNeedComputeFunction(Function computeMethod) {
return new EfficientCacheCompute<>(computeMethod);
}
@Override
public ResultT compute(final KeyT keyT) {
while (IS_NOT_RETURN) {
Future resultFuture = cacheResult.get(keyT);
if (isNotExitResult(resultFuture)) {
resultFuture = tryPutFutureToCacheAndRunCompute(keyT);
}
return getResultT(resultFuture);
}
}
private boolean isNotExitResult(Future resultFuture) {
return resultFuture == null;
}
private Future tryPutFutureToCacheAndRunCompute(KeyT keyT) {
Message message_of_putToCacheResult;
message_of_putToCacheResult = tryPutFutureToCache(keyT);
return getFutureFromCacheAndRunComputeIfNecessary(message_of_putToCacheResult);
}
private Message tryPutFutureToCache(KeyT keyT) {
Callable computeMethod_Of_PutTheKey = () -> computeMethod.apply(keyT);
FutureTask willPut = new FutureTask<>(computeMethod_Of_PutTheKey);
Future putResult = cacheResult.putIfAbsent(keyT, willPut);
return new Message(putResult, willPut);
}
private Future getFutureFromCacheAndRunComputeIfNecessary(Message message) {
if (isPutSuccess(message)) {
message.willPut.run();
return message.willPut;
} else {
return message.putResult;
}
}
private boolean isPutSuccess(Message message) {
return message.putResult == null;
}
private ResultT getResultT(Future resultTFuture) {
ResultT resultT = null;
try {
resultT = resultTFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return resultT;
}
private class Message {
public final Future putResult;
public final FutureTask willPut;
public Message(Future putResult, FutureTask willPut) {
this.putResult = putResult;
this.willPut = willPut;
}
}
}
EfficientCacheCompute
에 대한 재구성은 몇 가지 문제점을 드러냈다. 하나는 방법의 상하 순서에 대한 것이다. 나는 클린코드에서 코드를 아름다운 문장을 읽는 것처럼 단락이 뚜렷하고 차원감이 풍부하게 써야 한다고 언급한 것을 기억한다.방법의 정렬은 문장의 제목과 단락과 같다. 그러면 겹쳐진 방법을 포함하는 정렬은 다음과 같다.public void one() {
firstInOne();
secondInOne();
thirdInOne();
}
public void firstInOne() {}
public void secondInOne() {}
public void thirdInOne() {}
그렇다면 여러 겹으로 끼워 넣은 것이 존재하는 것에 대해 어떻게 정렬합니까?다음과 같은 두 가지 정렬 방법 중 하나를 나열했습니다.
public void one() {
firstInOne();
secondInOne();
}
public void firstInOne() {
AInFirst();
BInFirst();
}
public void AInFirst() {}
public void BInFirst() {}
public void secondInOne() {}
방법 2:
public void one() {
firstInOne();
secondInOne();
}
public void firstInOne() {
AInFirst();
BInFirst();
}
public void secondInOne() {}
public void AInFirst() {}
public void BInFirst() {}
이 두 가지 차이점은 방식은 취향과 선형 읽기이다. 한 방법을 읽을 때 바로 아래에서 순서대로 대응하는 방법을 볼 수 있다.방식 두 번째는 모든 방법에 대한 전체적인 파악성을 더욱 중시한다.하루가 넘는 무의식적인 사고를 통해 지금은 방법이 더욱 좋다고 생각한다. 왜냐하면 방법이 더욱 규칙적이고 독자에게 방법 안에 대응하는 방법이 어디에 있는지 알려주며 읽기에 더욱 차원감이 있기 때문이다.
총결산
이번 코드 리뷰를 통해 코드의 가독성을 크게 강화할 수 있다. 이렇게 많은 방법, 이름이 길고 주석도 없는 것을 보면 짜증이 나고 보고 싶지 않을 수도 있다. 그러나 클린 코드의 묘미는 네가 열심히 읽기만 하면 마치 아름다운 산문을 읽는 것처럼 이해할 수 있다. 코드의 주석은 모두 코드 안에 숨겨져 있고 전체 작품이 통속적이고 이해하기 쉬우며 얼마나 아름다운 느낌인지 알 수 있다.