ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체 지향과 디자인 패턴 - 다형성과 추상 타입
    언어/객체지향 2023. 8. 13. 15:33

    - 객체 지향의 장점은 구현 변경의 유연함 

    - 캡슐화를 통해 내부 구현의 유연함을 얻을 수 있었음

    - 또 다른 유연함은 추상화를 통해 얻을 수 있다. 

     

    * 객체 지향의 사실과 오해 -> 추상화의 방법 1. 공통점(객체 구분) 2. 일반/서브타입 나누기 (부모/자식) 

     

    1. 상속 

     

    - 상속은 기존 클래스의 기능을 그대로 사용하면서 확장할 수 있게 한다. 

    - 오버라이드

     

    2. 다형성과 상속

     

    - 다형성이란 한 객체가 여러 모습을 가진다 -> 여러 기능(책임)을 수행할 수 있다.

    - 자바와 같은 정적 타입 언어는 상속을 통해서 다형성을 구현한다.

     

    3. 인터페이스 상속 및 구현 

     

    - 인터페이스 상속과 구현 상속으로 구분 할 수 있다.

    - 인터페이스 상속: 타입(개념상 인터페이스) 정의만 상속받는 것 (추상클래스, 인터페이스 등)

                 구현 상속: 이미 구현된 클래스 상속 -> 기능 재사용 + 다형성 함께 제공 

     

    4. 추상 타입과 유연함 

     

    - 추상화는 데이터나 프로세스등 의미가 비슷한 개념이나 표현으로 정의하는 과정 -> 단순화 

    * 추상화가 반드시 추상타입만을 의미하는 것은 아님

     

    ex) FTP에서 파일다운로드, 소켓으로 데이터 읽기, DB 테이블의 데이터 조회 

          -> 추상화 -> 로그수집 

         이러한 추상화를 통해 인터페이스를 도출할 수 있다. 

         추상화된 타입은 오퍼레이션 시그니처만 제공할 뿐 실제 구현은 없다 (인터페이스 타입) 

     

    4.1 추상타입과 실제 구현 연결 

     

    - 상속을 통해서 연결한다 즉, 구현 클래스가 추상 타입을 상속 받는 방법으로 연결한다. 

    - 실제 구현을 제공하는 클래스를 콘크리트 클래스라고 부른다.

     

    4.2 추상 타입을 이용한 구현 교체의 유연함 

    public class FlowController{
    
    	private boolean useFile;
        
        public FlowController (boolean useFile){
        	this.useFile= useFile;
        }
        
        public void process(){
        	byte[] data = null;
            
            if(useFile){
            	FileDataReader filereader = new FileReader();
                data = fileReader.read();
            }else{
            	SocketDataReader soketreader = new SoketDataReader();
                data = soketreader.read();
            }
            
            // 읽어온 데이터 암호화 코드..쓰는 코드 
    
        
        }
    
    }

    - FlowController 본연의 책임(흐름제어)와 상관 없이 데이터를 읽는 구현의 변경(요구사항의 변경)으로 FlowController도 함께 바뀌었다.

    - 소켓을 통해 읽던, 파일에서 읽던 바이트 데이터를 읽는다는 공통점이 있다. -> 추상화 

     

    public interface ByteSource{
    	public byte[] read();
    }
    
    public class FileDataReader implements ByteSource{//.. }
    public class SoketDataReader implements ByteSource{//.. }
    
    
    // 코드 변경 
    ByteSoruce source =null
    if(useFile) source = new FiledataReader();
    else source = new SocketDataReader();

    - 추상화를 통해 ByteSource 인터페이스 도출 -> ByteSource 도출 -> 기존 코드 단순화

    - 아직 if-else 문에 있으니 ByteSource 구현 객체 생성하는 부분 별도처리 

       의존 객체 변경되더라도, 기존 객체 변경x의 방법 

       -> 객체 생성하는 기능을 별도 객체 분리, 그 객체로 ByteSource 생성 

       -> 생성자(또는 다른 메서드)를 이용해서 ByteSoruce 전달받기 

     

    public class FlowController{
    
    	public void process(){
        
        	ByteSource source = ByteSourceFactory.getInstance().create();
            byte[] data = source.read();
        	// 팩토리 메서드 패턴을 통해 위와 같이 변경 
        }
    
    
    }

    - FlowController에서 ByteSourceFactory로 분리할 수 있었다. 

    - 푸상화로 공통 개념 도출 + 많은 책임을 가진 객체 책임 분리 

     

    * FlowContrller가 그 안의 다른 객체 dataReader등 (의존하는 객체)들 보다 더 큰 수준에서 재사용이 이루어진다.

       재사용의 중요성으로 봤을 때 하위수준 상세구현보다 변하지 않는 상위 수준의 로직을 재사용 가능하게 설계하는 것이 중요하다.

     

    4.3 변화되는 부분을 추상화하기 

     

    - 추상화는 어렵다 -> 쉽게 추상화하는 기준 -> 변화되는 부분을 추상화하자! 

    - 변화가 자주 발생하는 곳을 추상 타입으로 교체하면 향후 변경에 유연하게 대처할 가능성이 높아진다.

    - 변화 부분에 추상화가 잘 적용되어 있지 않으면, if-else 중첩구조 발생한다. 

     

    4.4 인터페이스에 대고 프로그래밍 하기 

     

    - 오퍼레이션을 정의한 개념상의 인터페이스를 의미한다.

    - 콘크리트 클래스가 아닌, 기능을 정의한 인터페이스를 사용해서 프로그래밍 하라는 것이다. 

    - 하지만, 인터페이스는 요구사항의 변화와 함께 점진적으로 도출됨  -> 변화 가능성이 높아 보이는 곳에 사용하자 

     

    4.5 인터페이스는 사용자 입자에서 만들자

     

    - 인터페이스를 작성할 때는 그 인터페이스를 사용하는 코드 입장에서 작성하자 

    - 사용의 혼란을 야기할 수 있다.

     

    4.6 인터페이스와 테스트 

     

    - 객체의 책임이 인터페이스를 사용하여 추상화되어 있다면, 테스트가 보다 쉬워진다. 

     

    public void testProcess(){
    
    	FlowController fc = new FlowController();
        fc.process();
    	// 만약 process() 내부에 아직 데이터 읽는 ByteSoruce가 완성 전이라면 테스트 할 수 없음
    }
    
    
    //1 .테스트를 위해 생성자를 추가하자 (FlowController에 )
    
    public class FlowController {
    private ByteSource byteSource;
    
    public FlowController(ByteSource bytesource){
    	this.byteSource = bytesource;
    }
    
    public void process(){
    	byte[] data = byteSource.read();
    }
    }
    // 2. ByteSource를 구현한 임의의 콘크리트 클래스를 재정의한 뒤에 이를 주입 
    
    public void testProcess(){
    	ByteSoruce mocksource = new ByteSource{
        	@Override
            public byte[] read(){
            	//..구현
            }
        
        };
        
        FlowController fc = new FlowController(mocksource);
        fc.process();
    
    }

    - 위와 같이 추상화된 부분은 테스트하기 더 쉽다. (임의 구현으로 구현을 교체해서 테스트하면 됨)

    - 임의 객체 사용할 대상을 인터페이스로 추상화하면 더 쉽게 Mock객체와 같은 것을 만들 수 있다. 

Designed by Tistory.