기능을 제공한다.
절차지향은 데이터 중심, 객체지향은 기능 중심
객체 = 데이터 + 프로시져(오퍼레이션, 메소드, 함수)
가장 중요한 키워드 1. 정보은닉 2. 유연성
객체지향의 정의 한마디로: 유지보수성이 뛰어난 프로그래밍 설계 방식 3요소 5규칙(java)을 지키면서 오는 잇점을 바탕으로...
정환이라면 -> 추상화 -> , 관계
다형성은 왜 있는가?
개념적 인터페이스
: 객체가 제공하는 모든 오퍼레이션(기능)의 집합을 객체의 인터페이스라고 부른다. 객체를 사용하기 위한 일종의 명세 혹은 규칙이라고 생각하자. 한객체가 갖는 책임을 정의한 것
각각 언어에서 지원하는 인터페이스 -> 자바에서는 interface => implement로 인터페이스의 목적을 달성하게 하는 기능이 있다. 인터페이스는 오퍼레이션의 정의, implement에서 구현
캡슐화와 의존의 양면성은 서로 충돌하는데 이걸 어떻게 설명해야하지?
객체지향의 장점: 한 곳의 구현 변경이 다른 곳에 변경을 가하지 않도록 하는데 있다.
캡슐화란: 객체가 내부적으로 기능을 어떻게 구현하는지 감추는 것 즉, 내부 구현이 변경되더라도 그 기능을 사용하는 코드는 영향을 받지 않는다.
캡슐화를 위한 두개의 규칙
// 절차지향
if (member.getExpiryDate()!=null && member.getExpiryDate().getDate() < System.currentTimeMillis()){
// 처리
}
// 객체지향
if (member.isExpired()){
// 처리
}
law of memeter(데미테르의 법칙)
// ex
if (member.getDate().getTime()< ...){ // 데미테르 법칙 위반
}
// 그럼 어떻게해야함?
class member {
private Datetime datetime;
public getDate(){
return this.datetime;
}
public getTimeByDate() {
return this.getDate().getTime();
}
}
if (member.isOverTime()){
// 데미테르 법칙 지킴
}
디미터의 법칙("친구하고만 대화하라")이 좋은 출발점이긴 하지만, 이런 식으로 생각하자. 자기 소유의 장난감, 자기가 만든 장난감, 그리고 누군가 자기에게 준 장난감하고만 놀 수 있다. 하지만 절대 장난감의 장난감과 놀면 안 된다.
// 이렇게 써도 문제 없음
IntStream.of(1, 15, 20, 3, 9)
.filter(x -> x > 10)
.distinct()
.count();
of
, filter
, distinct
메서드는 모두 IntStream
이라는 동일한 클래스의 인스턴스를 반환한다. 즉, 이들은 IntStream
의 인스턴스를 또다른 IntStream
의 인스턴스로 변환한다. 따라서 이 코드는 디미터 법칙을 위반하지 않는다.
IntStream
의 내부 구조가 외부로 노출됐는가? 그렇지 않다. 단지 IntStream
을 다른 IntStream
으로 변환할 뿐, 객체를 둘러싸고 있는 캡슐은 그대로 유지된다.정보은닉에 대한 글: http://egloos.zum.com/aeternum/v/1232020
정보은닉에 대한 내 정리:
슐화라는 것은 간단히 말하면 오브젝트 내부의 성질을 오브젝트 외부에서 직접 관여하지 못하게 하는 것이고 오직 오브젝트가 제공하는 루트를 통해서만 관여할 수 있게끔 하는 것입니다.
한 타입을 그대로 사용할 수 있으면서, 구현을 추가해주는 방법.
확장에는 열려있고, 변화에 유연하게 대처 가능하다.(유연성의 목적을 달성하는 또다른 방법)
한마디로 정의: 한 타입을 그대로 사용하면서, 추가적인 구현을 가능하도록 하는 방법 혹은 기능
// 쿠폰이 있고, 그 쿠폰이 할인해 줄 수 있는 돈이 정해져있다.
// 사용자가 이 쿠폰을 사용할때, 쿠폰으로 할인받고 나서의 가격을 반환받는 기능이 있다.
public class Coupon{
private int discountAmount;
public Coupon(int discountAmount){
this.discountAmount = discountAmount;
}
public int getDiscountAmount(){
return this.discountAmount;
}
public int calculateDiscountedPrice(int price){
if (this.discountAmount <= price) {
return price - dicscountAmount;
}
return 0;
}
}
// 쿠폰중에서, 지정한 가격보다 큰 상품을 구매할때만 쿠폰을 적용할 수 있는 기능을 추가하고 싶다.
public class LimitPriceCoupon extends Coupon {
private int limitPrice;
public LimitedPriceCoupon(int limitPrice, int discountAmount){
super(discpuntAmount);
this.limitPrice = limitPrice;
}
public int getLimitPrice(){
return this.limitPrice;
}
@override
public int claculateDiscountedPrice(int price){
if (price < this.limitPrice){
return price;
}
return super.calculateDiscountedPrice(int price);
}
}
상속을 통해서, 다형성이라는 특징을 갖게 됩니다. 다형성의 뜻은 한 객체가 여러가지 타입을 가질 수 있다는 성질인데, 이러한 다형성을 갖게 할 수 있는 방법이 바로 상속입니다.
상속은 두가지로 나뉩니다. 인터페이스 상속과, 구현상속이 있습니다.
public class FlowController {
// 소켓 데이터 요청을 받기 전
public void process() {
// 데이터 읽기 객체 직접 생성
FileDataReader reader = new FileDataReader();
// 흐름제어: 1 읽기
byte[] data = reader.read();
Encryptor encryptor = new Encryptor();
// 흐름제어: 2 암호화
byte[] encryptedData = encryptor.encrypt(data);
FileDataWriter writer = new FileDataWriter();
// 흐름제어: 3 쓰기
writer.write(encryptedData);
}
}
데이터를 읽어오는 객체를 생성하는 책임
과 흐름을 제어하는 책임
을 동시에 가지고 있다. 이를 분리하기 위해 추상화작업을 통하여, ByteSourceFactory라는 클래스를 생성하고, 그 팩토리 클래스에서 데이터를 읽어오는 책임
을 부여하는 것이다.// byteSourceFactory 클래스 코드
public class ByteSourceFactory {
public ByteSource create() {
if (useFile()) {
return new FileDataReader();
} else if {
return new SocketDataReader();
}
}
private boolean useFile() {
String useFileVal = System.getProperty("useFile");
return useFileVal != null && Boolean.valueOf(useFileVal);
}
// lazyHolder 방법의 싱글톤 패턴
private ByteSourceFactory() {}
public static ByteSourceFactory getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final ByteSourceFactory INSTANCE = new ByteSourceFactory();
}
}
// flowController 클래스 수정
public class FlowController {
// 소켓 데이터 요청을 받기 전
public void process() {
ByteSource source = ByteSourceFactory.getInstance().create();
byte[] data = source.read();
Encryptor encryptor = new Encryptor();
byte[] encryptedData = encryptor.encrypt(data);
FileDataWriter writer = new FileDataWriter();
writer.write(encryptedData);
}
}
상위클래스 변경의 어려움
클래스의 불필요한증가
상속의 오용
컨테이너의 수화물 목록을 관리하는 클래스가 필요하다고 할때, 이 클래스는 세가지 기능을 제공한다고 하자.
- 수화물을 넣는다.
- 수화물을 뺀다.
- 수화물을 넣을 수 있는지 확인한다.
ArrayList를 상속하여 Contatiner클래스를 만들어 사용하면, ArrayList의 메서드와 Container에서 정의한 사용자 함수를 구분하지 못하고, 사용자 입장에서 혼용하여 쓸 수 있다. 이를 방지하기 위하여 조립이라는 방법으로 해결하고자 한다.
필드에서 다른 객체를 참조하는 방식
으로 구현한다.// 예를들어 FlowController클래스의 경우,
public class FlowController {
private Encryptor encryptor = new Encryptor();
public void process(){
...
byte[] encryptedData = enryptor.encrypt(data);
...
}
}
확장에는 열려있어야 하고, 변경에는 닫혀있어야 한다.
추상화와 다형성을 잘 이용하면 개방 폐쇄 원칙을 지키게 된다. 일 예로 펙토리 패턴
을 보면 이해하기 쉽다.
또 다른 방법은 상속을 이용하는 것이다.
템플릿 패턴
을 이용해서 일련의 과정을 정의하고, 세부 과정은 각 세부 클래스를 구현하여 각각의 기능을 수행하도록 하는 방법 -> 뜬구름 잡는 모습인데, 그냥 템플릿 패턴을 이용하면 상속을 통해 개방 폐쇄 원칙을 지킨다고 생각하면 된다.변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 해준다.
instanceof
연산자를 이용해 분기를 만들면 확장에 열려있지 않다는 뜻이다.(아직 잘 이해가 안감) 리스코프 치환 법칙을 위반할 때 자주 발생하는 증상임public class CouPon{
// 할인에 적용될 가격
private float discoutRate = 0.5;
public int calculateDiscountAmount(Item item){
return item.getPrice() * discountRate;
}
}
// 추가 기능 요청 -> 특정 클래스의 아이템일 경우, 할인에 적용되지 않게 한다.
public class CouPon{
// 할인에 적용될 가격
public int calculateDiscountAmount(Item item){
if(item instanceof SpecialItem){
return 0;
}
return item.getPrice() * discountRate;
}
}
nstanceof
를 제거하기 위해 다음과 같이 변형한다.public Class Item {
public boolean isDiscountAvailable(){
return true;
}
}
public class SpecialItem extends Item {
@Override
public boolean isDiscountAvailable(){
return false;
}
}
public class CouPon{
// 할인에 적용될 가격
public int calculateDiscountAmount(Item item){
if(!item.isDiscountAvailable()){
return 0;
}
return item.getPrice() * discountRate;
}
}
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저 수준 모듈이 고 수준 모듈에서 정의한 추상타입에 의존해야한다.
고수준 모듈: 어떤 의미있는 단일 기능을 제공하는 모듈
저수준 모듈: 고 수준 모듈 기능을 구현하기 위해 필요한 하위 기능의 실제 구현
저수준의 모듈이 고수준의 모듈을 의존하도록 한다.
어떻게? byteSourceFactory예제를 보면 추상화가 이루어질 수 있다.
정환 ->> 추상성이 낮은 클래스보다 추상성이 높은 클래스에 의존성을 맺어야 한다.
child라는 클래스가 있고, robot, rccar, doll이 라는 인터페이스가 있으면 이 모든 인터페이스들은 child이 playWith(Toy toy)하는 메소드를 쓸때는 has a 관계이고 이는 child가 toy에 의존하게 되며 toy인터페이스를 상속하는 robot, rccar, doll의 변화가 있더라도 toy에는 아무런 영향을 끼치지 않으므로 유지보수성을 보장할 수 있다.
소스코드상에서 의존성의 역전을 지키는 것이지 런타임상의 역전을 원하는 것은 아니다.
의존 역전 원칙은 개방폐쇄 원칙을 클래스 수준뿐 아니라 패키지 수준에서까지 확장시켜준다.