1. 소개

이 튜토리얼에서는 객체 지향 설계의 SOLID 원칙에 대해 설명 합니다.

먼저, 그 이유와 소프트웨어를 설계 고려해야하는 이유를 살펴 보겠습니다 . 그런 다음 요점을 강조하기 위해 몇 가지 예제 코드와 함께 각 원칙을 간략하게 설명합니다.

2. SOLID 원칙의 이유

SOLID 원칙은 Robert C. Martin이 2000 년 논문 인 Design Principles and Design Patterns 에서 처음 개념화했습니다  .  이러한 개념은 나중에 SOLID 약어를 소개 한 Michael Feathers에 의해 구축되었습니다. 그리고 지난 20 년 동안이 5 가지 원칙은 객체 지향 프로그래밍의 세계에 혁명을 일으켜 우리가 소프트웨어를 작성하는 방식을 변화 시켰습니다.

그렇다면 SOLID 란 무엇이며 더 나은 코드를 작성하는 데 어떻게 도움이됩니까? 간단히 말해서 Martin과 Feathers의  디자인 원칙은 우리가보다 유지 관리하고 이해하기 쉬우 며 유연한 소프트웨어를 만들도록 장려합니다 . 결과적으로 애플리케이션의 크기가 커짐에 따라 애플리케이션의 복잡성을 줄이고 앞으로 나아가는 많은 골칫거리를 줄일 수 있습니다!

다음 5 가지 개념이 SOLID 원칙을 구성합니다.

  1. S 화롯불 책임
  2. O 펜 / 닫힘
  3. L iskov 대체
  4. I nterface 독방
  5. D의 ependency 반전

이 단어 중 일부는 벅차게 들릴 수 있지만 간단한 코드 예제를 사용하면 쉽게 이해할 수 있습니다. 다음 섹션에서는 각 원칙을 설명하는 간단한 Java 예제와 함께 이러한 각 원칙이 의미하는 바에 대해 자세히 알아 봅니다.

3. 단일 책임

단일 책임 원칙으로 시작합시다. 우리가 예상 할 수 있듯이이 원칙은 한 학급이 단 하나의 책임 만 가져야한다고 말합니다 . 또한 변경해야하는 이유는 하나만 있어야합니다.

이 원칙이 더 나은 소프트웨어를 만드는 데 어떻게 도움이됩니까? 몇 가지 이점을 살펴 보겠습니다.

  1. 테스트  – 책임이 하나 인 클래스는 테스트 케이스 수가 훨씬 적습니다.
  2. 낮은 결합  – 단일 클래스의 기능이 적을수록 의존성이 감소합니다.
  3. 조직  – 모 놀리 식 클래스보다 작고 잘 구성된 클래스가 검색하기 쉽습니다.

예를 들어 간단한 책을 나타내는 클래스를 생각해보십시오.

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters
}

이 코드에서는 Book 인스턴스와 관련된 이름, 저자 및 텍스트를 저장합니다 .

이제 텍스트를 쿼리하는 몇 가지 메서드를 추가해 보겠습니다.

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters

    // methods that directly relate to the book properties
    public String replaceWordInText(String word){
        return text.replaceAll(word, text);
    }

    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

이제 Book  클래스가 잘 작동하고 애플리케이션에 원하는만큼의 책을 저장할 수 있습니다. 그러나 텍스트를 콘솔에 출력하고 읽을 수 없다면 정보를 저장하는 것이 무슨 소용이 있을까요?

바람에주의를 기울이고 인쇄 방법을 추가해 보겠습니다.

public class Book {
    //...

    void printTextToConsole(){
        // our code for formatting and printing the text
    }
}

그러나이 강령은 앞서 설명한 단일 책임 원칙을 위반합니다. 문제를 해결하려면 텍스트 인쇄에만 관련된 별도의 클래스를 구현해야합니다.

public class BookPrinter {

    // methods for outputting text
    void printTextToConsole(String text){
        //our code for formatting and printing the text
    }

    void printTextToAnotherMedium(String text){
        // code for writing to any other location..
    }
}

대박. 책  의 인쇄 의무 를 덜어주는 클래스를 개발했을뿐만 아니라  BookPrinter  클래스를 활용 하여 텍스트를 다른 미디어로 보낼 수도 있습니다  .

이메일, 로깅 또는 기타 어떤 것이 든이 문제에 대한 별도의 클래스가 있습니다.

4. 확장을 위해 열기, 수정을 위해 닫기

