컴퓨터/정보처리기사 SW 공학

| 니앙팽이 - 객체지향(OOP) | 4-0 | SOLID 원칙

객체지향

📕 4. 객체지향 디자인 패턴

유니티에서 사용하면 좋을 디자인 패턴만 명시한다.


📄 0. SOLID Principle

1). SRP (Single Responsibility Principle)

ⓐ 단일 책임 원칙

  • 클만변

    1. 클래스 만들때
      • 걔는 단 하나의 책임을 가져야한다
    2. 클래스 변경할때
      • 걔는 단 한가지 이유가 있어야한다
  • 높은 응집력과 낮은 결합

ⓒ 예시

📂SRP BAD 🤪📂
class BlogPost
{
    private Author author;
    private string title;
    private string content;
    private DateTime date;
    // ..
    public Dictionary getData()
    {
        Dictionary<string, string> ret = new Dictionary<string, string>();
        ret.Add("author", this.author.fullName());
        ret.Add("title", this.title);
        ret.Add("content", this.content);
        ret.Add("timestamp", this.date.getTimestamp());
        return ret;
    }

    public String printJson() { return json_encode(getData()); }

    public String printString()
    {
        return $"{this.title} {this.date.setFormat("Y-m-d H:i:s")} {this.author.fullName()} {this.content}";
    }
}

//여기서 주요 문제는 
//필요한 경우 다양한 형식, json, html 등으로 인쇄를 담당한다는 것입니다. 
//그럼 이것이 어떻게 개선될 수 있는지 봅시다.
📂SRP GOOD 😎📂
class BlogPost
{
    private Author author;
    private string title;
    private string content;
    private DateTime date;
    // ..
    public Dictionary getData()
    {
        Dictionary<string, string> ret = new Dictionary<string, string>();
        ret.Add("author", this.author.fullName());
        ret.Add("title", this.title);
        ret.Add("content", this.content);
        ret.Add("timestamp", this.date.getTimestamp());
        return ret;
    }
}

//BlogPost 클래스에서 인쇄 방법을 제거
interface IPrintableBlogPost
{
    public string print(BlogPost _blogPost);
}

class JsonBlogPostPrinter : IPrintableBlogPost
{
    public string print(BlogPost _blogPost)
    {
        return json_encode(_blogPost.getData());
    }
}

class HtmlBlogPostPrinter : IPrintableBlogPost
{
    public string print(BlogPost _blogPost)
    {
        return $"{_blogPost.getData().title}" +
            $"{_blogPost.getData().date.setFormat("Y-m-d H:i:s")}" +
            $"{_blogPost.getData().author.fullName()}" +
            $"{_blogPost.getData().content}";
    }
}
📂Python : SRP 비준수와 준수📂
# 다음은 두개의 기능이 섞여있어 옳지 않다
def addPrint(a, b):
    res = a + b
    print(res)
# 다음과 같이 단 하나의 책임으로 분리하자


def add(a, b):
    return a + b


def printf(str):
    print(str)

2). OCP (Open-Closed Principle)

ⓐ 확장-폐쇠 원칙

  • 확변
    1. 확장시
      • 열려있어야함
    2. 변경시
      • 닫혀있어야함

ⓑ 정확한 설명

  • 1. 새로운 기능을 추가싶을때 있다.
    2. 그런데 추가 시킬때마다 만들었던 코드를 수정해야된다면
    3. 너무 불편하지 않겠나? (애초에 수정이라는 과정에서 버그가 발생하기 마련이다)
    4. 그러지말고 그냥 한줄만 추가해도 기능 확장이 쉬운 구조로 만들자는것
    

ⓒ 예시

📂OCP BAD 🤪📂
class Dog {
    public string bark() { return "woof woof";}
}

class Duck{    
    public string bark() { return "quack quack";}    
}

class Fox {
    public string whatDoesTheFoxSay() { return "ring-ding-ding-ding-dingeringeding!, Wa-pa-pa-pa-pa-pa-pow!";}
}

class Communication {
    public string communicate(?????? _animal)
    {
        switch (true) {
            case animal.IsInstanceOfType(Dog):
                return animal.bark();
            case animal.IsInstanceOfType(Duck):
                return animal.quack();
            case animal.IsInstanceOfType(Fox):
                return animal.whatDoesTheFoxSay();
            default:
                throw new InvalidArgumentException("Unknown animal");
        }
    }
}

//기존 코드를 변경하지 않고 새 동물 클래스를 추가할 수 있습니까? 
//새 동물 클래스를 추가하려면 communi () 함수에서 스위치를 수정해야 합니다. 

//게다가 벌써부터 communicate에 오류가 났다.
📂OCP 준수 😎📂
interface IComunicatable
{
    public string speak();
}

class Dog : IComunicatable
{
    public string speak() { return "'woof woof'"; }
}

class Duck : IComunicatable {   
    public string speak() { return "'quack quack'"; }
}

class Fox : IComunicatable {
    public string speak() { return "ring-ding-ding-ding-dingeringeding!, Wa-pa-pa-pa-pa-pa-pow!"; }
}

