OOP 마스터하기: 상속, 인터페이스 및 추상 클래스에 대한 실용적인 가이드
게시 됨: 2022-03-10내가 말할 수 있는 한, 이론과 실제 정보의 적절한 혼합을 제공하는 소프트웨어 개발 분야의 교육 콘텐츠를 접하는 것은 드문 일입니다. 그 이유를 추측하자면, 이론에 치중하는 사람은 가르치는 일에 몰두하는 경향이 있고, 실용적인 정보에 치중하는 사람은 특정 언어와 도구를 사용하여 특정 문제를 해결하는 데 돈을 받는 경향이 있기 때문이라고 생각합니다.
물론 이것은 광범위한 일반화이지만 논쟁을 위해 간단히 받아들인다면 교사의 역할을 맡은 많은 사람들(모든 사람이 아님)은 특정 개념과 관련된 실용적인 지식을 설명하는 것.
이 기사에서는 대부분의 객체 지향 프로그래밍(OOP) 언어에서 찾을 수 있는 세 가지 핵심 메커니즘인 상속 , 인터페이스 ( 프로토콜 이라고도 함) 및 추상 클래스 에 대해 최선을 다해 설명합니다. 각 메커니즘 이 무엇인지에 대한 기술적이고 복잡한 구두 설명을 제공하기보다, 그들이 하는 일과 사용 시기 에 초점을 맞추기 위해 최선을 다할 것입니다.
그러나 그것들을 개별적으로 다루기 전에 이론적으로 건전하지만 실질적으로 쓸모없는 설명을 한다는 것이 무엇을 의미하는지 간략하게 논의하고 싶습니다. 제 희망은 이 정보를 사용하여 다양한 교육 리소스를 살펴보고 이해가 되지 않을 때 자신을 비난하지 않도록 하는 것입니다.
다양한 지식 수준
이름 알기
어떤 것의 이름을 아는 것은 틀림없이 가장 얕은 지식의 형태일 것입니다. 사실, 이름은 많은 사람들이 같은 것을 지칭하기 위해 일반적으로 사용하거나 사물을 설명하는 데 도움이 되는 범위에서만 일반적으로 유용합니다. 불행히도, 이 분야에서 시간을 보낸 사람이라면 누구나 알 수 있듯이 많은 사람들이 동일한 것에 대해 다른 이름(예: 인터페이스 및 프로토콜 ), 다른 것에 대해 동일한 이름(예: 모듈 및 구성 요소 ) 또는 난해한 이름을 사용합니다. 터무니없는 점(예: 어느 쪽 모나드 ). 궁극적으로 이름은 정신 모델에 대한 포인터(또는 참조)일 뿐이며 다양한 정도의 유용성을 가질 수 있습니다.
이 분야를 연구하기 더 어렵게 만들기 위해 나는 대부분의 개인에게 코드 작성이 매우 독특한 경험(또는 최소한 그랬던 것)이라고 추측할 위험이 있습니다. 훨씬 더 복잡한 것은 해당 코드가 궁극적으로 기계어로 컴파일되고 물리적 현실에서 시간이 지남에 따라 변화하는 일련의 전기 충격으로 표현되는 방법을 이해하는 것입니다. 프로그램에 사용된 프로세스, 개념 및 메커니즘의 이름을 기억할 수 있다고 해도 그러한 대상에 대해 생성하는 멘탈 모델이 다른 개인의 모델과 일치한다는 보장은 없습니다. 객관적으로 정확한지는 말할 것도 없고요.
이러한 이유로 전문 용어에 대한 기억력이 선천적으로 좋지 않다는 사실과 함께 이름을 아는 것이 가장 중요하지 않은 측면이라고 생각합니다. 이름이 쓸모가 없다는 말은 아니지만, 나는 과거에 내 프로젝트에서 많은 디자인 패턴을 배우고 적용했지만 일반적으로 사용되는 이름을 몇 달 또는 몇 년 후에 배웠습니다.
언어적 정의와 유추 알기
언어적 정의는 새로운 개념을 설명하기 위한 자연스러운 출발점입니다. 그러나 이름과 마찬가지로 유용성과 관련성이 다를 수 있습니다. 그 대부분은 학습자의 최종 목표가 무엇인지에 달려 있습니다. 내가 언어적 정의에서 볼 수 있는 가장 일반적인 문제는 일반적으로 전문 용어의 형태로 지식이 가정된다는 것입니다.
예를 들어, 스레드 가 주어진 프로세스 의 동일한 주소 공간 을 점유한다는 점을 제외하고는 스레드 가 프로세스 와 매우 흡사하다고 설명한다고 가정해 보겠습니다. 프로세스 와 주소 공간 에 이미 익숙한 누군가에게 나는 본질적으로 스레드 가 프로세스 에 대한 이해와 연관될 수 있지만(즉, 동일한 특성을 많이 가짐) 고유한 특성에 따라 구별될 수 있다고 말했습니다.
그 지식을 소유하지 않은 사람에게, 나는 기껏해야 아무 의미도 없었고, 최악의 경우 학습자가 내가 그들이 알아야 한다고 생각 했던 것들을 알지 못하는 것에 대해 어떤 식으로든 자신이 부족하다고 느끼게 만들었습니다. 공정하게 말하면, 이것은 학습자가 실제로 그러한 지식(예: 대학원생 또는 숙련된 개발자 교육)을 소유해야 하는 경우 허용되지만 입문 수준의 자료에서 그렇게 하지 않는 것은 기념비적인 실패라고 생각합니다.
종종 학습자가 이전에 본 것과는 다른 개념에 대한 좋은 구두 정의를 제공하는 것이 매우 어렵습니다. 이 경우, 교사는 일반 사람에게 친숙할 가능성이 있는 유추를 선택하는 것이 매우 중요하며 개념의 동일한 특성을 많이 전달하는 한 관련성이 있습니다.
예를 들어, 소프트웨어 개발자는 소프트웨어 엔터티(프로그램의 다른 부분)가 밀접하게 결합 되거나 느슨하게 결합될 때 그것이 의미하는 바를 이해하는 것이 매우 중요합니다. 텃밭 헛간을 지을 때 어린 목수는 나사 대신 못을 사용하여 조립하는 것이 더 빠르고 쉽다고 생각할 수 있습니다. 이것은 실수가 발생하거나 정원 헛간 디자인의 변경으로 인해 헛간 일부를 재건해야 할 때까지 사실입니다.
이 시점에서 못을 사용하여 정원 헛간 부분을 단단히 연결하기로 결정하면 건설 과정이 전체적으로 더 어려워지고 느려질 수 있으며 망치로 못을 뽑으면 구조물이 손상 될 위험이 있습니다. 반대로 나사는 조립하는 데 시간이 조금 더 걸릴 수 있지만 쉽게 제거할 수 있고 창고 근처에 손상을 줄 위험이 거의 없습니다. 이것이 내가 느슨하게 결합된 것을 의미합니다. 물론 정말 못이 필요한 경우도 있지만 그 결정은 비판적 사고와 경험에 따라 결정되어야 합니다.
나중에 자세히 논의하겠지만, 다양한 수준의 결합 을 제공하는 프로그램의 일부를 함께 연결하는 다양한 메커니즘이 있습니다. 못 과 나사 처럼. 내 비유가 이 매우 중요한 용어가 의미하는 바를 이해하는 데 도움이 되었을지 모르지만 정원 헛간을 짓는 맥락 외에 적용하는 방법에 대한 아이디어는 제공하지 않았습니다. 이것은 나를 가장 중요한 종류의 지식으로 이끌고 모든 탐구 분야에서 모호하고 어려운 개념을 깊이 이해하는 열쇠입니다. 비록 우리는 이 기사에서 코드 작성을 고수할 것입니다.
코드로 알기
제 생각에는 엄격하게 소프트웨어 개발과 관련하여 개념을 아는 가장 중요한 형태는 작업 응용 프로그램 코드에서 사용할 수 있다는 것입니다. 이러한 형태의 지식은 단순히 많은 코드를 작성하고 다양한 문제를 해결함으로써 얻을 수 있습니다. 전문 용어 이름과 구두 정의는 포함할 필요가 없습니다.
내 경험에 따르면 단일 인터페이스 를 통해 원격 데이터베이스 및 로컬 데이터베이스와 통신하는 문제를 해결한 것을 기억합니다. 원격 및 로컬(또는 테스트 데이터베이스)을 명시적으로 호출해야 하는 클라이언트( 인터페이스 와 통신하는 모든 클래스)보다는. 사실 클라이언트는 인터페이스 뒤에 무엇이 있는지 몰랐기 때문에 프로덕션 앱에서 실행 중이든 테스트 환경에서 실행 중이든 상관없이 변경할 필요가 없었습니다. 이 문제를 풀고 약 1년 후에 "Facade Pattern"이라는 용어를 접하게 되었고 얼마 지나지 않아 "Repository Pattern"이라는 용어를 접하게 되었습니다. 둘 다 앞서 설명한 솔루션에 대해 사람들이 사용하는 이름입니다.
이 모든 서문은 상속 , 인터페이스 및 추상 클래스 와 같은 주제를 설명할 때 가장 자주 발생하는 몇 가지 결함을 조명하기 위한 것입니다. 세 가지 중 상속 은 사용하고 이해하기에 가장 간단한 것입니다. 프로그래밍 학생과 교사로서의 내 경험에 따르면, 앞에서 논의한 실수를 피하기 위해 특별한 주의를 기울이지 않는 한 나머지 두 가지는 거의 예외 없이 학습자에게 문제가 됩니다. 이 시점부터 나는 이러한 주제를 가능한 한 단순하게 만들기 위해 최선을 다할 것입니다. 그러나 더 단순하지는 않습니다.
예에 대한 참고 사항
Android 모바일 응용 프로그램 개발에 가장 능숙하므로 해당 플랫폼에서 가져온 예제를 사용하여 Java의 언어 기능을 소개함과 동시에 GUI 응용 프로그램 빌드에 대해 가르칠 것입니다. 그러나 Java EE, Swing 또는 JavaFX에 대한 피상적인 이해를 가진 사람이 예제를 이해할 수 없을 정도로 자세하게 설명하지는 않겠습니다. 이러한 주제를 논의하는 궁극적인 목표는 거의 모든 종류의 응용 프로그램에서 문제를 해결하는 맥락에서 이러한 주제가 의미하는 바를 이해하도록 돕는 것입니다.
친애하는 독자 여러분, 때때로 제가 특정 단어와 그 정의에 대해 불필요하게 철학적이고 현학적인 것처럼 보일 수 있음을 경고하고 싶습니다. 그 이유는 구체적인 것(실제)과 추상적 인 것(실제보다 덜 상세함)의 차이를 이해하는 데 필요한 깊은 철학적 토대가 실제로 있기 때문입니다. 이러한 이해는 컴퓨팅 분야 외의 많은 것들에 적용되지만 모든 소프트웨어 개발자가 추상화 의 본질을 이해하는 것은 특히 매우 중요합니다. 어쨌든 내 말이 실패하면 코드의 예제는 그렇지 않을 것입니다.
상속 및 구현
GUI(그래픽 사용자 인터페이스)를 사용하여 응용 프로그램을 빌드할 때 상속 은 틀림없이 응용 프로그램을 빠르게 빌드할 수 있도록 하는 가장 중요한 메커니즘입니다.
나중에 논의할 상속 을 사용하면 덜 이해되는 이점이 있지만 주요 이점은 클래스 간에 구현을 공유하는 것 입니다. "구현"이라는 단어는 최소한 이 기사의 목적에 따라 뚜렷한 의미를 갖습니다. 영어로 단어에 대한 일반적인 정의를 내리자면, 저는 무언가를 구현 한다는 것은 그것을 현실로 만드는 것이라고 말할 수 있습니다.
소프트웨어 개발에 대한 기술적 정의를 내리기 위해 소프트웨어를 구현 하는 것은 해당 소프트웨어의 요구 사항을 충족하는 구체적인 코드 줄을 작성하는 것이라고 말할 수 있습니다. 예를 들어 내가 sum 메서드 를 작성한다고 가정해 보겠습니다. private double sum(double first, double second){
private double sum(double first, double second){ //TODO: implement }
위의 스니펫은 반환 유형( double
)과 인수( first, second
)와 해당 메서드 를 호출하는 데 사용할 수 있는 이름( sum
)을 지정하는 메서드 선언 까지 작성했지만, 구현 되지 않았습니다 . 이를 구현 하려면 다음과 같이 메서드 본문 을 완료해야 합니다.
private double sum(double first, double second){ return first + second; }
당연히 첫 번째 예제는 컴파일되지 않지만 인터페이스 는 이러한 종류의 구현되지 않은 함수를 오류 없이 작성할 수 있는 방법이라는 것을 잠시 보게 될 것입니다.
자바의 상속
아마도 이 기사를 읽고 있다면 extends
Java 키워드를 한 번 이상 사용했을 것입니다. 이 키워드의 역학은 간단하며 다양한 종류의 동물이나 기하학적 모양과 관련된 예를 사용하여 가장 자주 설명됩니다. Dog
및 Cat
은 Animal
등을 확장합니다. 기본적인 유형 이론을 설명할 필요가 없다고 가정하고 extends
키워드를 통해 Java 상속 의 주요 이점을 살펴보겠습니다.
Java로 콘솔 기반 "Hello World" 애플리케이션을 구축하는 것은 매우 간단합니다. Java 컴파일러( javac )와 런타임 환경( jre )이 있다고 가정하면 다음과 같이 주요 기능을 포함하는 클래스를 작성할 수 있습니다.
public class JavaApp{ public static void main(String []args){ System.out.println("Hello World"); } }
거의 모든 주요 플랫폼(Android, Enterprise/Web, Desktop)에서 Java로 GUI 애플리케이션을 빌드하고 IDE의 약간의 도움으로 새 앱의 골격/보일러플레이트 코드를 생성하는 것도 비교적 쉽습니다. 키워드를 extends
합니다.
tvDisplay
라는 TextView
(텍스트 레이블과 같은)를 포함하는 activity_main.xml
(일반적으로 레이아웃 파일을 통해 Android에서 선언적으로 사용자 인터페이스를 빌드함)이라는 XML 레이아웃이 있다고 가정합니다.
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </FrameLayout>
또한 tvDisplay
가 "Hello World!"라고 말하기를 원한다고 가정합니다. 그렇게 하려면 단순히 extends
키워드를 사용하여 Activity
클래스에서 상속하는 클래스를 작성하면 됩니다.
import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((TextView)findViewById(R.id.tvDisplay)).setText("Hello World"); }
Activity
클래스의 구현을 상속하는 것의 효과는 소스 코드를 빠르게 살펴보면 가장 잘 알 수 있습니다. 일부 텍스트가 포함된 간단한 창을 생성하기 위해 시스템과 상호 작용하는 데 필요한 8000개 이상의 라인 중 작은 부분이라도 구현 해야 했다면 Android가 지배적인 모바일 플랫폼이 되었을지 매우 의심스럽습니다. 상속 을 통해 Android 프레임워크 또는 작업 중인 플랫폼이 무엇이든 처음부터 다시 빌드할 필요가 없습니다.
상속을 추상화에 사용할 수 있습니다.
클래스 간에 구현을 공유하는 데 사용할 수 있는 한 상속 은 비교적 이해하기 쉽습니다. 그러나 상속 을 사용할 수 있는 또 다른 중요한 방법이 있습니다. 이는 곧 논의할 인터페이스 및 추상 클래스 와 개념적으로 관련됩니다.
당신이 원한다면, 가장 일반적인 의미에서 사용되는 추상화 가 사물에 대한 덜 상세한 표현 이라고 다음 잠시 동안 가정해 보십시오. 긴 철학적 정의로 그것을 한정하는 대신, 나는 추상화 가 일상 생활에서 어떻게 작동하는지 지적하려고 노력할 것이고, 곧 그것들을 소프트웨어 개발의 관점에서 명시적으로 논의할 것입니다.
당신이 호주로 여행 중이고 당신이 방문하는 지역이 특히 고밀도 내륙 타이판 뱀의 숙주라는 것을 알고 있다고 가정합니다. 이미지 및 기타 정보를 보고 Wikipedia에 대해 더 자세히 알아보기로 결정했습니다. 그렇게 함으로써, 당신은 지금까지 한 번도 본 적이 없는 특정한 종류의 뱀을 예리하게 인식하고 있습니다.
추상화, 아이디어, 모델 또는 그 밖의 무엇이라고 부르고 싶은 것은 사물에 대한 덜 상세한 표현입니다. 실제 뱀이 당신을 물 수 있기 때문에 실제보다 덜 자세하게 묘사하는 것이 중요합니다. Wikipedia 페이지의 이미지는 일반적으로 그렇지 않습니다. 추상화 는 컴퓨터와 인간의 두뇌 모두 정보를 저장, 통신 및 처리하는 데 제한된 용량을 가지고 있기 때문에 중요합니다. 메모리에서 너무 많은 공간을 차지하지 않고 이 정보를 실용적인 방식으로 사용할 수 있는 충분한 세부 정보가 있어야 컴퓨터와 인간의 두뇌가 모두 문제를 해결할 수 있습니다.
이것을 상속 으로 다시 묶기 위해 여기에서 논의하는 세 가지 주요 주제 모두를 추상화 또는 추상화 메커니즘으로 사용할 수 있습니다. "Hello World" 앱의 레이아웃 파일에 ImageView
, Button
및 ImageButton
을 추가하기로 결정했다고 가정합니다.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageButton android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
또한 Activity가 클릭을 처리하기 위해 View.OnClickListener
를 구현했다고 가정합니다.
public class MainActivity extends Activity implements View.OnClickListener { private Button b; private ImageButton ib; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... b = findViewById(R.id.imvDisplay).setOnClickListener(this); ib = findViewById(R.id.btnDisplay).setOnClickListener(this); iv = findViewById(R.id.imbDisplay).setOnClickListener(this); } @Override public void onClick(View view) { final int id = view.getId(); //handle click based on id... } }
여기서 핵심 원칙은 Button
, ImageButton
및 ImageView
가 View
클래스에서 상속된다는 것입니다. 결과적으로 이 함수 onClick
은 덜 상세한 상위 클래스로 참조하여 이질적인(계층적으로 관련되어 있지만) UI 요소에서 클릭 이벤트를 수신할 수 있습니다. 이것은 Android 플랫폼 에서 모든 종류의 위젯을 처리하기 위한 고유한 메서드 를 작성해야 하는 것보다 훨씬 더 편리합니다(사용자 지정 위젯은 말할 것도 없고).
인터페이스와 추상화
내가 선택한 이유를 이해했더라도 이전 코드 예제가 약간 영감을 주지 못했다는 것을 알았을 수도 있습니다. 클래스 계층 구조에서 구현 을 공유할 수 있다는 것은 매우 유용하며 상속 의 주요 유틸리티라고 주장하고 싶습니다. 공통 부모 클래스 가 있는 클래스 집합을 동일한 유형 (즉, 부모 클래스 )으로 취급할 수 있도록 하는 것과 관련하여 상속 의 해당 기능은 사용이 제한적입니다.
제한적으로, 나는 자식 클래스가 부모 클래스를 통해 참조되거나 부모 클래스로 알려지기 위해 동일한 클래스 계층 구조 내에 있어야 한다는 요구 사항에 대해 말하고 있습니다. 즉, 상속 은 추상화 에 대한 매우 제한적인 메커니즘입니다. 사실, 추상화 가 다른 수준의 세부 사항(또는 정보) 사이를 이동하는 스펙트럼이라고 가정하면 상속 은 Java에서 추상화 를 위한 최소한의 추상 메커니즘이라고 말할 수 있습니다.
인터페이스 에 대한 논의를 진행하기 전에 Java 8 에서 Default Methods 와 Static Methods 라는 두 가지 기능이 인터페이스 에 추가되었음을 언급하고 싶습니다. 나는 그것들에 대해 나중에 논의하겠지만, 당분간은 그것들이 존재하지 않는 척 하고 싶습니다. 이것은 처음에 그리고 틀림없이 여전히 Java에서 추상화를 위한 가장 추상적인 메커니즘 인 인터페이스 사용의 주요 목적을 더 쉽게 설명할 수 있도록 하기 위한 것입니다.
더 적은 디테일은 더 많은 자유를 의미합니다
상속 에 대한 섹션에서 나는 구현 이라는 단어에 대한 정의를 제공했는데, 이는 이제 우리가 다룰 다른 용어와 대조를 이루기 위한 것입니다. 분명히 말해서, 나는 단어 자체에 대해 또는 당신이 그 사용에 동의하는지 여부에 신경 쓰지 않습니다. 그것들이 개념적으로 가리키는 것을 이해하기만 하면 됩니다.
상속 은 주로 클래스 집합에서 구현 을 공유하는 도구인 반면 인터페이스 는 기본적으로 클래스 집합에서 동작 을 공유하는 메커니즘이라고 말할 수 있습니다. 이러한 의미에서 사용되는 동작 은 추상 메서드 에 대한 비기술적 단어일 뿐입니다. 추상 메서드 는 실제로 메서드 본문 을 포함하지 않는 메서드 입니다.
public interface OnClickListener { void onClick(View v); }
나와 내가 지도한 많은 사람들의 자연스러운 반응은 인터페이스 를 처음 본 후 반환 유형 , 메서드 이름 및 매개 변수 목록 만 공유하는 것이 얼마나 유용한지 궁금해하는 것이었습니다. 표면적으로는 자신이나 인터페이스 를 implements
하는 클래스를 작성하는 다른 사람을 위해 추가 작업을 만드는 좋은 방법처럼 보입니다. 대답은 인터페이스 가 동일한 방식으로 동작 하는 클래스 집합을 원하지만(즉, 동일한 공개 추상 메서드 를 소유함) 해당 동작 을 다른 방식으로 구현 하기를 기대하는 상황에 완벽하다는 것입니다.
간단하지만 관련성이 높은 예를 들어, Android 플랫폼에는 주로 사용자 인터페이스의 일부를 생성하고 관리하는 업무인 Activity
및 Fragment
의 두 가지 클래스가 있습니다. 따라서 이러한 클래스는 위젯을 클릭할 때(또는 그렇지 않으면 사용자가 상호 작용할 때) 팝업되는 이벤트를 수신 대기해야 하는 경우가 많습니다. 논쟁을 위해 잠시 시간을 내어 상속 이 그러한 문제를 거의 해결하지 못하는 이유를 알아보겠습니다.
public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... } }
Activity와 Fragments 를 OnClickManager 에서 OnClickManager
하도록 하면 이벤트를 다른 방식으로 처리할 수 없을 뿐만 아니라 우리가 원하더라도 그렇게 할 수 없다는 것이 더 큰 문제입니다. Activity 와 Fragment 는 이미 부모 클래스 를 확장하고 Java는 여러 부모 클래스 를 허용하지 않습니다. 따라서 우리의 문제는 클래스 집합이 동일한 방식으로 동작 하기를 원하지만 클래스가 해당 동작 을 구현 하는 방법에 대한 유연성이 있어야 한다는 것입니다. 그러면 View.OnClickListener
의 이전 예제로 돌아갑니다.
public interface OnClickListener { void onClick(View v); }
이것은 실제 소스 코드( View
클래스에 중첩됨)이며 이 몇 줄을 사용하면 다양한 위젯( Views )과 UI 컨트롤러( Activity, Fragments 등 ) 간에 일관된 동작 을 보장할 수 있습니다.
추상화는 느슨한 결합을 촉진합니다.
Java에 인터페이스가 존재하는 이유에 대한 일반적인 질문에 희망적으로 답변했습니다. 다른 많은 언어 중에서. 한 가지 관점에서 볼 때 클래스 간에 코드를 공유하는 수단일 뿐이지만 다른 구현 을 허용하기 위해 의도적으로 덜 상세합니다. 그러나 상속 이 코드 공유와 추상화 를 위한 메커니즘으로 모두 사용될 수 있는 것처럼(클래스 계층 구조에 대한 제한이 있지만) 인터페이스 는 추상화 를 위한 보다 유연한 메커니즘을 제공합니다.
이 기사의 이전 섹션에서 나는 못 과 나사 를 사용하여 일종의 구조를 만드는 것의 차이점을 비유하여 느슨한/조임 연결 주제를 소개했습니다. 요약하자면, 기본 아이디어는 기존 구조 변경(실수 수정, 설계 변경 등의 결과일 수 있음)이 발생할 가능성이 있는 상황에서 나사 를 사용하려는 것입니다. 못 은 구조물의 일부를 함께 고정해야 하고 가까운 시일 내에 분리될 염려가 없을 때 사용하면 좋습니다.
못 과 나사 는 클래스 간의 구체적 및 추상 참조 ( 종속성 이라는 용어도 적용됨)와 유사합니다. 혼란이 없도록 다음 샘플에서 내가 의미하는 바를 보여줍니다.
class Client { private Validator validator; private INetworkAdapter networkAdapter; void sendNetworkRequest(String input){ if (validator.validateInput(input)) { try { networkAdapter.sendRequest(input); } catch (IOException e){ //handle exception } } } } class Validator { //...validation logic boolean validateInput(String input){ boolean isValid = true; //...change isValid to false based on validation logic return isValid; } } interface INetworkAdapter { //... void sendRequest(String input) throws IOException; }
여기에 두 종류의 참조 가 있는 Client
라는 클래스가 있습니다. Client
가 참조 생성과 관련이 없다고 가정하면(실제로는 하지 않아야 함) 특정 네트워크 어댑터의 구현 세부 정보에서 분리됩니다.
이 느슨한 결합 에는 몇 가지 중요한 의미가 있습니다. 우선 INetworkAdapter
구현 과 완전히 격리된 상태에서 Client
를 빌드할 수 있습니다. 두 명의 개발자로 구성된 팀에서 일하고 있다고 잠시 상상해 보십시오. 하나는 프론트 엔드를 빌드하고 하나는 백엔드를 빌드합니다. 두 개발자가 각각의 클래스를 함께 연결하는 인터페이스 를 계속 알고 있는 한, 서로 거의 독립적으로 작업을 계속할 수 있습니다.
둘째, 두 개발자가 각자의 구현 이 서로의 진행 상황과 무관하게 제대로 작동하는지 확인할 수 있다고 말하면 어떻게 될까요? 이것은 인터페이스를 사용하면 매우 쉽습니다. 적절한 인터페이스 를 implements
하는 Test Double 을 빌드하기만 하면 됩니다.
class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); } }
원칙적으로 관찰할 수 있는 것은 추상 참조 로 작업하면 모듈성, 테스트 가능성 및 Facade Pattern , Observer Pattern 등과 같은 매우 강력한 일부 디자인 패턴이 증가한다는 것입니다. 또한 개발자는 구현 세부 사항에 얽매이지 않고 동작 ( Program To An Interface )에 따라 시스템의 다른 부분을 설계하는 행복한 균형을 찾을 수 있습니다.
추상화에 대한 최종 요점
추상화 는 구체적인 것과 같은 방식으로 존재하지 않습니다. 이것은 추상 클래스 와 인터페이스 가 인스턴스화되지 않을 수 있다는 사실에 의해 Java 프로그래밍 언어에 반영됩니다.
예를 들어 다음은 확실히 컴파일되지 않습니다.
public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { //ERROR x2: Foo f = new Foo(); Bar b = new Bar() } private abstract class Foo{} private interface Bar{} }
사실, 구현되지 않은 인터페이스 나 추상 클래스 가 런타임에 작동할 것으로 기대한다는 생각은 UPS 유니폼이 패키지를 배달할 때 떠다니기를 기대하는 것만큼이나 의미가 있습니다. 유용하려면 추상화 뒤에 구체적인 무언가가 있어야 합니다. 호출 클래스가 실제로 추상 참조 뒤에 무엇이 있는지 알 필요가 없더라도.
추상 수업: 모든 것을 합치기
여기까지 했다면 더 이상 번역할 철학적 접선이나 전문 용어가 없다는 것을 알려드리게 되어 기쁩니다. 간단히 말해서 추상 클래스 는 클래스 집합에서 구현 및 동작 을 공유하기 위한 메커니즘입니다. 이제 추상 클래스 를 자주 사용하지 않는다는 사실을 바로 인정하겠습니다. 그럼에도 불구하고 이 섹션이 끝날 때쯤에는 그들이 언제 요청을 받았는지 정확히 알게 되기를 바랍니다.
운동 기록 사례 연구
Java로 Android 앱을 빌드한 지 약 1년 동안 저는 처음부터 Android 앱을 처음부터 다시 빌드하고 있었습니다. 첫 번째 버전은 거의 지침 없이 독학으로 개발한 개발자에게 기대할 수 있는 엄청난 양의 코드였습니다. 내가 새로운 기능을 추가하고 싶을 즈음에, 내가 전적으로 못 으로 지은 단단히 결합된 구조가 유지 관리가 불가능하여 완전히 다시 만들어야 한다는 것이 분명해졌습니다.
이 앱은 운동을 쉽게 기록할 수 있도록 설계된 운동 로그와 과거 운동 데이터를 텍스트 또는 이미지 파일로 출력할 수 있는 기능이었습니다. 너무 자세히 설명하지 않고 앱의 데이터 모델을 구성하여 Exercise
개체 모음으로 구성된 Workout
개체가 있도록 했습니다(이 논의와 관련이 없는 다른 필드 중에서).
운동 데이터를 일종의 시각적 매체로 출력하는 기능을 구현하면서 문제를 해결해야 한다는 것을 깨달았습니다. 운동의 종류에 따라 다른 종류의 텍스트 출력이 필요합니다.
대략적인 아이디어를 제공하기 위해 다음과 같이 운동 유형에 따라 출력을 변경하고 싶었습니다.
- 바벨: 10회 반복 @ 100LBS
- 덤벨: 10회 반복 @ 50LBS x2
- 체중: 10 REPS @ 체중
- 체중 +: 10 REPS @ 체중 + 45 LBS
- 시간: 60초 @ 100LBS
계속하기 전에 다른 유형이 있었고(운동이 복잡해질 수 있음) 내가 보여줄 코드는 기사에 잘 맞도록 잘리고 변경되었다는 점에 유의하십시오.
이전의 정의에 따라 추상 클래스를 작성하는 목표는 추상 클래스 의 모든 자식 클래스 에서 공유되는 모든 것( 변수 및 상수 와 같은 상태 포함)을 구현하는 것입니다. 그런 다음 해당 자식 클래스 에서 변경되는 모든 것에 대해 추상 메서드 를 만듭니다.
abstract class Exercise { private final String type; protected final String name; protected final int[] repetitionsOrTime; protected final double[] weight; protected static final String POUNDS = "LBS"; protected static final String SECONDS = "SEC "; protected static final String REPETITIONS = "REPS "; public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) { this.type = type; this.name = name; this.repetitionsOrTime = repetitionsOrTime; this.weight = weight; } public String getFormattedOutput(){ StringBuilder sb = new StringBuilder(); sb.append(name); sb.append("\n"); getSetData(sb); sb.append("\n"); return sb.toString(); } /** * Append data appropriately based on Exercise type * @param sb - StringBuilder to Append data to */ protected abstract void getSetData(StringBuilder sb); //...Getters }
나는 분명히 말할 수 있지만 추상 클래스 에서 구현해야 하는 것과 구현 하지 말아야 하는 것에 대해 질문이 있는 경우 핵심은 모든 자식 클래스에서 반복된 구현 부분을 살펴보는 것입니다.
이제 모든 연습에서 공통적인 사항을 설정했으므로 각 종류의 문자열 출력에 대한 전문화를 사용하여 자식 클래스를 만들 수 있습니다.
바벨 운동:
class BarbellExercise extends Exercise { public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append("\n"); } } }
덤벨 운동:
class DumbbellExercise extends Exercise { private static final String TIMES_TWO = "x2"; public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append(TIMES_TWO); sb.append("\n"); } } }
체중 운동:
class BodyweightExercise extends Exercise { private static final String BODYWEIGHT = "Bodyweight"; public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(BODYWEIGHT); sb.append("\n"); } } }
나는 기민한 독자들이 더 효율적인 방식으로 추상화 할 수 있었던 것들을 찾을 것이라고 확신하지만, 이 예제의 목적(원래 소스에서 단순화된)은 일반적인 접근 방식을 보여주기 위한 것입니다. 물론, 실행할 수 있는 것이 없으면 프로그래밍 기사가 완성될 수 없습니다. 테스트하려는 경우 이 코드를 실행하는 데 사용할 수 있는 여러 온라인 Java 컴파일러가 있습니다(이미 IDE가 없는 경우).
public class Main { public static void main(String[] args) { //Note: I actually used another nested class called a "Set" instead of an Array //to represent each Set of an Exercise. int[] reps = {10, 10, 8}; double[] weight = {70.0, 70.0, 70.0}; Exercise e1 = new BarbellExercise( "Barbell", "Barbell Bench Press", reps, weight ); Exercise e2 = new DumbbellExercise( "Dumbbell", "Dumbbell Bench Press", reps, weight ); Exercise e3 = new BodyweightExercise( "Bodyweight", "Push Up", reps, weight ); System.out.println( e1.getFormattedOutput() + e2.getFormattedOutput() + e3.getFormattedOutput() ); } }
Executing this toy application yields the following output: Barbell Bench Press
10 REPS @ 70.0LBS 10 REPS @ 70.0LBS 8 REPS @ 70.0LBS Dumbbell Bench Press 10 REPS @ 70.0LBSx2 10 REPS @ 70.0LBSx2 8 REPS @ 70.0LBSx2 Push Up 10 REPS @ Bodyweight 10 REPS @ Bodyweight 8 REPS @ Bodyweight
추가 고려 사항
Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementation , as opposed to behavior . These features are known as Default Methods and Static Methods .
I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.
I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods ) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance . A common pattern in Java is to write what is known as a Utility class, which is a simple class
containing the requisite implementation in a static method :
public class TimeConverterUtil { /** * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate * format such as 12, 30 -> 12:30 pm */ public static String convertTime (int hour, int minute){ String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute); DateFormat f1 = new SimpleDateFormat("HH:mm"); Date d = null; try { d = f1.parse(unformattedTime); } catch (ParseException e) { e.printStackTrace(); } DateFormat f2 = new SimpleDateFormat("h:mm a"); return f2.format(d).toLowerCase(); } }
Using this static method in an external class (or another static method ) looks like this:
public class Main { public static void main(String[] args){ //... String time = TimeConverterUtil.convertTime(12, 30); //... } }
컨닝 지
We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.
I Want A Set Of Child Classes To Share Implementation
Classic inheritance , which requires a child class to inherit from a parent class , is a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent class , is to see whether it is repeated in a number of different classes line for line. The acronym DRY ( Don't Repeat Yourself ) is a good mnemonic device to watch out for this situation.
While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent class , which provides a limited degree of abstraction .
I Want A Set Of Classes To Share Behavior
Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior ), but you do not expect the implementation of that behavior to be repeated across inheritors.
By definition, Java interfaces may not contain any implementation (except for Default and Static Methods ), but any class which implements an interface , must supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy .
I Want A Set Of Child Classes To Share Behavior And Implementation
Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract class
, and anything which requires flexibility may be specified as an abstract method .