이제 'O'의 시간입니다. 공식적으로는 개방 폐쇄 원칙이라고 합니다. 간단히 말해서, 클래스는 확장을 위해 열려 있어야하지만 수정을 위해 닫혀 있어야합니다. 그렇게함으로써, 우리는 기존 코드를 수정 하고 그렇지 않으면 행복한 애플리케이션에서 잠재적 인 새로운 버그일으키는 것을 막습니다.

물론 규칙한 가지 예외는 기존 코드의 버그를 수정할 때입니다.

간단한 코드 예제를 통해 개념을 더 자세히 살펴 보겠습니다. 새로운 프로젝트의 일환으로 Guitar  클래스를 구현했다고 상상해보십시오  .

그것은 완전히 본격적이며 볼륨 노브도 있습니다.

public class Guitar {

    private String make;
    private String model;
    private int volume;

    //Constructors, getters & setters
}

우리는 응용 프로그램을 시작하고 모두가 그것을 좋아합니다. 그러나 몇 달 후,  기타  가 약간 지루하고 멋진 불꽃 패턴을 사용하여 좀 더 '로큰롤'처럼 보이게 할 수 있다고 결정했습니다.

이 시점에서 Guitar  클래스를 열고 불꽃 패턴을 추가하는 것이 유혹적 일 수  있지만 애플리케이션에서 발생할 수있는 오류를 누가 압니까?

대신 open-closed 원칙을 고수하고 Guitar  클래스 를 확장 해  보겠습니다 .

public class SuperCoolGuitarWithFlames extends Guitar {

    private String flameColor;

    //constructor, getters + setters
}

Guitar  클래스 를 확장함으로써  기존 애플리케이션이 영향을받지 않도록 할 수 있습니다.

5.리스 코프 교체

다음 List은 Liskov 치환 인데, 이는 5 가지 원칙 중 가장 복잡한 것입니다. 간단히 말해, 클래스 A 가 클래스 B 의 하위 유형 이면 프로그램의 동작을 방해하지 않고 를  로 대체 할 수 있어야합니다  .

이 개념을 이해하는 데 도움이되는 코드로 바로 이동해 보겠습니다.

public interface Car {

    void turnOnEngine();
    void accelerate();
}

위에서 우리 는 모든 자동차가 수행 할 수 있어야하는 두 가지 방법 (엔진을 켜고 앞으로 가속 )을 사용하여 간단한 Car  인터페이스를 정의합니다  .

인터페이스를 구현하고 메서드에 대한 몇 가지 코드를 제공하겠습니다.

public class MotorCar implements Car {

    private Engine engine;

    //Constructors, getters + setters

    public void turnOnEngine() {
        //turn on the engine!
        engine.on();
    }

    public void accelerate() {
        //move forward!
        engine.powerOn(1000);
    }
}

코드가 설명 하듯이, 우리는 켤 수있는 엔진을 가지고 있고, 파워를 높일 수 있습니다. 그러나 잠깐, 2019 년과 Elon Musk는 바쁜 사람이었습니다.

우리는 이제 전기 자동차 시대에 살고 있습니다.

public class ElectricCar implements Car {

    public void turnOnEngine() {
        throw new AssertionError("I don't have an engine!");
    }

    public void accelerate() {
        //this acceleration is crazy!
    }
}

엔진이없는 자동차를 믹스에 넣음으로써 우리는 본질적으로 프로그램의 동작을 바꾸고 있습니다. 이것은 Liskov 대체의 노골적인 위반이며 이전의 두 가지 원칙보다 수정하기가 조금 더 어렵습니다 .

한 가지 가능한 해결책은 우리 모델을 자동차 의 엔진이없는 상태를 고려하는 인터페이스로 재 작업하는 것  입니다.

6. 인터페이스 분리

SOLID의 'I'는 인터페이스 분리를 ​​의미하며, 이는 단순히 더 큰 인터페이스를 더 작은 인터페이스로 분할해야 함을 의미 합니다. 그렇게함으로써 우리는 클래스를 구현하는 데 관심있는 메서드에만 관심을 갖도록 할 수 있습니다.

이 예에서는 사육사로 손을 잡을 것입니다. 더 구체적으로, 우리는 곰 인클로저에서 작업 할 것입니다.

베어 키퍼로서 우리의 역할을 설명하는 인터페이스부터 시작하겠습니다.

public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}

열렬한 사육사로서 우리는 사랑하는 곰을 씻고 먹이를 줄 수있어 기쁩니다. 그러나 우리는 그들을 쓰다듬는 것의 위험성을 너무 잘 알고 있습니다. 불행히도 우리의 인터페이스는 다소 크기 때문에 곰을 쓰다듬는 코드를 구현하는 것 외에 선택의 여지가 없습니다.