class Communication
{
    public string communicate(IComunicatable _animal)
    {
        return _animal.speak();
    }
}
📂Python : OCP 비준수 코드📂 : Cow와 Sheep을 추가하기위해 hey함수의 수정이 필요하다.
class Animal():
    def __init__(self, type):
        self.type = type


def hey(animal):
    if animal.type == 'Cat':
        print('meow')
    elif animal.type == 'Dog':
        print('bark')


bingo = Animal('Dog')
kitty = Animal('Cat')

# Cow와 Sheep을 추가하기위해 hey함수의 수정이 필요하다.

hey(bingo)
hey(kitty)
📂Python : OCP 준수 코드📂 : 추가되는 동물에 대해 hey함수의 수정을 필요로 하지 않는다L
# interface method
class Animal:
    def speak(self):
        pass


class Cat(Animal):
    def speak(self):
        print("meow")


class Dog(Animal):
    def speak(self):
        print("bark")


class Sheep(Animal):
    def speak(self):
        print("meh")


class Cow(Animal):
    def speak(self):
        print("moo")


def hey(animal):
    animal.speak()

# 추가되는 동물에 대해 hey함수의 수정을 필요로 하지 않는다


bingo = Dog()
kitty = Cat()
sheep = Sheep()
cow = Cow()

hey(bingo)
hey(kitty)
hey(sheep)
hey(cow)

3). LSP (리스코프 Substitute Principle)

ⓐ 리스코프 치환 원칙

  • 상위 클래스가 하위 클래스로 치환될때 문제없이 잘 돌아가야한다.
    • we can use any inheriting class in place of the base class.
  • 자식 클래스가 부모클래스의 역할을 제대로 수행 못할때.

ⓑ 예시

📂LSP BAD 🤪📂
class Rectangle
{
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height){
        this.height = height;
    }

    public int calculateArea(): int
    {
        return this.width * this.height;
    }
}

class Square : Rectangle
{
    public void setWidth(int width)
    {
        this.width = width;
        this.height = width;
    }

    public void setHeight(int height)
    {
        this.width = height;
        this.height = height;
    }
}

class RectangleTest extends TestCase
{
    public  testCalculateArea()
    {
        shape = /* new Rectangle(); - lets replace by */ new Square();
        shape.setWidth(10);
        shape.setHeight(2);

        this.assertEquals(shape.calculateArea(), 20); // FAILS - 4 != 20

        shape.setWidth(5);
        this.assertEquals(shape.calculateArea(), 10); // FAILS - 25 != 10
    }
}

//부모 클래스의 메서드가 작동하는 방식을 변경해서는 안 됩니다.

//Square 클래스는 Rectangle 클래스에서 상속하면 안 됩니다. 
//이 두 클래스가 모두 계산 영역을 가질 수 있는 경우 
//공통 인터페이스를 구현하고 상당히 다르기 때문에 서로 상속하지 않도록 합니다.
📂LSP GOOD 😎📂
interface IICalculableArea
{
    public calculateArea();
}

class Rectangle : ICalculableArea
{
    protected int width;
    protected int height;

    public void __construct(int width, int height)
    {
        this.width = width;
        this.height = height;
    }

    public int calculateArea() {
        return this.width * this.height;
    }
}

class Square : ICalculableArea
{
    protected int edge;

    public void __construct(int edge) {
        this.edge = edge;
    }

    public  calculateArea(){
        return this.edge * this.edge ;
    }
}
📂LSP가 잘못된 상황📂
public class 망치 {
  void virtual 못을박기() {...}
  void virtual 공구상자에넣기(){...}
}

public class 귀상어_HammerheadShark : 망치(){
  void override 못을박기(){...}       //귀상어로 못을 박을수가 있나?
  void virtual 공구상자에넣기(){...}  //귀상어를 공구상자에 넣읗수 있나??
  //사용안할 메소드를 받는게 잘 된건가?
}

public class 목공 {
  public void 연결(, 망치, 나무들){
    만약... 인수 타입이 "귀상어_HammerheadShark"라면?
    귀상어로 망치질 할수 있나?
  }
}

static void Main(String argv){
}
📂LSP 예시📂
class Animal:
    def speak(self):
        pass

class Cat(Animal):
    def speak(self):
        print("meow")


class BlackCat(Cat):
    def speak(self):
        print("black meow")

class Fish(Animal) :
    def speak(self):
        raise Exception("Fish cannot speak")

def speak(_animal: Animal):
    _animal.speak()


animal = Cat()
speak(animal);
animal = BlackCat()
speak(animal);
animal = Fish();
speak(animal);

4). ISP (Interface Segregation Principle) ❓

ⓐ 인터페이스 분리 원칙

커버져린 인터페이스 사용시 사용하지도 않을 인터페이스가
클래스로 들어오게 된다면?..

  • 뚱뚱한 인터페이스를 사용 해서는 안된다.
    • 클라이언트는 사용하지도 않을 메소드들에게
      의존하게 해선 안된다 인터페이스를 더 작은 단위로 나누자
    • 1. 자동차 인터페이스는 
           * 자동차 인터페이스 클래스만
      2. 보트 인터페이스는 
           *  보트 인터페이스 클래스만
      3. 만약 양육자동차려면
           * 자동차 보트 인터페이스 둘다 가져오기
      

