Abstraction functions & rep invariants
Objectives
- Invariants
- representation exposure
- abstraction functions
representation Invariants
- 추상화 함수 그리고 Invariants(상수)의 개념을 통해 class에 ADT를 구현하는것이 무엇을 의미하는지에 대해 공부한다.
- 이러한 수학적 개념은 소프트웨어 디자인에 있어 매우 실용적이다.
- 추상함수는 데이터 유형에 따른 equality를 정의하는 방법을 제공한다.
- rep invariant는 데이터 구조로 인해 발생되는 버그를 더 쉽게 찾을 수 있게 제공한다.
Invariants
- 좋은 ADT를 만드는것에 대한 논의에 대해서 가장 중요한 속성은 그 자체로 불변성을 보존하는 것이다.
- 불변성이란? → 프로그램의 런타임 상황에서 항상 True를 유지하는 상태
- 불변의 대상은 lifeTime 내내 같은 값을 유지해야한다.
- ADT가 고유 불변량을 보존한다는 것은 그에 대한 책임이 있다는 말입니다.
- ADT가 자신의 불변성을 보장할때 코드는 더 쉬워집니다. String 같은 불변 객체를 다룰 때에는 대게 String을 배제합니다.
Immutability
첫번째 예시
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/** * This immutable data type represents a tweet from Twitter. */ public class Tweet { public String author; public String text; public Date timestamp; /** * Make a Tweet. * @param author Twitter user who wrote the tweet. * @param text text of the tweet * @param timestamp date/time when the tweet was sent */ public Tweet(String author, String text, Date timestamp) { this.author = author; this.text = text; this.timestamp = timestamp; } }
- 어떻게 해당
Tweet
객체를 불변으로 할 수 있을까요? immutabiliy에 대한 첫 번째 위협으로 뽑자면 누군가가 해당 객체를 수정하는데 있습니다.
1 2 3 4
Tweet t = new Tweet("justinbieber", "Thanks to all those beliebers out there inspiring me every day", new Date()); t.author = "rbmllr";
- 이런 클래스 변수를 노출하는 것은 representaion exposure 의 한 예이다.
- 이런 representation exposure는 오직 invariants 뿐만 아니라 representation independence까지 위협한다.
접근 제어자를 사용한 예
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
public class Tweet { private final String author; private final String text; private final Date timestamp; public Tweet(String author, String text, Date timestamp) { this.author = author; this.text = text; this.timestamp = timestamp; } /** @return Twitter user who wrote the tweet */ public String getAuthor() { return author; } /** @return text of the tweet */ public String getText() { return text; } /** @return date/time when the tweet was sent */ public Date getTimestamp() { return timestamp; } }
public
private
같은 접근 제어자는 외부에서 접근 할 수 있는 필드를 나타낸다.- 앞전에 배웠던
final
도 마찬가지. 그러나 여전히 노출 된곳은 존재한다…
1 2 3 4 5 6
/** @return a tweet that retweets t, one hour later*/ public static Tweet retweetLater(Tweet t) { Date d = t.getTimestamp(); d.setHours(d.getHours()+1); return new Tweet("rbmllr", t.getText(), d); }
retweetLater
는 리트윗을 시도하는 코드입니다. ( 한시간 뒤에 )d
는 mutable 객체라는 것을 명심하자. → d를 수정시 t까지 수정함- 해당 코드는 객체 Tweet에 대한 불변성이 깨진 상태
이건 앞에서 배운 defensive copying으로 조절 가능
1 2 3
public Date getTimestamp() { return new Date(Date.getTime()); }
clone()
은 가변객체를 복제하는 기능을 제공한다.defensive copying으로도 여전히 문제되는 부분이 존재한다.
1 2 3 4 5 6 7 8 9 10
/** @return a list of 24 inspiring tweets, one per hour today */ public static List<Tweet> tweetEveryHourToday () { List<Tweet> list = new ArrayList<Tweet>(); Date date = new Date(); for (int i=0; i < 24; i++) { date.setHours(i); list.add(new Tweet("rbmllr", "keep it up! you can do it", date)); } return list; }
- 이 코드는 24시간 마다 새로운 Date 객체를 만들어 매 시간마다 트윗한다.
- 하지만 이 코드는 모든 Tweet이 하나의 Date 객체를 참조한다…
defensive copying을 활용해서 이를 해결
1 2 3 4 5
public Tweet(String author, String text, Date timestamp) { this.author = author; this.text = text; this.timestamp = new Date(timestamp.getTime()); }
- 일반적으로 모든 인자 타입과 리턴 타입을 주의 깊게 검사해야한다.
- 유형 중에 하나라도 mutable 한 경우 직접 그 참조를 반환하지 않도록 주의한다… 노출이 생기지 않게 주의하자
그렇다면 이렇게 코드로 하지 말고 상세한 명세로 회피 할순 없는걸까?
1 2 3 4 5 6 7 8
/** * Make a Tweet. * @param author Twitter user who wrote the tweet. * @param text text of the tweet * @param timestamp date/time when the tweet was sent. Caller must never * mutate this Date object again! */ public Tweet(String author, String text, Date timestamp) {
- 이러한 접근은 다른 대안이 없을때 주로 사용된다.
- 예를들어 가변 객체가 너무 클 경우가 있다.
- 하지만 이러한 방식은 프로그램을 추론하는데 들어가는 비용이 너무 큼을 명시하자.
- 불변성을 보장하는 것은 항상 가치가 높다는 것을 명심하자.
- 물론 처음부터 불변 유형을 고르는게 가장 합리적이다.
**Immutable Wrappers Around Mutable Data Types**
- 자바 collections는 immutable wrappers라는 걸 제공한다.
Collections.unmodifiableList()
를 사용하면 일반적인 List로 보이지만 불변 개체로 wrapping한다.- 하지만 여전히 runTime의 불변성은 보장되지만 Compile 시간에는 불변성이 보장 되지 못한다.
Rep Invariant and Abstraction Function
- 추상 데이터에 대한 기초가 되는 이론을 더 자세히 공부
- 밑의 이론은 그 자체로 추상 유형의 설계와 구현에 도움을 준다.
- 추상 유형을 생각할 때 두 개체의 관계를 고려하는건 도움이 된다.
- rep values(표현 값 공간)은 실제 개체의 값으로 구성된다.
- rep values 값의 공간은 설계된 값으로 구성된다. → 추상 값 공간 (플라토닉 엔티티)
- 그들은 정신적 객체로써 실존하진 않는다. 그러나 추상적인 타입으로써 클라이언트가 보기 원하는 방식이다.
- 예를 들어 일반적인 경계가 없는 정수의 경우는 수학정 정수 전체가 추상 값의 공간(AF)이다.
- 하지만 경계가 있는 정수 배열은 이러한 정수의 경계는 중요하지 않다.
물론 추상 유형의 생성자는 실제 경계를 생각을 해야한다.
1 2 3 4
public class CharSet { private String s; ... }
- 표현공간 R(문자열) 그리고 추상공간 A(수학적 문자 집합)이다.
- R >- A 라고 볼 수 있다.
- 몇가지 주의할 점
- 모든 추상값은 어떤 참조 값에 의해 mapped된다.
- 일분 추상 값은 둘 이상의 참조 값에 의해 mapped된다. {”abc”, “bac”} → {a, b, c}
- 모든 참조 값이 mapped된건 아니다. {”aabbcc” → x } → 중복을 허용하지 않는다면…
- 우리는 몇개의 요소와 관게를 설명한다.
참조 값을 추상값에 매핑하는 추상화 함수
$AF : R → A$
- 이 관계가 일대일 대응이라고는 볼수 없지만… 종종 부분적이라고는 말할 수 있다.
참조 값을 booleans에 매핑하는 rep invariant
$RI : R → boolean$
- 참조 값 r 같은 경우 r이 AF에 의해서 매핑될 때만 RI(r)가 참이다.
- 즉 주어진 참조 값이 잘 형성되었는지 여부를 알인다.
rep invariant와 추상화 함수는 코드 옆에 문서화 되어야함을 기억하자
1 2 3 4 5 6 7 8
public class CharSet { private String s; // Rep invariant: // s contains no repeated characters // Abstraction Function: // represents the set of characters found in s ... }
- 추상 함수와 rep invariants의 일반적인 혼란은 그들이 참조와 추상 값의 공간에 대해서 결정된다는 것이다.
- 추상 값의 공간 만으로는 AF와 RI를 결정할 순 없다.
- 동일한 추상 타입에 대해 여러 표현 기능이 가능할 수 있다. 예를 들어 문자 집합은 문자열 또는 bit vector와 동일하게 표현될 수도 있다.
- 두 공간의 AF와 RI를 결정하지 않은 이유는 덜 명확할 수 있다.
- 표현 값 공간에 대한 유형을 정의하고 그것을 선택한다고 해서 그중 어떤 표현 값이 유요하다고 판단 될것이고 유효한 값중에는 어떻게 해석할지가 결정되지 않는다는 것이다.
위 처럼 문자열에 중복이 없다고 결정하는 대신 중복을 허용하지만 비 내림차순으로 나타난다고 요구하는것도 가능하다.
1 2 3 4 5 6 7 8
public class CharSet { private String s; // Rep invariant: // s[0] <= s[1] <= ... <= s[s.length()-1] // Abstraction Function: // represents the set of characters found in s ... }
- 각각의 가능성 있는 문자들은 서로 다른 추상함수로 매핑 될수 도 있다.
- 동일한 rep value를 가지더라도 다른 AF를 가진다.
- RI 가 모든 문자열을 가진다고 가정한다면 위에서처럼 AF를 정의해 배열 요소의 집합 요소로 해석 가능하다.
- $AF(s)={[s1−s1],[s2−s2],…,[sn−sn]}$
- 연속된 문자 쌍을 부분범위로 해석해 “acgg” sms {a-c}, {g-g}로 해석이 가능한 형태로 {a,b,c,g} 로 나타낼수 있다.
- $RI(s)=true for any string s$
RI 가 같더라도 다양한 추상화함수가 아래와 같이 정의 가능하다.
1 2 3 4 5 6 7 8 9 10 11
public class CharSet { private String s; // Rep invariant: // s.length is even // s[0] <= s[1] <= ... <= s[s.length()-1] // Abstraction Function: // represents the union of the ranges // {s[i]...s[i+1]} for each adjacent pair of characters // in s ... }
- 추상 타입을 설계한다는 것은 두 공간(추상 값 공간, 대표 값 공간)을 선택하는것 뿐만 아니라, 어떤 rep values를 결정하고 그거를 해석할지를 결정 한다는 것이다.
Example: Rational Numbers
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
31
32
33
34
35
36
37
38
39
40
41
public class RatNum {
private final int numer;
private final int denom;
// Rep invariant:
// denom > 0
// numer/denom is in reduced form
// Abstraction Function:
// represents the rational number numer / denom
/** Make a new Ratnum == n. */
public RatNum(int n) {
numer = n;
denom = 1;
checkRep();
}
/**
* Make a new RatNum == (n / d).
* @param n numerator
* @param d denominator
* @throws ArithmeticException if d == 0
*/
public RatNum(int n, int d) throws ArithmeticException {
// reduce ratio to lowest terms
int g = gcd(n, d);
n = n / g;
d = d / g;
// make denominator positive
if (d < 0) {
numer = -n;
denom = -d;
} else {
numer = n;
denom = d;
}
checkRep();
}
}
- RI 는 numerator/denominator가 감소된 상태, (2,4), (18,12) 같은 쌍은 RI 외부에 그린다.
- 더 많은 허용적인 RI로 동일한 ADT를 구현하는 것이 훨씬 합리적이다.
Checking the Rep Invariant
- RI 는 단순하게 깔끔한 수학적 아이디어라곤 볼 수 없다.
- RI 불변성을 초기에 주장하는 경우 버그를 조기에 발견 할 수도 있다.
RatNum의 RI를 테스트 하는 방법
1 2 3 4 5 6 7
// Check that the rep invariant is true // *** Warning: this does nothing unless you turn on assertion checking // by passing -enableassertions to Java private void checkRep() { assert denom > 0; assert gcd(numer, denom) == 1; }
- 전체적으로
checkRep()
을 호출하여 모든 생성과 변형 작업에서 RI 불변성을 주장해야한다. - 위 코드는 각각의 constructors의 끝에 checkRep()을 호출하고 있다.
- Obersver methods는 일반적으로
checkRep()
을 호출할 필요는 없지만 이렇게 하는 방식은 좀더 defensive 한 방식. - 왜? 모든 방향에서
checkRep()
을 호출하는것은 당신이 RI를 조금더 잘 잡을수 있다는 것. - 왜 checkRep이 private인가? → RI를 체크하고 RI를 확인할 책임은 누가져야하는가? clients? 구현자?
No Null Values in the Rep
- 이전 파트에서 읽었듯이 null values는 여러가지 문제를 일으킨다.
- null은 우리 공부에서 언제나 가장 제거해야하는 항목이다.
- 추상 데이터 타입에 또한 null 참조를 금지하는 것이 좋다.
1
2
3
class CharSet {
String s;
}
s
는 당연히 null이 아님- 하지만
checkRep()
을 구현 할때 여전히s != null
이라는걸 체크가 필요함 s
가null
이라면 바로 실패자바에서 기본으로 제공해주는 s를 null 인지 알려주는 예시
1 2 3 4
private void checkRep() { assert s.length() % 2 == 0; ... }
- assert가 있는데 이렇게 굳이 할 필요는 없지..
- assert
s != null
매우 명시적이다.
**Documenting the AF, RI, and Safety from Rep Exposure**
- class 안의 RI 와 AF를 문서화하은 좋은편
- 또다른 논쟁은 Rep Exposure argument safety. 이는 rep의 각 부분을 조사하고 코드에에서 rep를 다루는 부분에서(특히 파라미터와 return값)을 살펴보고 rep가 노출하지 않은 이유를 서술
Tweet
은 Invariant, abstraction function 안정성이 완전히 문서화된 예1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// Immutable type representing a tweet. public class Tweet { private final String author; private final String text; private final Date timestamp; // Rep invariant: // author is a Twitter username (a nonempty string of letters, digits, underscores) // text.length <= 140 // Abstraction Function: // represents a tweet posted by author, with content text, at time timestamp // Safety from rep exposure: // All fields are private; // author and text are Strings, so are guaranteed immutable; // timestamp is a mutable Date, so Tweet() constructor and getTimestamp() // make defensive copies to avoid sharing the rep's Date object with clients. // Operations (specs and method bodies omitted to save space) public Tweet(String author, String text, Date timestamp) { ... } public String getAuthor() { ... } public String getText() { ... } public Date getTimestamp() { ... } }
- 우리는 어떠한 명시적인 불변 조건도
timestamp
에 가지고 있지 않는다timestamp != null
그러나 여전히timestamp
의 전체 유형의 불변성 속성은 변경되지 않은 immutability - 왜냐하면 전체 내부 argument에서 이것을 변경할 것이 없다.
RatNum
의 argument1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// Immutable type representing a rational number. public class RatNum { private final int numer; private final int denom; // Rep invariant: // denom > 0 // numer/denom is in reduced form, i.e. gcd(|numer|,denom) = 1 // Abstraction Function: // represents the rational number numer / denom // Safety from rep exposure: // All fields are private, and all types in the rep are immutable. // Operations (specs and method bodies omitted to save space) public RatNum(int n) { ... } public RatNum(int n, int d) throws ArithmeticException { ... } ... }
How to Establish Invariants
Invariant는 프로그램 전체에서 항상 참인 특성임. 객체의 invariant는 객체 수명 동안 유지되어야 함. Invariant를 유지하려면 다음을 해야 함: 객체 초기 상태에서 invariant를 참으로 설정해야 함. 객체가 변경될 때마다 invariant를 참으로 유지해야 함. ADT 연산자로 보면 의미는 이렇다: creators랑 producers는 새로운 객체 인스턴스에 대해 invariant를 설정해야 함. mutators랑 observers는 invariant를 계속 유지해야 함. rep exposure는 항상 문제를 더 복잡하게 만듦. 만약 rep이 노출되면 ADT 연산뿐 아니라 프로그램 어디서든 객체가 수정될 수 있음. 이렇게 되면 invariant를 보장할 수 없게 됨.
- Structural Induction 추상 데이터 타입의 invariant가 다음과 같은 경우
- creators와 producers에 의해 설립됨
- mutators와 observers에 보존됨
- rep exposure가 노출되지 않음
**ADT invariants replace preconditions**
- 지금까지 공부한것을 합쳐 잘 설계된 추상 타입은 규정해야할 전제 조건을 캡슐화 할 수 있다는 것
예를 들어 spec이 다음과 같을때 정교한 precondition
1 2 3 4 5 6 7
/** * @param set1 is a sorted set of characters with no repeats * @param set2 is likewise * @return characters that appear in one set but not the other, * in sorted order with no repeats */ static String exclusiveOr(String set1, String set2);
이를 대체하는 ADT를 사용할 수 있다. (precondition을 대신함)
1 2
/** @return characters that appear in one set but not the other */ static SortedSet<Character> exclusiveOr(SortedSet<Character> set1, SortedSet<Character> set2);
- ADT의 이름 같은 경우 이 프로그램을 사용하는 모든 프로그래머가 사용하기에 이해가 더 쉽다.
- 또한 Static Checking이 가능하다.
Summary
- invariant는 개체의 수명동안 ADt 개체가 항상 참인 것이다.
- 좋은 ADT는 그 자체에 대한 불변성을 보존한다. → 버그로부터 안전해짐
- Rep exposure는 RI와 invariant preservation을 모두 보존한다.
- AF는 구체적인 값을 추상적인 값에 매핑해주는 함수이다.
- ADT를 활용해 이해하기 쉽고 변화할 준비가 된 코드를 작성 가능하다.