큰 인터페이스를 3 개의 별도 인터페이스로 분할하여이 문제를 해결 하겠습니다 .

public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
    void feedTheBear();
}

public interface BearPetter {
    void petTheBear();
}

이제 인터페이스 분리 덕분에 우리에게 중요한 메서드 만 자유롭게 구현할 수 있습니다.

public class BearCarer implements BearCleaner, BearFeeder {

    public void washTheBear() {
        //I think we missed a spot...
    }

    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}

마지막으로 위험한 것은 미친 사람들에게 맡길 수 있습니다.

public class CrazyPerson implements BearPetter {

    public void petTheBear() {
        //Good luck with that!
    }
}

더 나아가서  이전 예제에서 BookPrinter  클래스를 분리하여 동일한 방식으로 인터페이스 분리를 ​​사용할 수도 있습니다. 단일 인쇄  방법 으로 프린터 인터페이스  를 구현하여  별도의 ConsoleBookPrinter  및  OtherMediaBookPrinter  클래스를 인스턴스화 할 수  있습니다.

7. 의존성 반전

Dependency Inversion의 원리는 소프트웨어 모듈의 분리를 나타냅니다. 이렇게하면 저수준 모듈에 의존하는 고수준 모듈 대신 둘 다 추상화에 의존합니다.

이를 증명하기 위해 구식으로 돌아가서 코드를 사용하여 Windows 98 컴퓨터를 구현해 보겠습니다.

public class Windows98Machine {}

그러나 모니터와 키보드가없는 컴퓨터는 무슨 소용이 있습니까? 생성자에 각각 하나를 추가하여 인스턴스화 하는 모든  Windows98 컴퓨터 Monitor StandardKeyboard 가 미리 포장되어 있습니다 .

public class Windows98Machine {

    private final StandardKeyboard keyboard;
    private final Monitor monitor;

    public Windows98Machine() {
        monitor = new Monitor();
        keyboard = new StandardKeyboard();
    }

}

이 코드는 작동하며 Windows98Computer  클래스 에서 StandardKeyboardMonitor를 자유롭게  사용할 수 있습니다 . 문제 해결됨? 좀 빠지는. 선언에 의해 StandardKeyboard  와  모니터를  와  새  키워드, 우리는 단단하게이 3 개 클래스를 결합했습니다.

이로 인해  Windows98Computer  를 테스트하기가 어려울뿐만 아니라 필요에 따라 StandardKeyboard  클래스를 다른 클래스 로 전환 할 수있는 기능도 손실되었습니다 . 그리고 우리는 모니터 클래스에도 붙어 있습니다.

좀 더 일반적인 키보드 인터페이스 를 추가 하고 클래스에서 이것을 사용하여 StandardKeyboard 에서 시스템을 분리 해 보겠습니다 .

public interface Keyboard { }
public class Windows98Machine{

    private final Keyboard keyboard;
    private final Monitor monitor;

    public Windows98Machine(Keyboard keyboard, Monitor monitor) {
        this.keyboard = keyboard;
        this.monitor = monitor;
    }
}

여기서는 Windows98Machine 클래스에 키보드 의존성 을 쉽게 추가하기 위해 여기에 의존성 주입 패턴을 사용하고 있습니다.

또한 StandardKeyboard 클래스를 수정  하여 Keyboard 인터페이스 를 구현 하여 Windows98Machine 클래스 에 삽입하는 데 적합합니다 .

public class StandardKeyboard implements Keyboard { }

이제 클래스가 분리되고 키보드 추상화를 통해 통신합니다 . 원하는 경우 인터페이스의 다른 구현을 사용하여 시스템에서 키보드 유형을 쉽게 전환 할 수 있습니다. Monitor 클래스에 대해서도 동일한 원칙을 따를 수 있습니다 .

우수한! 의존성을 분리했으며 선택한 테스트 프레임 워크를 사용하여 Windows98Machine 자유롭게  테스트 할 수 있습니다.

8. 결론

이 튜토리얼에서는 객체 지향 설계의 SOLID 원칙에 대해 자세히 살펴 보았습니다 .

우리 는 SOLID의 역사와 이러한 원칙이 존재하는 이유부터 시작했습니다.

한 글자 씩, 우리는 그것을 위반하는 빠른 코드 예제로 각 원칙의 의미를 세분화했습니다. 그런 다음 코드를 수정하고 SOLID 원칙을 준수 하는 방법을 보았습니다  .

항상 그렇듯이 코드는 GitHub에서 사용할 수 있습니다 .