콜라문제에 대한 알고리즘을 풀었다. 그렇게 어렵진 않았지만 어제 새벽까지 작업하느라 머리가 제대로 굴러가지 않았다. 타인들의 풀이를 보니
return (n > b ? n - b : 0) / (a - b) * b;한줄로 완성한 로직이 있었는데 오.. 이게 왜 되지 싶더라 저걸 어떻게 생각한건지 궁금해진다.
n-b / a-b * b 기가막히다. 역시 수학을 잘하면 코드가 깔끔해지는거 같다.
답을 알고도 왜 저게 되는지 제대로 이해가 되지 않는다.
영상랑크
gitgubsource
며칠간 콘솔로 다양한 기능들을 집어넣으려 무수한 노력을 했었고 원하는 기능은 대부분 넣은거 같다
완성도를 높히기 위해서 파일시스템 까지 넣으면 좋겠지만, 콘솔UI로 파일시스템 까지 넣는건 굉장히 고되다고 생각되어서 그만두었다. 시간이 남았다면 IM_GUI를 활용해서 무언가 더 만들어 볼 수 있었을거 같으나. 마감 임박시간이 다가왔으니 이정도로 만족하는게 좋겠다.
마지막으로 검색시스템을 개선했다. 이스케이프 문자가 들어가면 제대로 검색이 되지 않기 때문에 기능을 수정하였다.
응집도는 클래스 내 모듈들이 얼마나 관련되어 있는지를 나타낸다.
응집도가 높으면 관련있는 모듈들만 모여있기 때문에 효율적인 코드가 된다.
필요한 기능들만 모아서 구성하는 정도라고 생각하면 될 거 같다.
결합도는 각 모듈들의 서로 얼마나 의존하는지 나타낸다.
즉 결합도는 낮은게 좋다. 서로 의존도가 높다면 구성변경시 모두 변경해 주어야 하기 때문이다.
#include <iostream>
#include <string>
using namespace std;
class Engine {
public:
string state;
Engine() : state("off") {}
void start() {
state = "on";
}
};
class Car {
public:
Engine engine; // Car 클래스가 Engine 클래스에 강하게 의존
void startCar() {
if (engine.state == "off") {
engine.start();
cout << "Car started" << endl;
}
}
};
int main() {
Car myCar;
myCar.startCar();
// Car에서 ElectricEngine을 사용하려면 전체 구조를 다시 수정해야 함
ElectricEngine electricEngine;
if (electricEngine.state == "off") {
electricEngine.start();
cout << "Electric car started" << endl;
}
return 0;
}
위의 코드는 의존도가 높기 떄문에 변경이 발생할 경우 모든 코드를 수정을 해야한다.
아래 코드는 객체를 활용하는 예제이다.
#include <iostream>
#include <memory>
using namespace std;
// 공통 인터페이스 정의
class Engine {
public:
virtual void start() = 0;
virtual ~Engine() = default;
};
// DieselEngine 구현
class DieselEngine : public Engine {
public:
void start() {
cout << "Diesel Engine started" << endl;
}
};
// 새로운 ElectricEngine 구현
class ElectricEngine : public Engine {
public:
void start() {
cout << "Electric Engine started silently" << endl;
}
};
// Car 클래스는 Engine 인터페이스에만 의존
class Car {
private:
unique_ptr<Engine> engine;
public:
Car(unique_ptr<Engine> eng) : engine(move(eng)) {}
void startCar() {
engine->start();
cout << "Car started" << endl;
}
};
int main() {
// DieselEngine을 사용하는 경우
auto dieselEngine = make_unique<DieselEngine>();
Car dieselCar(move(dieselEngine));
dieselCar.startCar();
// ElectricEngine을 사용하는 경우
auto electricEngine = make_unique<ElectricEngine>();
Car electricCar(move(electricEngine));
electricCar.startCar();
return 0;
}
원하는 대로 구현된 코드를 수정을 할 수 있기 때문에 유연하다
응집도는 높고 결합도는 낮아야 좋은 코드이다.
객체지향을 위한 원칙이다.
유지보수와 확장성을 목적으로 한 5가지 원칙을 이야기 한다.단일 원칙 책임(SRP)
하나의 클래스는 너무 많은 기능을 담당하면 안된다.
200줄이 넘어가면 가독성이 나빠지고 유지보수가 어렵게 되기 때문에
클래스의 역할과 책임을 분리해서 변경이 필요한 경우에만 수정이 되도록 하는 것이다.
아래는 제대로 원칙이 지켜지지 않은 예시이다.
#include <iostream>
#include <string>
class Student {
public:
void setName(const std::string& name) {
this->name = name;
}
void displayDetails() {
std::cout << "Student Name: " << name << std::endl;
}
void calculateGrade(int score) {
if (score >= 90) {
std::cout << "Grade: A" << std::endl;
} else if (score >= 80) {
std::cout << "Grade: B" << std::endl;
} else {
std::cout << "Grade: C" << std::endl;
}
}
private:
std::string name;
};
아래는 단일 책임 원칙이 적용된 예시이다.
#include <iostream>
#include <string>
// 학생 정보 관리 클래스
class Student {
public:
void setName(const std::string& name) {
this->name = name;
}
std::string getName() const {
return name;
}
private:
std::string name;
};
// 성적 계산 클래스
class GradeCalculator {
public:
void calculateGrade(int score) {
if (score >= 90) {
std::cout << "Grade: A" << std::endl;
} else if (score >= 80) {
std::cout << "Grade: B" << std::endl;
} else {
std::cout << "Grade: C" << std::endl;
}
}
};
// 출력 클래스
class StudentPrinter {
public:
void displayDetails(const Student& student) {
std::cout << "Student Name: " << student.getName() << std::endl;
}
};
확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 하는 원칙이다.
기존 코드를 변경하지 않고, 새로운 기능을 추갛라 수 있도록 하는게 목적이다.
아래는 적용이 되지 않은 예시이다.
class ShapeManager {
public:
void drawShape(int shapeType) {
if (shapeType == 1) {
// 원 그리기
} else if (shapeType == 2) {
// 사각형 그리기
}
}
};
타입을 인자로 받아서 기능을 수행한다.
수정에 열려있고 확장에는 닫혀있기 때문에 원칙을 지키지 못한 코드이다.
아래는 위의 코드를 확장에 유연하게 수정한 버젼이다.
class Shape {
public:
virtual void draw() = 0; // 순수 가상 함수
};
class Circle : public Shape {
public:
void draw() {
// 원 그리기
}
};
class Square : public Shape {
public:
void draw() {
// 사각형 그리기
}
};
class ShapeManager {
public:
void drawShape(Shape& shape) {
shape.draw(); // 다형성 활용
}
};
자식 클래스는 부모클래스에서 기대되는 행동을 보장해야 한다.
이것을 '다형성'이라고 하는데 인터페이스를 하나 만들고 자식클래스에서 구현하는 방식이다.
다형성이 동작하려면, 부모클래스 인터페이스와 잘 맞도록 구현이 되어야 한다.
#include <iostream>
class Rectangle {
public:
virtual void setWidth(int w) { width = w; }
virtual void setHeight(int h) { height = h; }
int getWidth() const { return width; }
int getHeight() const { return height; }
int getArea() const { return width * height; }
private:
int width = 0;
int height = 0;
};
class Square : public Rectangle {
public:
void setWidth(int w) override {
Rectangle::setWidth(w);
Rectangle::setHeight(w); // 정사각형은 너비와 높이가 같아야 함
}
void setHeight(int h) override {
Rectangle::setHeight(h);
Rectangle::setWidth(h); // 정사각형은 너비와 높이가 같아야 함
}
};
void testRectangle(Rectangle& rect) {
rect.setWidth(5);
rect.setHeight(10);
std::cout << "Expected area: 50, Actual area: " << rect.getArea() << std::endl;
}
int main() {
Rectangle rect;
testRectangle(rect); // Expected area: 50
Square square;
testRectangle(square); // Expected area: 50, Actual area: 100 (문제 발생)
return 0;
}
위의 상속예제는 부모의 기능을 모두 사용하지 못한다.
아래는 리스코프치환이 적용된 예제이다. 넓이를 구하는 가상함수를 상속을 을 받아서 도형에서 재정의를 하면서 어떤 객체에서 상속을 받아도 해당 함수를 실행하면 동작할 수 있게끔 구조를 변경한 것이다.
#include <iostream>
class Shape {
public:
virtual int getArea() const = 0; // 넓이를 계산하는 순수 가상 함수
};
class Rectangle : public Shape {
public:
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int getWidth() const { return width; }
int getHeight() const { return height; }
int getArea() const override { return width * height; }
private:
int width = 0;
int height = 0;
};
class Square : public Shape {
public:
void setSide(int s) { side = s; }
int getSide() const { return side; }
int getArea() const override { return side * side; }
private:
int side = 0;
};
void testShape(Shape& shape) {
std::cout << "Area: " << shape.getArea() << std::endl;
}
int main() {
Rectangle rect;
rect.setWidth(5);
rect.setHeight(10);
testShape(rect); // Area: 50
Square square;
square.setSide(7);
testShape(square); // Area: 49
return 0;
}
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.
정의가 어렵긴 하지만 각 불필요한 메서드를 구현하지 않는다는 것이다.
아래는 분리가 안된 것의 예시이다.
class Machnine {
private:
public:
Machnine() {}
void print() {
//세부 기능 구현
}
void scan() {
//세부 기능 구현
}
};
한 class가 상반된 두가지 기능을 담당하고 있다 단일 책임 원칙에도 어긋나는 코드다
따라서 분리를 해서 구현을 해야 한다.
class Printer {
public:
virtual void print() = 0;
};
class Scanner {
public:
virtual void scan() = 0;
};
class BasicPrinter : public Printer {
public:
void print() override {
// 문서 출력
}
};
class MultiFunctionDevice {
private:
Printer* printer;
Scanner* scanner;
public:
MultiFunctionDevice(Printer* p, Scanner* s) : printer(p), scanner(s) {}
void print() {
if (printer) printer->print();
}
void scan() {
if (scanner) scanner->scan();
}
};
여러 부품들이 있고 확장된 부품을 받아서 기능을 확장하는 방식으로 구현된 예제이다.
고수준 모듈은 저수준 모듈에 의존하지 않고 둘 다 추상화에 의존해야 한다.
고수준 모듈(다른 클래스나 모듈을 사용하는 사용자 역할)은 저수준 모듈(구체적인 작업을 처리하는 세부사항을 담은 클래스)에 의존하면 좋은 설계가 아니다.
class Keyboard {
public:
std::string getInput() {
return "입력 데이터";
}
};
class Monitor {
public:
void display(const std::string& data) {
// 출력
}
};
class Computer {
Keyboard keyboard;
Monitor monitor;
public:
void operate() {
std::string input = keyboard.getInput();
monitor.display(input);
}
};
class InputDevice {
public:
virtual std::string getInput() = 0;
};
class OutputDevice {
public:
virtual void display(const std::string& data) = 0;
};
class Keyboard : public InputDevice {
public:
std::string getInput() override {
return "키보드 입력 데이터";
}
};
class Monitor : public OutputDevice {
public:
void display(const std::string& data) override {
// 화면에 출력
}
};
class Computer {
private:
InputDevice* inputDevice;
OutputDevice* outputDevice;
public:
Computer(InputDevice* input, OutputDevice* output)
: inputDevice(input), outputDevice(output) {}
void operate() {
std::string data = inputDevice->getInput();
outputDevice->display(data);
}
};