| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- basis step
- 자기호출
- 클린코드
- 3장
- Repository
- 원의 방정식
- 백준
- mirror
- 문제풀이
- Wiki
- 1011
- 프로그래머스
- dp
- 8장
- 규칙
- 재귀
- 7장
- 수학
- 2장
- N으로 표현
- 4장
- BOJ
- 기하학
- recursion
- Clean code
- 0장
- 6장
- programmers
- inductive step
- 1024
- Today
- Total
LeeA0의 공부 일기
[Clean Code] 4장. 주석 본문
[사담]
다음주면 벌써 추석이네요~! ㅎㅎㅎ
명절동안 미뤄두었던 알고 정리랑 클린코드 정리를 하고 싶네요~
평소에 알고리즘 문제를 풀고, 주석을 하나하나 남겨 놓는게 좋은 건줄 알았는 데, 이번 장을 읽으면서 생각이 바뀌었습니다..ㅎㅎ 앞으로 필요한 정보만 주석으로 남기도록해야겠네요!
[정리]
나쁜 코드에 주석을 달지 마라. 새로 짜라.
| 잘 달린 주석 | 경솔하고 근거없고 오래된 주석 |
| 그 어떤 정보보다 유용 | 코드를 이해하기 어렵게 만듬 거짓과 잘못된 정보를 퍼트림 |
- 만약, 프로그래밍 언어 자체가 표현력이 풍부하다면? 주석은 필요하지 않다.
- 주석은 코드로 의도를 표현하는 것에 실패했을 때, 이를 만회하기 위해 사용하는 것이다.
- 부정확한 주석은 아예 없는 주석보다 훨씐 더 나쁘다.
=> 진실은 오직 코드에만 존재한다!
<안 좋은 예시>
MockRequest request;
private final String HTTP_DATE_REGEXP =
"[SMTWF][a-z]{2}\\,\\s[0-9]\\s[JFMASOND][a-z]{2}\\s[0-9]{4}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\sGMT";
private Response response;
private FitNesseContext context;
private FileResponder responder;
private Locale saveLocale;
// Example: "Tue, 02 Apr 2003 22:18:49 GMT"
-> 분명 HTTP_DATE_REGEXP에 달린 주석이었겠지만, 중간에 다른 변수가 추가되면서 낙동강 오리알이 되어버렸다..!
<주석은 나쁜 코드를 보완하지 못한다>
코드에 주석을 추가하는 이유 : 코드 품질이 나쁨 -> 이를 보완하기 위해 주석을 담
=> 아니다! 코드를 정리해야한다!
=> 코드로 의도를 표현해라!
<안 좋은 예시>
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if((employee.flags & HOURLY_FLAG) && (employee.age > 65))
<좋은 예시>
if(employee.isEligibleForFullBenefits())
<좋은 주석>
1. 법적인 주석
- 각 소스 파일 첫머리에 주석으로 들어가는 저작권 정보와 소유권 정보
- 모든 조항과 조건을 열거하는 대신에 가능하다면 표준 라이선스나 외부 문서를 참조
<예시>
// Copyright (C) 2003,2004,2005 Object Mentor, Inc. All rights reserved.
// GNU General Public License 버전 2이상을 따르는 조건으로 배포한다.
2. 정보를 제공하는 주석
- 기본적인 정보를 제공
<예시>
// 테스트 중인 Responder 인스턴스를 반환한다.
protected abstract Responder responderInstance();
-> 함수 이름을 responderBeingTested로 바꾸면 주식이 필요없음
<더 나은 예시>
// kk:mm:ss EEE, MMM dd, yyy 형식이다.
Pattern timeMatcher = Pattern.compile("\\d*:\\d*:\\d* \\w*, \\w*, \\d*, \\d*");
-> 시간과 날짜를 변환하는 클래스를 만들어 코드를 옮기면 더 좋음
3. 의도를 설명하는 주석
- 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명
<안 좋은 예시>
public int compareTo(Object o){
if(o instanceof WikiPagePath){
WikiPagePath p = (WikiPagePath) o;
String compressedName = StringUtil.join(names, "");
String compressedArgumentName = StringUtil.join(p.names, "");
return compressedName.compareTo(compressedArgumentName);
}
return 1; // 오른쪽 유형이므로 정렬 순위가 더 높다.
}
<좋은 예시>
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});
String text = "'''bold text'''";
ParentWidget parent = new BoldWidget(new MockWidgetRoot(), "'''bold text'''");
AtomicBoolean failFlag = new AtomicBoolean();
failFlag.set(false);
// 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
for(int i = 0; i < 25000; i++){
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
Thread thread = new Thread(widgetBuilderThread);
thread.start();
}
assertEquals(false, failFlag.get());
}
4. 의미를 명료하게 밝히는 주석
- 인수나 반환값이 표준 라이브러리나 변경하지 못하는 코드에 속할 때 사용
<예시>
public void testCompareTo() throws Exception{
WikiPagePath a = PathParser.parse("PageA");
WikiPagePath ab = PathParser.parse("PageA.PageB");
WikiPagePath b = PathParser.parse("PageB");
WikiPagePath aa = PathParser.parse("PageA.PageA");
WikiPagePath bb = PathParser.parse("PageB.PageB");
WikiPagePath ba = PathParser.parse("PageB.PageA");
assertTrue(a.compareTo(a) == 0); // a == a
assertTrue(a.compareTo(b) != 0); // a != b
assertTrue(ab.compareTo(ab) == 0); // ab == ab
assertTrue(a.compareTo(b) == -1); // a < b
assertTrue(aa.compareTo(ab) == -1); // aa < ab
assertTrue(ba.compareTo(bb) == -1); // ba < bb
assertTrue(b.compareTo(a) == 1); // b > a
assertTrue(ab.compareTo(aa) == 1); // ab > aa
assertTrue(bb.compareTo(ba) == 1); // bb > ba
}
-> 그릇된 주석을 달을 위험이 상당히 높음
5. 결과를 경고하는 주석
- 다른 프로그래머에게 결과를 경고할 목적으로 사용
<괜찮은 예시>
// 여유 시간이 충분하지 않다면 실행하지 마십시오.
public void _testWithReallyBigFile(){
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToSend(this);
String responseString = output.toString();
assertSubString("Content-Length: 1000000000", responseString);
assertTrue(byteSent > 1000000000);
}
-> 특정 테스트 케이스를 꺼야하는 이유를 설명
-> JUnit4가 나오기 전에는 메서드 이름 앞에 _를 쓰는 것이 일반적인 관례였음
-> 요즘에는 @Ignore 속성을 이용해 이유를 서술하고, 테스트 케이스를 끔
<좋은 예시>
public static SimpleDateFormat makeStandardHttpDateFormat(){
// SimpleDateFormat은 스레드에 안전하지 못하다.
// 따라서 각 인스턴스를 독립적으로 생성해야 한다.
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyy HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df;
}
6. TODO 주석
- 앞으로 할 일을 남겨둔 주석
- 프로그래머가 필요하다 여기지만 당장 구현하기 어려운 업무를 기술
But. 시스템에 나쁜 코드를 남겨 놓는 핑계가 되서는 안 됨
<예시>
// TODO-MdM 현재 필요하지 않다.
// 체크아웃 모델을 도입하면 함수가 필요 없다.
protected VersionInfo makeVersion() throws Exception{
return null;
}
7. 중요성을 강조하는 주석
- 무언가의 중요성을 강조하기 위해 사용
<예시>
String listItemContent = match.group(3).trim();
// 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
8. 공개 API에서 Javadocs
<나쁜 주석>
1. 주절거리는 주석
- 특별한 이유없이 의무감으로 혹은 프로세스에서 하라고 하니까 마지못해 달은 주석
<예시>
public void loadProperties(){
try{
String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
FileInputStream propertiesStream = new FileInputStream(propertiesPath);
loadedProperties.load(propertiesStream);
}
catch(IOException e){
// 속성 파일이 없다면 기본 값을 모두 메모리로 읽어 들었다는 의미이다.
}
}
2. 같은 이야기를 중복하는 주석
- 헤더에 달린 주석이 같은 코드 내용을 그대로 중복하는 주석
<예시1>
// this.closed가 true일 때 반환되는 유틸리티 메서드다.
// 타임아웃에 도달하면 예외를 던진다.
public synchronized void waitForClose(final long timeoutMillis) throws Exception{
if(!closed){
wait(timeoutMillis);
if(!closed){
throw new Exception("MockResponseSender could not be closed");
}
}
}
-> 주석이 코드보다 더 많은 정보를 제공하지 못함
<예시2>
public abstract class ContainerBase
implements Container, Lifecycle, Pipeline, MBeanRegistration, Serializable{
/**
* 이 컴포넌트의 프로세서 지연값
*/
protected int backgroundProcessorDelay = -1;
/**
* 이 컴포넌트를 지원하기 위한 생명주기 이벤트
*/
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
/**
* 이 컴포넌트를 위한 컨테이너 이벤트 Listener
*/
protected ArrayList listeners = new ArrayList();
/**
* 컨테이너와 관련된 Loader 구현
*/
protected Loader loader = null;
/**
* 컨테이너와 관련된 Logger 구현
*/
protected Log logger = null;
...
}
-> 코드만 지저분하고 정신없게 만들 뿐, 기록이라는 목적에 전혀 기여하지 못함
3. 오해할 여지가 있는 주석
<예시>
// this.closed가 true일 때 반환되는 유틸리티 메서드다.
// 타임아웃에 도달하면 예외를 던진다.
public synchronized void waitForClose(final long timeoutMillis) throws Exception{
if(!closed){
wait(timeoutMillis);
if(!closed){
throw new Exception("MockResponseSender could not be closed");
}
}
}
-> this.closed가 true로 변하는 순간에 메서드가 반환되는 지, true일 때 반환되는지 애매하다.
-> true일 때 반환되거나 타임아웃이 되고 this.closed가 true가 아니면 예외를 던진다.
-> 경솔하게 이 함수를 호출하면 자기 코드가 왜 굼벵이 기어가듯 돌아가는지 모른다!!
4. 의무적으로 다는 주석
- 모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달은 것
-> 코드를 헷갈리게 만들고, 거짓말할 가능성을 높이며, 잘못된 정보를 제공할 여지만 만듬
<예시>
/**
*
* @param title CD 제목
* @param author CD 저자
* @param tracks CD 트랙 숫자
* @param durationInMinutes CD 길이(단위: 분)
*/
public void addCD(String title, String author, int tracks, int durationInMinutes){
CD cd = new CD();
cd.title = title;
cd.author = author;
cd.tracks = tracks;
cd.duration = durationInMinutes;
cdList.add(cd);
}
5. 이력을 기록하는 주석
- 모듈을 편집할 때마다 모듈 첫머리에 추가한 주석
-> 지금은 소스 코드 관리 시스템이 있어서 제거하는 편이 좋다.
<예시>
* 변경 이력 (11-Oct-2001부터)
* -----------------------------
* 11-Oct-2001 : 클래스를 다시 정리하고 새로운 패키지인 com.jrefinery.date로 옮겼다 (DG);
* 05-Nov-2001 : getDescription() 메서드를 추가했으며 NotableDate class를 제거했다 (DG);
* 12-Nov-2001 : IBD가 setDescription() 메서드를 요구한다. NotableDate 클래스를 없앴다 (DG);
* getPreviousDayOfWeek(), getFollowingDayOfWeek(), getNearestDayOfWeek()를 변경해
* 버그를 수정했다(DG);
* 05-Dec-2001 : SpreadsheetDate 클래스에 존재하는 버그를 수정했다 (DG);
* 29-May-2002 : month 상수를 독자적인 인터페이스로 옮겼다 (MonthConstants) (DG);
* 27-Aug-2002 : addMonths() 메서드에 있는 버그를 수정했다. N???levka Petr 덕분이다 (DG);
...
6. 있으나 마나 한 주석
- 너무 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석
<예시1>
/**
* 기본 생성자
*/
protected AnnualDateRule(){
}
/** 월 중 일자 */
private int dayOfMonth;
/**
* 월 중 일자를 반환한다.
*
* @return 월 중 일자
*/
public int getDayOfMonth(){
return dayOfMonth;
}
-> 개발자가 주석을 무시하는 습관에 빠짐
-> 코드를 읽으며 자동으로 주석을 건너뜀
-> 코드가 바뀌면서 거짓말 주석이 됨
<예시2>
private void startSending(){
try{
doSending();
}
catch(SocketException e){
// 정상. 누군가 요청을 멈췄다.
}
catch(Exception e){
try{
response.add(ErrorResponder.makeExceptionString(e));
response.closeAll();
}
catch(Exception e1){
// 이게 뭐야!
}
}
}
-> 첫번째 주석은 catch블록을 무시해도 괜찮은 이유를 설명하는 주석
-> 두번째 주석은 블록을 짜다 짜증이 났는지 주석을 막 달았다.
<개선한 예시>
private void startSending(){
try{
doSending();
}
catch(SocketException e){
// 정상. 누군가 요청을 멈췄다.
}
catch(Exception e){
addExceptionAndCloseResponse(e);
}
}
private void addExceptionAndCloseResponse(Exception e){
try{
response.add(ErrorResponder.makeExceptionString(e));
response.closeAll();
}
catch(Exception e1){
}
}
-> 있으나 마나한 주석을 없애고, try/catch 블록을 독자적인 함수로 만듬
7. 무서운 잡음
- 문서를 제공해야 한다는 잘못된 욕심으로 탄생하는 잡음
- 때로는 Javadocs도 잡음
=> 함수나 변수로 표현할 수있다면 주석을 달지마라
<예시1>
/** The name. */
private String name;
/** The version. */
private String version;
/** The licenceName. */
private String licenceName;
/** The version. */
private String info;
<예시2>
// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if(smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
<개선한 예시>
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if(moduleDependees.contains(ourSubSystem))
-> 주석을 변수로 변경
8. 위치를 표시하는 주석
- 소스 파일에서 특정 위치를 표시하려 사용한 주석
- 가독성만 낮추는 주석
<예시>
// Actions ////////////////////////////////
9. 닫는 괄호에 다는 주석
- 닫는 괄호에 달은 특수한 주석
- 중첩이 심하고 장황하다면 의미가 있지만, 캡슐화된 함수에는 잡음임
=> 닫는 괄호에 주석을 달지말고, 함수를 줄이려고 노력하자!
<예시>
public class wc{
public static void main(String[] args){
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line;
int lineCount = 0;
int charCount = 0;
int wordCount = 0;
try{
while((line = in.readLine()) != null){
lineCount++;
charCount += line.length();
String words[] = line.split("\\W");
wordCount += words.length;
} //while
System.out.println("wordCount = " + wordCount);
System.out.println("lineCount = " + lineCount);
System.out.println("charCount = " + charCount);
} //try
catch (IOException e) {
System.err.println("error:" + e.getMessage());
} //catch
} //main
}
10. 공로를 돌리거나 저자를 표시하는 주석
=> 소스코드 관리 시스템에 저장해라!
<예시>
/* 릭이 추가함 */
11. 주석으로 처리한 코드
<예시>
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));
-> 주석으로 처리된 코드는 '이유가 있어 남겨놓은 것', '중요한 것'이라고 생각해 지우기를 주저함
-> 쓸모 없는 코드가 쌓임
12. HTML 주석
- HTML주석은 편집기/IDE에서조차 읽기 어렵다.
<예시>
/**
* 적합성 테스트를 수행하기 위한 과업
* 이 과업은 적합성 테스트를 수행해 결과를 출력한다.
* <p/>
* <pre>
* 용법:
* <taskdef name="execute-fitnesse-tests"
* classname-"fitnesse.ant.ExecuteFitnesseTestsTask"
* classpathref="classpath" />
* 또는
* <taskdef classpathref="classpath"
* resource="tastks.properties" />
* <p/>
* <execute-fitnesse-tests
* suitepage="FitNesse.SuiteAcceptanceTests"
* fitnesseport="8082"
* resultsdir="${results.dir}"
* resultshtmlpage="fit-results.html"
* classpathref="classpath" />
* </pre>
*/
13. 전역 정보
- 코드 일부에 주석을 달면서 시스템의 전반적인 정보를 기술한 주석
=> 근처에 있는 코드만 주석을 달아라
<예시>
/**
* 적합성 테스트가 동작하는 포트: 기본값은 <b>8082</b>.
*
* @param fitnessePort
*/
public void setFitnessePort(int fitnessePort){
this.fitnessePort = fitnessePort;
}
-> 주석은 기본 포트 정보를 기술하지만 함수 자체는 포트 기본값을 전혀 통제하지 못한다.
14. 너무 많은 정보
- 흥미로운 역사나 관련 없는 정보를 장황하게 늘어놓은 주석
<예시>
/*
RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
1부: 인터넷 메시지 본체 형식
6.8절. Base64 내용 전송 인코딩(Content-Transfer-Encoding)
인코딩 과정은 입력 비트 중 24비트 그룹을 인코딩된 4글자로 구성된
...
*/
15. 모호한 관계
- 주석과 주석이 설명하는 코드의 사이가 명백하지 않은 주석
<예시>
/*
* 모든 픽셀을 담을 만큼 충분한 배열로 시작한다(여기에 필터 바이트를 더한다).
* 그리고 헤더 정보를 위해 200바이트를 더한다.
*/
this.pngBytes = new Byte[((this.width + 1) * this.height * 3) + 200];
-> 필터 바이트란 무엇일까? +1과 관련이 있을까? 아니면 *3과 관련이 있을까? 아니면 둘 다?
한 픽셀이 한 바이트 인가? 200을 추가하는 이유는? ...
-> 주석이 모호하다!
16. 함수 헤더
- 짧은 함수는 긴 설명이 필요없음
- 짧고 한 가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 좋음
17. 비공개 코드에서 Javadocs
- 시스템 내부에 속한 클래스와 함수에 생성한 Javadocs
<전체적인 예제>
<예시>
/**
* 이 클래스틑 사용자가 지정한 최대 값까지 소수를 생성한다. 사용된 알고리즘은 에라스토테네스의 체다.
* <p>
* 에라스토테네스: 기원전 276년에 리비아 키레네에서 출생, 기원전 194년에 사망
* ...
* <p>
* 알고리즘은 상당히 단순하다. 2에서 시작하는 정수 배열을 대상으로
* 2의 배수를 모두 제거한다. 다음으로 남은 정수를 찾아 이 정수의 배수를 모두 지운다.
* 최대 값의 제곱근이 될 때까지 이를 반복한다.
*
* @author Alphonse
* @version 13 Feb 2002 atp
*/
import java.util.*;
public class GeneratePrimes{
/**
* @param maxValue는 소수를 찾아낼 최대 값
*/
public static int[] generatePrimes(int maxValue){
if(maxValue >= 2){ // 유일하게 유효한 경우
// 선언
int s = maxValue + 1; // 배열 크기
boolean[] f = new boolean[s];
int i;
// 배열을 참으로 초기화
for(i = 0; i < s; i++)
f[i] = true;
// 소수가 아닌 알려진 숫자를 제거
f[0] = f[1] = false;
// 체
int j;
for(i = 2; i < Math.sqrt(s) + 1; i++){
if(f[i]){ // i가 남아 있는 숫자라면 이 숫자의 배수를 구한다.
for(j = 2 * i; j < s; j += i)
f[j] = false; // 배수는 소수가 아니다.
}
}
// 소수 개수는?
int count = 0;
for(i = 0; i < s; i++){
if(f[i])
count++; // 카운트 증가
}
int[] primes = new int[count];
// 소수를 결과 배열로 이동한다.
for(i = 0, j = 0; i < s; i++){
if(f[i]) // 소수일 경우에
primes[j++] = i;
}
return primes; // 소수를 반환한다.
}
else // maxValue < 2
return new int[0]; // 입력이 잘못되면 비어 있는 배열을 반환한다.
}
}
<개선한 예시>
/**
* 이 클래스는 사용자가 지정한 최대 값까지 소수를 구한다.
* 알고리즘은 에라스토테네스의 체다.
* 2에서 시작하는 정수 배열을 대상으로 작업한다.
* 처음으로 남아 있는 정수를 찾아 배수를 모두 제거한다.
* 배열에 더 이상 배수가 없을 때까지 반복한다.
*/
public class PrimeGenerator{
private static boolean[] crossedOut;
private static int[] result;
public static int[] generatePrimes(int maxValue){
if(maxValue < 2)
return new int[0];
else{
uncrossIntegersUpTo(maxValue);
crossOutMultiples();
putUncrossedIntegersIntoResult();
return result;
}
}
private static void uncrossIntegersUpTo(int maxValue){
crossedOut = new boolean[maxValue + 1];
for(int i = 2; i < crossedOut.length; i++)
crossedOut[i] = false;
}
private static void crossOutMultiples(){
int limit = determineIterationLimit();
for(int i = 2; i <= limit; i++)
if(notCrossed(i))
crossOutMultiplesOf(i);
}
private static int determineIterationLimit(){
// 배열에 있는 모든 배수는 배열 크기의 제곱근보다 작은 소수의 인수다.
// 따라서 이 제곱근보다 더 큰 숫자의 배수는 제거할 필요가 없다.
double iterationLimit = Math.sqrt(crossedOut.length);
return (int) iterationLimit;
}
private static void crossOutMultiplesOf(int i){
for(int multiple = 2*i; multiple < crossedOut.length; multiple += i)
crossedOut[multiple] = true;
}
private static boolean notCrossed(int i){
return crossedOut[i] == false;
}
private static void putUncrossedIntegersIntoResult(){
result = new int[numberOfUncrossedIntegers()];
for(int j = 0, i = 2; i < crossedOut.length; i++)
if(notCrossed(i))
result[j++] = i;
}
private static int numberOfUncrossedIntegers(){
int count = 0;
for(int i = 2; i < crossedOut.length; i++)
if(notCrossed(i)
count++;
return count;
}
}
|
|
![]() |
|
'Java > Clean Code' 카테고리의 다른 글
| [Clean Code] 4장 - 실습 (0) | 2021.09.19 |
|---|---|
| [Clean Code] 3장. 함수 (미완) (0) | 2021.09.11 |
| [Clean Code] 2장 - 실습 (0) | 2021.09.02 |
| [Clean Code] 2장. 의미 있는 이름 (0) | 2021.08.29 |
| [Clean Code] 1장. 깨끗한 코드 (0) | 2021.08.22 |
