<캡슐화 코드예시>
// 하나의 소스파일에는 public class 또는 public interface 중 하나만 존재해야함.
// 인터페이스 정의
interface Shape { // <인터페이스 제공>
double getArea(); // 도형의 넓이를 계산하는 메서드
double getPerimeter(); // 도형의 둘레를 계산하는 메서드
}
// Circle 클래스가 Shape 인터페이스를 구현
class Circle implements Shape {
private double radius; // <데이터 은닉>
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public double getPerimeter() {
return 2 * Math.PI * radius;
}
}
// Rectangle 클래스가 Shape 인터페이스를 구현
class Rectangle implements Shape {
private double width; // _데이터 은닉
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
@Override
public double getPerimeter() {
return 2 * (width + height);
}
}
public class Main {
public static void main(String[] args) {
// Circle 객체 생성 및 사용
Circle circle = new Circle(5.0);
System.out.println("Circle Area: " + circle.getArea());
System.out.println("Circle Perimeter: " + circle.getPerimeter());
// Rectangle 객체 생성 및 사용
Rectangle rectangle = new Rectangle(4.0, 6.0);
System.out.println("Rectangle Area: " + rectangle.getArea());
System.out.println("Rectangle Perimeter: " + rectangle.getPerimeter());
}
}
<상속 코드예시>
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " makes a sound");
}
}
class Dog extends Animal {
public Dog(String name) {
super(name); // 슈퍼 클래스의 생성자 호출
}
// 슈퍼 클래스의 메서드를 재정의
@Override
public void speak() {
System.out.println(name + " barks");
}
// 메서드 확장
public void wagTail() {
System.out.println(name + " wags its tail");
}
}
❔ 컴포지션(Composition) : 객체 지향 프로그래밍의 디자인 원칙 중 하나.
한 클래스가 다른 클래스의 객체를 포함하여 그 기능을 확장하는 방식.
즉, 한 클래스가 다른 클래스를 소유함.
한 클래스의 기능을 다른 클래스에서 재사용할 수 있음.
클래스들이 서로 독립적이며 변경에 따른 영향이 적어 시스템의 유연성과 확장성이 증가하여 낮은 결합도를 가짐.
객체의 관계가 명확해져 코드 이해와 유지보수가 용이함.
public class Engine { // 'Engine 클래스를 정의. 이 클래스는 엔진과 관련된 기능을 가지고 있음.
public void start() {
System.out.println("엔진이 시작됩니다.");
}
public void stop() {
System.out.println("엔진이 멈춥니다.");
}
}
//--------------------------------------------------------------
public class Car {
private Engine engine; // Engine 객체를 멤버 변수로 포함
public Car() {
engine = new Engine(); // Car 객체가 생성될 때 Engine 객체도 생성
}
public void startCar() {
engine.start(); // Engine 클래스의 start 메서드를 호출
System.out.println("자동차가 출발합니다.");
}
public void stopCar() {
engine.stop(); // Engine 클래스의 stop 메서드를 호출
System.out.println("자동차가 멈춥니다.");
}
}
//--------------------------------------------------------------
public class Main {
public static void main(String[] args) {
Car myCar = new Car(); // Car 객체 생성
myCar.startCar(); // 자동차를 출발시킵니다.
myCar.stopCar(); // 자동차를 멈춥니다.
}
}
List<Parent> parent = new ArrayList<>();
parent.add(new Child1());
parent.add(new Child2());
parent.add(new Child3());
List<Child1> child1
List<Child2> child2
List<Child3> child3
<다형성 코드예시>
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
void wagTail() {
System.out.println("Dog wags its tail");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows");
}
void purr() {
System.out.println("Cat purrs");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 다형성: 부모 클래스 타입으로 자식 클래스 객체 생성
myAnimal.makeSound(); // 다형성을 통해 Dog의 makeSound() 메서드 호출
// wagTail() 메서드를 호출하려면 Dog 타입으로 명시적으로 캐스팅 해야함.
// Dog myDog = (Dog) myAnimal;
myAnimal = new Cat(); // 다형성: 부모 클래스 타입으로 다른 자식 클래스 객체 생성
myAnimal.makeSound(); // 다형성을 통해 Cat의 makeSound() 메서드 호출
}
}
상속은 계층적 구조를 가지고 있으며 단일상속이다. (extends)
확장성은 좋지만 부모에게 필요없는 멤버를 물려받을 수 있다.
부모의 메서드를 자식이 재정의하는 오버라이딩이 가능하며 자식에서 부모의 멤버를 사용할 수 있다.
인터페이스는 추상적이며 메서드의 선언부만 있고 구현부는 없으며 다중구현이다. (implements)
인터페이스에 있는 것들은 이를 사용하는 클래스에서 반드시 구현해야 한다는 강제성이 있다.
반드시 필요한 것들만 정의하여 사용하기 때문에 필요없는 코드가 존재하지 않는다.
상속보다 결합도가 낮다.
<추상화 예시코드>
// 추상 클래스: Shape
abstract class Shape {
// 멤버 변수는 없음
// 추상 메서드: 도형의 넓이를 계산하는 메서드
abstract double calculateArea();
}
// 구체적인 도형 클래스: Circle
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double calculateArea() {
return Math.PI * radius * radius;
}
}
// 구체적인 도형 클래스: Rectangle
class Rectangle extends Shape {
double length;
double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double calculateArea() {
return length * width;
}
}
public class AbstractionExample {
public static void main(String[] args) {
// Shape 타입의 배열을 생성하고 다양한 도형 객체를 저장
Shape[] shapes = new Shape[2];
shapes[0] = new Circle(5.0);
shapes[1] = new Rectangle(4.0, 3.0);
// 배열을 순회하면서 각 도형의 넓이를 출력
for (Shape shape : shapes) {
System.out.println("도형의 넓이: " + shape.calculateArea());
}
}
}
각 클래스는 하나의 책임만을 가져야 한다.
책임의 정의 : 클래스가 수행해야 하는 기능이나 역할.
변경의 이유 : 클래스를 변경해야 하는 이유는 단 하나여야 함. 여러 이유로 인해 클래스가 변경되어야 한다면 SRP를 위반하는 것.
즉, 클래스가 변경되어야 하는 이유는 하나이어야 하며 하나의 기능이나 역할에만 집중해야 한다.
소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만, 수정에 대해서는 닫혀 있어야 한다.
개방성 : 소프트웨어의 행동은 새로운 기능이나 요구사항이 생겼을 때 확장할 수 있어야 함.
(추상화, 다형성)
폐쇠성 : 기존의 코드는 새로운 기능을 추가할 때 수정되지 않아야 한다.
즉, 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있어야 한다.
/*전략 인터페이스 정의*/
// 로깅 전략에 대한 인터페이스
public interface LoggingStrategy {
void log(String message);
}
//--------------------------------------------------------------
/*구체적인 전략 구현*/
// 콘솔에 로그를 출력하는 전략
public class ConsoleLogging implements LoggingStrategy {
@Override
public void log(String message) {
System.out.println("Console: " + message);
}
}
// 파일에 로그를 기록하는 전략
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class FileLogging implements LoggingStrategy {
@Override
public void log(String message) {
try (PrintWriter out = new PrintWriter(new FileWriter("log.txt", true))) {
out.println("File: " + message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//--------------------------------------------------------------
/*컨텍스트 클래스 구현*/
// 로거 클래스는 로깅 전략을 사용합니다.
public class Logger {
private LoggingStrategy strategy;
public Logger(LoggingStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(LoggingStrategy strategy) {
this.strategy = strategy;
}
public void log(String message) {
strategy.log(message);
}
}
//--------------------------------------------------------------
/*사용 예시*/
public class Main {
public static void main(String[] args) {
// 초기 전략을 콘솔 로깅으로 설정
Logger logger = new Logger(new ConsoleLogging());
// 콘솔에 로그를 남깁니다.
logger.log("First log entry");
// 로깅 전략을 파일 로깅으로 변경
logger.setStrategy(new FileLogging());
// 파일에 로그를 남깁니다.
logger.log("Second log entry");
}
}
// 처음에는 ConsoleLogging 전략을 사용하지만, 실행 중에 FileLogging 전략으로 변경 가능.
// 이 방식은 실행 중에 객체의 행동을 변경할 수 있게 하여, OCP 원칙을 준수.
/*추상 클래스 (템플릿 메서드 정의)*/
public abstract class DataProcessor {
// 템플릿 메서드
public final void process() {
readData();
processData();
writeData();
}
// 기본적인 동작을 정의
public void readData() {
System.out.println("Reading data...");
}
// processData는 서브클래스에서 구현해야 하는 추상 메서드
public abstract void processData();
// 기본적인 동작을 정의
public void writeData() {
System.out.println("Writing data...");
}
}
//--------------------------------------------------------------
/*구체적인 자식 클래스 구현*/
// JSON 데이터를 처리하는 서브클래스
public class JsonDataProcessor extends DataProcessor {
@Override
public void processData() {
System.out.println("Processing JSON data...");
}
}
// XML 데이터를 처리하는 서브클래스
public class XmlDataProcessor extends DataProcessor {
@Override
public void processData() {
System.out.println("Processing XML data...");
}
}
//--------------------------------------------------------------
/*사용 예시*/
public class Main {
public static void main(String[] args) {
DataProcessor jsonProcessor = new JsonDataProcessor();
DataProcessor xmlProcessor = new XmlDataProcessor();
System.out.println("JSON Processor:");
jsonProcessor.process();
System.out.println("\nXML Processor:");
xmlProcessor.process();
}
}
// JsonDataProcessor와 XmlDataProcessor 클래스는 이 processData()를 각각 JSON과 XML 데이터 처리 방식으로 구현.
// DataProcessor 클래스의 process() 메서드는 알고리즘의 구조를 정의, 실제 데이터 처리 방식은 자식 클래스에서 결정.
// 이 방식은 템플릿 메서드 패턴의 전형적인 사용 예시이며, 코드 재사용과 유연성을 증가.
자식 클래스는 언제나 그들의 기반 클래스로 대체될 수 있어야 한다.
즉, 프로그램에서 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대체해도 프로그램의 정확성이 깨지지 않아야 한다.
/*부모 클래스 (Animal) _ setAge 메서드가 모든 양수의 나이를 허용*/
public class Animal {
protected int age;
public void setAge(int age) {
if (age > 0) {
this.age = age;
} else {
throw new IllegalArgumentException("Age must be positive");
}
}
}
//--------------------------------------------------------------
/*자식 클래스 (Dog) _ setAge 메서드를 오버라이딩하여 나이가 20보다 작아야 한다는 더 엄격한 조건을 요구*/
public class Dog extends Animal {
@Override
public void setAge(int age) {
if (age > 0 && age < 20) {
this.age = age;
} else {
throw new IllegalArgumentException("Age must be between 1 and 19");
}
}
}
// Dog 클래스는 Animal 클래스보다 더 엄격한 입력 조건을 가지므로 LSP를 위반.
// Animal 객체를 Dog 객체로 대체하려고 할 때, Animal 클래스에 허용되는 나이가 Dog 클래스에서 허용되지 않을 수 있음.
// 기존의 Animal 객체를 사용하던 코드가 Dog 객체로 대체되었을 때 정상적으로 작동하지 않을 수 있음.
/*부모 클래스 _ withdraw 메서드를 가지고 있으며, 출금이 성공하면 true를 반환*/
public class BankAccount {
protected double balance;
public BankAccount(double balance) {
this.balance = balance;
}
public boolean withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}
}
//--------------------------------------------------------------
/*자식 클래스 _ withdraw 메서드를 오버라이딩하지만, 부모 클래스와 달리 출금 시도가 성공하더라도 항상 false를 반환*/
public class LimitedBankAccount extends BankAccount {
public LimitedBankAccount(double balance) {
super(balance);
}
@Override
public boolean withdraw(double amount) {
super.withdraw(amount);
return false; // 항상 false 반환, 부모 클래스의 동작을 약화시킴
}
}
// LimitedBankAccount 클래스는 BankAccount의 withdraw 메서드를 오버라이딩하지만, 부모 클래스에서 정의된 기능을 약화.
// BankAccount의 인스턴스를 LimitedBankAccount의 인스턴스로 대체하면, 기존 코드에서 예상한 출금 성공 여부의 확인이 잘못됨.
// 이는 소프트웨어의 예측 가능성과 신뢰성을 저하.
❔ 메서드의 사전조건 : 메서드나 함수가 실행되기 전에 충족되어야 하는 조건. 상태나 매개변수 조건을 명시.
ex) 특정 매개변수가 null이 아니어야 함, 특정 숫자가 양수이어야 함.
❔ 메서드의 사후조건 : 메서드나 함수가 실행이 완료된 후에 만족되어야 하는 조건.
ex) 메서드가 데이터베이스에서 레코드를 삭제한 후 그 레코드가 더 이상 존재하지 않아야 함.
클라이언트는 자신이 사용하지 않는 인터페이스를 구현하도록 강요받지 않아야 한다.
이를 방지하기 위해 ISP는 인터페이스를 더 작고, 구체적인 목적에 맞게 분리할 것을 권장.
고수준 모듈은 저수준 모듈에 의존해서는 안된다.
인터페이스는 구체적인 구현에 의존하지 않아야 하며 구체적인 구현이 인터페이스에 의존해야한다.
즉, 상세한 구현보다는 인터페이스나 추상 클래스에 의존해야 합니다.
❔ 고수준 모듈 : 비즈니스 로직을 담당하는 모듈
❔ 저수준 모둘 : 데이터베이스, 네트워크, 파일 시스템과 같은 세부적인 작업을 담당하는 모듈
/*저수준 모듈: 구체적인 이메일 서비스 구현*/
public class EmailService {
public void sendEmail(String message, String receiver) {
// 이메일을 보내는 로직 구현
System.out.println("Email sent to " + receiver + ": " + message);
}
}
//--------------------------------------------------------------
/*고수준 모듈: 고객 관리 시스템*/
public class CustomerService {
private EmailService emailService;
public CustomerService() {
// 고수준 모듈이 저수준 모듈의 구체적인 구현에 직접 의존
this.emailService = new EmailService();
}
public void notifyCustomer(String message, String email) {
// 고객에게 이메일을 보냄
emailService.sendEmail(message, email);
}
}
// CustomerService 클래스(고수준 모듈)가 EmailService 클래스(저수준 모듈)의 구체적인 구현에 직접적으로 의존.
// 이로 인해 EmailService의 구현이 변경되면 CustomerService도 영향을 받을 수 있음.
// EmailService를 다른 서비스로 교체하거나, 다른 방식으로 이메일을 보내야 할 경우, CustomerService 클래스의 변경이 불가피.
// 이 설계는 시스템의 유연성과 확장성을 제한하며, 테스트를 더 어렵게 만듬.
/*인터페이스 정의*/
public interface EmailService {
void sendEmail(String message, String receiver);
}
//--------------------------------------------------------------
/*인터페이스의 구체적인 서비스 구현*/
public class SimpleEmailService implements EmailService {
@Override
public void sendEmail(String message, String receiver) {
// 실제 이메일 전송 로직
System.out.println("Sending email to " + receiver + ": " + message);
}
}
//--------------------------------------------------------------
/*고수준 모듈: 의존성 주입을 사용*/
// 고수준 모듈인 CustomerService는 EmailService의 구체적인 구현 대신 인터페이스에 의존
public class CustomerService {
private EmailService emailService;
// 생성자를 통한 의존성 주입
public CustomerService(EmailService emailService) {
this.emailService = emailService;
}
public void notifyCustomer(String message, String email) {
emailService.sendEmail(message, email);
}
}
//--------------------------------------------------------------
/*사용 예시*/
// CustomerService 객체를 생성할 때, 필요한 EmailService 구현체(SimpleEmailService)를 외부에서 주입
public class Main {
public static void main(String[] args) {
EmailService emailService = new SimpleEmailService();
CustomerService customerService = new CustomerService(emailService);
customerService.notifyCustomer("Welcome to our service!", "customer@example.com");
}
}