ⓑ 예시

📂ISP 준수 비준수📂
//No Interface Segregation Principle

//Large Interface
interface ICarBoatInterface
{
	public void drive();
	public void turnLeft();
	public void turnRight();
	public void steer();
	public void steerLeft();
	public void steerRight();
}

//Interface Segregation Principle
//two small interfaces (Car, Boat)
interface ICarInterface
{
	public void drive();
	public void turnLeft();
	public void turnRight();
}

interface IBoatInterface
{
	public void steer() 	{ /*implemenetation*/ }
	public void steerLeft() { /*implmementation*/ }
	public void steerRight(){ /*implementation*/ }
}


class Avante : ICarInterface
{
	public void drive() 	{ /*implemenetation*/ }
	public void turnLeft() 	{ /*implmementation*/ }
	public void turnRight()	{ /*implementation*/ }
}


class CarBoat :ICarInterface , IBoatInterface
{
	public void drive() 	{ /*implemenetation*/ }
	public void turnLeft() 	{ /*implmementation*/ }
	public void turnRight()	{ /*implementation*/ }
	public void steer() 	{ /*implemenetation*/ }
	public void steerLeft() { /*implmementation*/ }
	public void steerRight(){ /*implementation*/ }
}

5). DIP (Dependency Inverse Principle) ❓

ⓐ 의존 역전 원칙

  • "추상화에 의존하라"

    • 구체적인 개념보다는 추상적인 개념에 의존해야 한다.
  • 변동성이 큰 클래스로부터 파생하지 말자, 대신 추상 팩토리로 파생하여 확장성을 높이자

  • 서브클래스에서는 추상 메소드를 구현하거나, 훅 메소드를 오버라이드하는 방법을 이용해 기능의 일부를 확장합니다.

ⓑ Hook 메소드 란?

  1. [1) Virtual 하게 정의] 해두거나 [2) 비워진 (추상적으로)] 수퍼 클래스에 정의된 메소드이며,
  • 특정 구현에 대한 종속성을 줄여야 하지만 인터페이스에 의존해야 합니다.

ⓑ 예시

📂DIP_BAD 🤪📂
class DatabaseLogger
{
    public void logError(string message)
    {
        // ..
    }
}

class MailerService
{
    private DatabaseLogger logger;

    public MailerService(DatabaseLogger logger)
    {
        this.logger = logger;
    }

    public void sendEmail()
    {
        try {
            // ..
        } catch (SomeException exception) {
            this.logger.logError(exception.getMessage());
        }
    }
}
📂DIP_GOOD 😎📂
interface ILogger
{
    public void logError(string message);
}

class DatabaseLogger : ILogger
{
    public void logError(string message)
    {
        // ..
    }
}

class MailerService
{
    private ILogger logger;

    public MailerService(ILogger logger)
    {
        this.logger = logger;
    }

    public void sendEmail()
    {
        try {
            // ..
        } catch (SomeException exception) {
            this.logger.logError(exception.getMessage());
        }
    }
}

/*
이러한 방식으로 세부 구현이 LoggerInterface 를 구현하는 한 
데이터베이스의 로그를 원하는 로그로 자유롭게 바꿀 수 있습니다 . 
이 변경 사항은 MailerService에 의존하지 않고 
인터페이스에만 의존하기 때문에
MailerService 를 수정할 필요가 없습니다 .
*/
📂Python : DIP_비준수📂
class Cat:
    def speak(self):
        print("meow")


class Dog:
    def speak(self):
        print("bark")


class Zoo:
    def __init__(self):
        self.cat = Cat()
        self.Dog = Dog()
        #self.sheep = Sheep()
        #self.cow = Cow()
📂Python : DIP_준수📂
class abstract_Animal:
    def speak(self):
        pass


class Cat(abstract_Animal):
    def speak(self):
        print("meow")


class Dog(abstract_Animal):
    def speak(self):
        print("bark")

# 동물원은 전혀 건들 필요 없어진다.


class Zoo:
    def __init__(self):
        self.animals = []  # 동물 리스트 만들기

    def AddAnimal(self, _animal: abstract_Animal):
        self.animals.append(_animal)

    def speakAll(self):
        for _animal in self.animals:
            _animal.speak()


zoo = Zoo()
zoo.AddAnimal(Cat())
zoo.AddAnimal(Dog())
zoo.speakAll()

6). 추가 : 디미터의 법칙(Demeter's Law, LoD)

'친한 친구들 하고만 이야기 하여라' = 동일한 객체 내의 메소드 및 속성을 주로 사용할 것
(SRP와 마찬가지로 결합도를 낮추고 응집도를 끌어올리는 역할을 하게 된다)

참고

  1. 코드없는 프로그래밍 SOLID
  2. https://github.com/accesto/solid-php
  3. https://accesto.com/blog/solid-php-solid-principles-in-php/