객체 간의 기능 이동
객체 간 이동이 가능한 상황 및 상황별 리팩토링 기법
- 기능을 넣을 적절한 위치를 찾는 경우
- 메서드 이동(Move Method)
-
필드 이동(Move Field)
- 방대해진 클래스의 정리 또는 클래스 기능이 너무 적은 경우
- 클래스 추출(Extract Class) - 기능이 너무 많아 방대해진 클래스
-
클래스 직접 삽입(Inline Class) - 리팩토링 결과로 클래스 내에 기능이 너무 작아진 경우 다른 클래스로 합침
- 다른 클래스(대리 클래스)를 이용할 경우
- 대리 객체 은폐(Hide Delegate) - 대리 클래스가 사용 중이라는 것을 외부에 감춤
-
과잉 중개 메서드 제거(Remove Middle Man) - 대리 객체 은폐시 대리 클래스를 사용 중인 클래스의 인터페이스가 변경될 때
- 클래스의 원본 코드에 접근할 수 없는 상황에서 수정 불가능한 클래스의 기능을 이동해야 할 때
- 외래 클래스에 메서드 추가(Introduce Foreign Method) - 기능을 이동할 메서드가 한두개 뿐일 때
- 국소적 상속확장 클래스 사용(Introduce Local Extension) - 기능을 이동할 메서드가 세개 이상일 때
메서드 이동 (Move Method)
메서드가 자신이 속한 클래스보다 다른 클래스의 기능을 더 많이 사용할 때
→ 그 메서드가 가장 많이 이용하는 클래스 안에서 비슷한 내용의 메서드를 작성
→ 기존 메서드는 간단한 대리 메서드로 전환하던지 아예 삭제
동기
언제 적용하는게 좋을까?
-
클래스에 기능이 너무 많을 경우
-
클래스가 다른 클래스와 과하게 연동되어 의존성이 지나칠 때
▶ 자신이 속한 객체보다 다른 객체를를 더 많이 참조하는 메서드
어떤 이점을 가져올 수 있을까?
-
클래스가 간결해짐
-
기능의 구현이 명확해짐
방법
- 원본 클래스에 정의되어 있는 원본 메서드에 사용된 모든기능을 검사하고 그 기능들도 옮겨야할지를 판단한다.
- 옮길 메서드에서만 사용되는 기능은 함께 옮겨야함
- 옮길 메서드와 함께 옮길 기능이 다른 메서드에서도 사용된다면 그 메서드도 함께 옮기는 것을 고려함
- 원본 클래스의 하위클래스와 상위클래스에서 그 메서드에 대한 다른 선언이 있는지 검사
- 다른 선언이 있다면 대상 클래스에도 재정의를 넣을 수 있을 때만 옮길 수 있을지도 모름
- 원본 메서드에 대응되는 대상 메서드를 대상 클래스에 선언
- 대상 클래스 안에 있을 때 더욱 어울리는 다른 이름으로 메서드를 정의해도 됨
- 원본 메서드의 코드를 대상 메서드에 복사한 후, 대상 클래스 안에서 잘 돌아가게끔 대상 메서드를 수정
- 대상 메서드가 원본 객체를 사용한다면 대상 메서드 안에서 원본 객체를 참조할 방법을 정한다.
- 대상 클래스에 원본 클래스를 참조
- 원본 객체를 매개변수로 전달 * 대상 메서드에 예외처리 코드가 있다면 예외를 논리적으로 어느 클래스가 처리할지 결정
-
대상 클래스를 컴파일
- 원본 객체에서 대상 객체를 참조할 방법의 결정
- 대상 클래스를 참조하는 속성이나 메서드가 있는지 확인하고 없으면 만든다.
-
원본 메서드를 위임 메서드로 전환
-
컴파일과 테스트
- 위임 메서드로 변경된 원본 메서드를 삭제하던지 위임 메서드로 계속 사용하도록 함
- 참조가 많을 때는 원본 메서드를 위임 메서드로 내버려두는 방법이 편함
- 원본 메서드를 삭제할 때는 기존의 참조를 전부 대상 메서드 참조로 수정
- 찾아바꾸기 기능을 이용해 한 번 실행으로 일괄적으로 바꿔도 됨
- 컴파일과 테스트
예제
public class Account {
private AccountType _type;
private int _daysOverdrawn;
...
double overdraftCharge() {
if (_type.isPremium()) {
double result = 10;
if (_daysOverdrawn > 1) {
result += (_daysOverdrawn - 7) * 0.85;
}
return result;
} else {
return _daysOverdrawn * 1.75;
}
}
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) {
result += overdraftCharge();
}
return result;
}
...
}
public class AccountType {
...
}
리팩토링 목표
- 계좌 유형(
AccountType
)이 추가적으로 생길 수 있는 상황에서 각 계좌 유형에 따라 다를 수 있는overdraftCharge
메서드를AccountType
클래스로 이동
public class Account {
...
// 삭제
//double overdraftCharge() {
// return _type.overdraftCharge(_daysOverdrawn);
//}
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) {
result += _type.overdraftCharge(_daysOverdrawn);
}
return result;
}
...
}
public class AccountType {
...
double overdraftCharge(int daysOverdrawn) {
if (isPremium()) {
double result = 10;
if (daysOverdrawn > 7) {
result += (daysOverdrawn - 7) * 0.85;
}
return result;
} else {
return daysOverdrawn * 1.75;
}
}
...
}
필드 이동 (Move Field)
어떤 필드가 자신이 속한 클래스보다 다른 클래스에서 더 많이 사용될 때는
→ 대상 클래스 안에 새 필드를 선언하고 그 필드 참조 부분을 전부 새 필드 참조로 수정
동기
언제 적용하는게 좋을까?
-
어떤 필드가 자신이 속한 클래스보다 다른 클래스에 있는 메서드들이 많이 참조하여 사용하는 경우
▶ 인터페이스에 따라 메서드를 옮길 수 도 있겠지만, 메서드의 위치가 올바르다고 판단되는 경우에는 필드를 옮긴다. -
클래스 추출(Extract Class) 를 실시하는 경우
▶ 필드의 이동이 메서드의 이동보다 우선한다.
방법
- 필드가
public
이면 필드 캡슐화(Encapsulate Field) 기법을 실시- 필드에 자주 접근하는 메서드를 옮기게 될 가능성이 높거나 그 필드에 많은 메서드가 접근할 때는 필드 자체 캡슐화(Self Encapsulate Field)를 실시
-
컴파일과 테스트
-
대상 클래스 안에 읽기/쓰기 메서드와 함께 필드를 작성
-
대상 클래스를 컴파일
- 원본 객체에서 대상 객체를 참조할 방법을 정함
- 원본 클래스에서 필드가 옮겨진 대상 클래스에 대한 객체를 참조할 수 있도록 방법을 마련
-
원본 클래스에서 필드를 삭제
- 원본 필드를 참조하는 모든 부분을 대상 클래스에 있는 적절한 메서드를 참조하게 수정
- 필드에 대한 읽기: getter 를 이용
- 핃들에 대한 쓰기: setter 를 이용
- 컴파일과 테스트
예제
public class Account {
...
private AccountType _type;
private double _interestRate;
double interestForAmount_days(double amount, int days) {
return _interestRate * amount * days / 365;
}
...
}
public class AccountType {
...
}
리팩토링 목표
- 이자율을 나타내는
_interestRate
필드를 계좌 유형에 따라 이자율이 변경될 수 있다는 생각하에AccountType
클래스로 옮기려 함
필드 캡슐화 (Encapsulate Field)
public class AccountType {
...
private double _interestRate;
public double getInterestRate() {
return _interestRate;
}
public void setInterestRate(double interestRate) {
_interestRate = interestRate;
}
...
}
public class Account {
private AccountType _type;
//private double _interestRate; 삭제
...
public double interestForAmount_days(double amount, int days) {
return _type.getInterestRate() * amount * days / 365;
}
...
}
필드 자체 캡슐화 (Self Encapsulate Field)
많은 메서드가 interestRate
필드를 사용한다면 내부적인 변화에 대응하기 쉽도록 필드에 대한 캡슐화(getter, setter)를 생성하여 사용한다.
public class Account {
private AccountType _type;
...
public double interestForAmount_days(double amount, int days) {
return getInterestRate() * amount * days / 365;
}
private void setInterestRate(double arg) {
_type.setInterestRate(arg);
}
private double getInterestRate() {
return _type.getInterestRate();
}
...
}
클래스 추출 (Extract Class)
두 클래스가 처리해야 할 기능이 하나의 클래스에 들어 있을 경우
→ 새 클래스를 만들고 기존 클래스의 관련 필드와 메서드를 새 클래스로 옮김
동기
클래스는 확실하게 추상화 되어야하며, 두세 가지의 명확한 기능을 담당해야 함
방대한 클래스
- 개발자는 클래스에 점증적으로 기능이나 데이터를 추가 → 클래스의 방대화
- 별도의 클래스로 만들기에는 사소한 기능의 추가가 계속되면 클래스의 복잡도가 증가
어느 부분에 적용해야 하나?
- 데이터의 일부분과 메서드의 일부분이 한 덩어리인 경우
- 함께 변화된 코드들
- 유난히 의존적인 데이터의 일부분
판단 방법
데이터나 메서드를 하나 제거하면 어떻게 될지, 다른 필드와 메서드를 추가하는 건 합리적이지 않은지 자문해 본다.
방법
-
클래스의 기능 분리 방법을 결정
-
분리한 기능을 넣을 새 클래스를 작성
-
원본 클래스에서 새 클래스로의 링크를 생성
-
옮길 필드마다 필드 이동(Move Field)을 적용
-
필드를 하나씩 옮길 때마다 컴파일과 테스트를 실시
-
메서드 이동(Move Method)을 실시해서 원본 클래스의 메서드를 새 클래스로 이동
하급메서드(피 호출 메서드)부터 시작해서 상급 메서드(호출 메서드)에 적용 -
메서드 이동을 실시할 때마다 테스트를 실시
- 각 클래스를 다시 검사해서 인터페이스를 줄임
- 양방향 링크가 있다면 가능하다면 단방향으로 바꿈
- 여러 곳에서 클래스에 접근할 수 있게 할지 결정
- 여러 곳에서 접근할 수 있게 할 경우 새 클래스를 참조 객체나 변경불가 값 객체로서 공개할지 여부를 결정
예제
public class Person {
private String _name;
private String _officeAreaCode;
private String _officeNumber;
...
public String getTelephoneNumber() {
return ("(" + _officeAreaCode + ")" + _officeNumber);
}
String getOfficeAreaCode() {
return _officeAreaCode;
}
void setOfficeAreaCode(String officeAreaCode) {
_officeAreaCode = officeAreaCode;
}
String getOfficeNumber() {
return _officeNumber;
}
void setOfficeNumber(String officeNumber) {
_officeNumber = officeNumber;
}
...
}
리팩토링 목표
Person
클래스에서 전화번호 기능을 따로 하나의 클래스로 분리
public class Person {
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
...
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return _officeTelephone.getTelephoneNumber();
}
// 위임메서드
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
...
class TelephoneNumber {
private String _areaCode;
private String _number;
public String getTelephoneNumber() {
return ("(" + _areaCode + ")" + _number);
}
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String areaCode) {
_areaCode = areaCode;
}
String getNumber() {
return _number;
}
void setNumber(String number) {
_number = number;
}
}
}
TelephoneNumber
클래스의 공개 정도
getOfficeTelephone
위임 메서드를 이용하며 패키지 내에서만 공개
클래스 공개시 객체 변경에 대처 방식
- 어디에서든 공개된 클래스의 객체가 변경될 수 있음을 받아들임
- 어느 주체든 원본 클래스를 거치지 않고는 클래스의 값을 변경하지 못하도록 함
- 클래스를 외부로 전달하기 전에 객체를 복제하여 원 객체가 변경되지 않도록 함
→ 코드를 보는 이들은 값의 변경이 가능한 것으로 착각할 수 있으므로 클라이언트 간의 왜곡 문제가 발생할 수 있음
클래스 내용 직접 삽입 (Inline Class)
클래스에 기능이 너무 적을 경우
→ 그 클래스의 모든 기능을 다른 클래스로 합치고 원본 클래스는 삭제
동기
클래스 내용 직접(Inline Class) 와 클래스 추출(Extract Class)는 반대 개념
적용 대상
클래스의 대부분의 기능이 리팩토링을 통해서 다른 곳으로 옮겨져 클래스가 더이상 존재할 이유가 없어진 클래스
방법
- 원본 클래스의
public
메서드를 합칠 클래스에 선언하고 전부 원본 클래스에 위임- 원본 클래스의 메서드 대신 별도의 인터페이스가 알맞다고 판단되면 클래스 내용 직접 삽입을 실시하기 전에 인터페이스 추출(Extract Interface) 기법을 실시
- 원본 클래스를 참조하고 있는 부분들을 합칠 클래스를 참조하도록 수정
- 원본 클래스를
private
로 선언하고 패키지 밖에서 참조하고 있는 부분들을 삭제 - 컴파일러가 껍데기만 남은 원본 클래스를 참조하고 있는 부분들을 찾아낼 수 있게 원본 클래스명을 변경
- 원본 클래스를
-
컴파일과 테스트를 실시
-
메서드 이동(Move Method)과 필드 이동(Move Field)을 실시해 기능들을 합칠 클래스로 이전
- 원본 클래스를 삭제
예제
public class Person {
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
...
public String getName() {
return _name;
}
public String getTelephoneNumber() {
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone() {
return _officeTelephone;
}
...
}
public class TelephoneNumber {
private String _areaCode;
private String _number;
...
public String getTelephoneNumber() {
return ("(" + _areaCode + ")" + _number);
}
String getAreaCode() {
return _areaCode;
}
void setAreaCode(String arg) {
_areaCode = arg;
}
String getNumber() {
return _number;
}
void setNumber(String arg) {
_number = arg;
}
...
}
public class Client {
public static void main() {
Person martin = new Person();
martin.getOfficeTelephone().setAreaCode("781");
...
}
}
리팩토링 목표
존재할 의미가 없는 TelephoneNumber
클래스를 Person
클래스로 병합
public class Person {
...
String getAreaCode() {
return _officeTelephone.getAreaCode();
}
void setAreaCode(String arg) {
_officeTelephone.setAreaCode(arg);
}
String getNumber() {
return _officeTelephone.getNumber();
}
void setNumber(String arg) {
_officeTelephone.setNumber(arg);
}
...
}
public class Client {
public static void main() {
Person martin = new Person();
martin.setAreaCode("781");
...
}
}
대리 객체 은폐 (Hide Delegate)
클라이언트가 객체의 대리 클래스를 호출할 경우
→ 대리 클래스를 감추는 메서드를 서버에 작성하자
동기
캡슐화란 객체가 시스템의 다른 부분에 대한 정보를 일부분만 알 수 있도록 함
→ _변경이 일어날 때 전달해야 하는 객체가 줄어들어 변경이 용이_해짐
- 클라이언트 객체에서 서버 객체의 필드 중 하나에 정의된 메서드를 호출할 때 그 클라이언트는 이 대리 객체에 관하여 알아야 함
- 이 대리 객체의 변경이 발생하면 클라이언트도 변경해야 할 가능성이 높아짐
- 서버 객체에 대리 객체를 감추는 위임 메서드를 두면 클라이언트 객체에 대리 객체를 숨길 수 있음 → 대리 객체의 변경이 클라이언트에 영향을 미치지 않음
방법
-
대리 객체에 들어 있는 각 메서드를 대상으로 서버에 간단한 위임 메서드를 작성
- 클라이언트를 수정해서 서버를 호출하도록 수정
- 클라이언트 클래스가 서버 클래스와 같은 패키지에 들어 있지 않다면 대리 클래스의 메서드에 대한 접근을 같은 패키지에 든 클래스만 접근할 수 있게 수정하는 것을 고려
-
각 메서드를 수정할 때마다 컴파일과 테스트를 실시
-
대리 객체를 읽고 써야 할 클라이언트가 하나도 남지 않게 되면, 서버에서 대리 객체를 제공하는 읽기/쓰기 메서드를 삭제
- 컴파일과 테스트를 실시
예제
public class Person {
Department _department;
...
public Department getDepartment() {
return _department;
}
public void setDepartment(Department arg) {
_department = arg;
}
...
}
public class Department {
private String _chargeCode;
private Person _manager;
...
public Department(Person manager) {
_manager = manager;
}
public Person getManager() {
return _manager;
}
...
}
// In client
Person manager = john.getDepartment().getManager();
리팩토링 목표
클라이언트에서 대리 객체인 Department
클래스를 은닉함
public class Person {
...
// 삭제
//public Department getDepartment() {
// return _department;
//}
// 삭제
//public void setDepartment(Department arg) {
// _department = arg;
//}
...
public Person getManager() {
return _department.getManager();
}
}
// In client
Person manager = john.getManager();
과잉 중개 메서드 제거 (Remove Middle Man)
클래스에 자잘한 위임이 너무 많을 경우
→ 대리 객체를 클라이언트가 직접 호출하도록 함
동기
대리 객체 은폐(Hide Delegate) 와 과잉 중개 메서드 제거(Remove Middle Man) 은 반대 개념
대리 객체의 캡슐화는 많은 장점을 가지지만,
클라이언트가 대리 객체의 기능을 사용할 때마다 서버에 위임 메서드를 만들어야 하는 단점도 존재
적절한 은폐의 정도
- 시간을 두고 조금씩 수정해 나감
- 기존에는 적절한 것으로 보이던 캡슐화가 현재는 불필요할 수 있을 수 있으므로 필요할 때마다 보수
방법
-
대리 객체에 대한 접근 메서드를 작성
-
대리 메서드를 클라이언트가 사용할 때마다 서버에서 메서드를 제거하고 클라이언트에서 호출을 대리 객체에서의 메서드 호출로 교체
-
메서드를 수정할 때마다 테스트를 실시
예제
public class Person {
...
public Person getManager() {
return _department.getManager();
}
...
}
// In client
Person manager = john.getManager();
리팩토링 목표
클라이언트에서 대리 객체인 Department
클래스를 직접 사용할 수 있도록 함
public class Person {
...
public Department getDepartment() {
return _department;
}
...
}
// In client
Person manager = john.getDepartment().getManager();
일부 클라이언트에서는 위임 메서드를 그대로 사용하고 일부 클라이언트에서는 대리 객체를 사용할 경우도 있을 수 있으므로
위임 메서드를 그대로 둬야할 경우도 있음
외래 클래스에 메서드 추가 (Introduce Foreign Method)
사용 중인 서버 클래스에 메서드를 추가해야 하는데 그 클래스를 수정할 수 없을 경우
→ 클라이언트 클래스 안에 서버 클래스의 인스턴스를 첫 번째 인자로 받는 메서드를 작성
동기
- 원본 클래스의 수정이 불가한 경우에 중복적으로 작성해야 코드
- 리팩토링을 실시할 때 새로 만드는 메서드를 외래 메서드로 만들어서 원본 메서드인 서버 메서드에 있어야 하는 것을 드러낼 수 있음
- 임시 방편으로 원래 있어야 할 위치로 보내는 것을 목표로 해야 함
방법
- 필요한 기능의 메서드를 클라이언트 클래스 안에 작성
- 메서드 내에서는 클라이언트 클래스의 어떠한 기능에도 접근해서는 안됨
→ 값이 필요할 때는 매개변수로 전달
- 메서드 내에서는 클라이언트 클래스의 어떠한 기능에도 접근해서는 안됨
-
서버 클래스의 인스턴스를 첫 번째 매개변수로 만듦
- 그 메서드에 ‘서버 클래스에 있을 외래 메서드’ 같은 주석을 작성
- 외래 메서드를 옮길 일이 생겼을 경우 문자열 검색 기능의 활용이 가능
예제
Calendar newStart1 = (Calendar)end1.clone();
newStart1.add(Calendar.DAY_OF_MONTH, 1);
Date newStart2 = new Date(end2.getYear(), end2.getMonth(), end2.getDate() + 1);
리팩토링 목표
중복적으로 발생할 수 있는 날짜 관련 처리를 확장하는 메서드를 생성
Calendar newStart1 = getNextDay(end1);
Date newStart2 = getNextDay(end2);
...
private static Calendar getNextDay(Calendar arg) {
Calendar newStart = (Calendar)arg.clone();
newStart.add(Calendar.DAY_OF_MONTH, 1);
return newStart;
}
private static Date getNextDay(Date arg) {
Date newStart = new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
return newStart;
}
국소적 상속확장 클래스 사용 (Introduce Local Extension)
사용 중인 서버 클래스에 여러 개의 메서드를 추가해야 하는데 클래스를 수정할 수 없을 땐
→ 새 클래스를 작성하고 그 안에 필요한 여러개의 메서드를 작성
→ 이 상속확장 클래스를 원본 클래스의 하위클래스나 래퍼클래스로 만듦
동기
원본 클래스의 수정이 불가능할 때 필요한 메서드가 한 두개라면 외래 클래스에 메서드 추가(Introduce Foreign Method) 기법을 실시
하지만 그 수가 세 개 이상이라면 메서드를 적당한 곳에 모아 두어야 함
해결을 위한 방법: 하위클래스화(subclassing) + 래퍼화(wrapping) → 국소적 상속확장 클래스(Local Extension)
국소적 상속확장 클래스는 별도의 클래스이지만 원본 클래스의 하위타입이므로, 원본 클래스의 모든 기능을 사용할 수 있으면서 추가 기능도 사용할 수 있게 됨 → 원본 클래스를 사용하는 게 아니라 국소 상속확장 클래스를 사용하게 됨
사용하면 얻을 수 있는 이점
메서드와 데이터가 체계적으로 묶여야 한다는 원칙이 지켜짐
하위클래스(subclass)
- 작업량 줄어듬
- 객체 생성 시점에서 하위클래스의 인스턴스로 만들어야 함
래퍼클래스(wrapper)
- 원본클래스를 래핑하고 있음
- 래퍼클래스의 변경이 원본클래스에 반영되고, 그 반대의 경우도 반영됨
방법
-
상속확장 클래스를 작성한 후 원본 클래스의 하위클래스나 래퍼클래스로 만듦
- 상속확장 클래스에 변환 생성자를 만듦
- 생성자에서 원본클래스를 인자로 받음
- 하위클래스: 적절한 상위클래스 생성자를 호출
- 래퍼클래스: 대리 필드에 그 인자를 할당
-
상속확장 클래스에 새 기능을 추가
-
필요한 위치마다 원본 클래스를 상속확장 클래스로 수정
- 원본 클래스 용으로 정의된 외래 메서드를 전부 상속확장 클래스로 옮김
예제
// 하위 클래스
class MfDateSub extends Date {
public MfDateSub getNextDay()...
public int dayOfYear()...
}
// 래퍼 클래스
class MfDateWrap {
private Date _original;
...
}
C# 의 확장 메서드
C# 의 확장 메서드는 원본 클래스의 파생(확장,하위)클래스를 만들지 않고 메서드를 추가하는 방법
→ 확장 메서드는 특수한 정적 메서드 이지만 원본 클래스의 인스턴스 메서드인 것처럼 호출됨
- 정적 클래스 내에 정의해야 함
- 첫 번째 매개변수는
this
키워드를 포함한 확장하고자 하는 원본 형식을 이용
public static class DateTimeExtensions
{
public static DateTime NextDay(this DateTime date)
{
return date.AddDays(1);
}
public static DateTime PreviousDay(this DateTime date)
{
return date.AddDays(-1);
}
}
[TestClass]
public class DateTimeExtensionsUnitTest
{
[TestMethod]
public void NextDayTest()
{
// Arrange
DateTime date1 = new DateTime(2015, 2, 28);
DateTime date2 = new DateTime(2016, 2, 28);
// Act
DateTime next1 = date1.NextDay();
DateTime next2 = date2.NextDay();
// Assert
Assert.AreEqual<int>(1, next1.Day);
Assert.AreEqual<int>(29, next2.Day);
}
[TestMethod]
public void PreviousTest()
{
// Arrange
DateTime date1 = new DateTime(2015, 3, 1);
DateTime date2 = new DateTime(2016, 3, 1);
// Act
DateTime prev1 = date1.PreviousDay();
DateTime prev2 = date2.PreviousDay();
// Assert
Assert.AreEqual<int>(28, prev1.Day);
Assert.AreEqual<int>(29, prev2.Day);
}
}
Comments