SOLID 원칙에 대한 확실한 가이드

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.리스 코프 교체

Next up on our list is Liskov substitution, which is arguably the most complex of the 5 principles. Simply put, if class A is a subtype of class B, then we should be able to replace B with A without disrupting the behavior of our program.

Let's just jump straight to the code to help wrap our heads around this concept:

public interface Car { void turnOnEngine(); void accelerate(); }

Above, we define a simple Car interface with a couple of methods that all cars should be able to fulfill – turning on the engine, and accelerating forward.

Let's implement our interface and provide some code for the methods:

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); } }

As our code describes, we have an engine that we can turn on, and we can increase the power. But wait, its 2019, and Elon Musk has been a busy man.

We are now living in the era of electric cars:

public class ElectricCar implements Car { public void turnOnEngine() { throw new AssertionError("I don't have an engine!"); } public void accelerate() { //this acceleration is crazy! } }

By throwing a car without an engine into the mix, we are inherently changing the behavior of our program. This is a blatant violation of Liskov substitution and is a bit harder to fix than our previous 2 principles.

One possible solution would be to rework our model into interfaces that take into account the engine-less state of our Car.

6. Interface Segregation

The ‘I ‘ in SOLID stands for interface segregation, and it simply means that larger interfaces should be split into smaller ones. By doing so, we can ensure that implementing classes only need to be concerned about the methods that are of interest to them.

For this example, we're going to try our hands as zookeepers. And more specifically, we'll be working in the bear enclosure.

Let's start with an interface that outlines our roles as a bear keeper:

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

As avid zookeepers, we're more than happy to wash and feed our beloved bears. However, we're all too aware of the dangers of petting them. Unfortunately, our interface is rather large, and we have no choice than to implement the code to pet the bear.

Let's fix this by splitting our large interface into 3 separate ones:

public interface BearCleaner { void washTheBear(); } public interface BearFeeder { void feedTheBear(); } public interface BearPetter { void petTheBear(); }

Now, thanks to interface segregation, we're free to implement only the methods that matter to us:

public class BearCarer implements BearCleaner, BearFeeder { public void washTheBear() { //I think we missed a spot... } public void feedTheBear() { //Tuna Tuesdays... } }

And finally, we can leave the dangerous stuff to the crazy people:

public class CrazyPerson implements BearPetter { public void petTheBear() { //Good luck with that! } }

Going further, we could even split our BookPrinter class from our example earlier to use interface segregation in the same way. By implementing a Printer interface with a single print method, we could instantiate separate ConsoleBookPrinter and OtherMediaBookPrinter classes.

7. Dependency Inversion

The principle of Dependency Inversion refers to the decoupling of software modules. This way, instead of high-level modules depending on low-level modules, both will depend on abstractions.

To demonstrate this, let's go old-school and bring to life a Windows 98 computer with code:

public class Windows98Machine {}

But what good is a computer without a monitor and keyboard? Let's add one of each to our constructor so that every Windows98Computer we instantiate comes pre-packed with a Monitor and a StandardKeyboard:

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

This code will work, and we'll be able to use the StandardKeyboard and Monitor freely within our Windows98Computer class. Problem solved? Not quite. By declaring the StandardKeyboard and Monitor with the new keyword, we've tightly coupled these 3 classes together.

Not only does this make our Windows98Computer hard to test, but we've also lost the ability to switch out our StandardKeyboard class with a different one should the need arise. And we're stuck with our Monitor class, too.

Let's decouple our machine from the StandardKeyboard by adding a more general Keyboard interface and using this in our class:

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; } }

Here, we're using the dependency injection pattern here to facilitate adding the Keyboard dependency into the Windows98Machine class.

Let's also modify our StandardKeyboard class to implement the Keyboard interface so that it's suitable for injecting into the Windows98Machine class:

public class StandardKeyboard implements Keyboard { }

Now our classes are decoupled and communicate through the Keyboard abstraction. If we want, we can easily switch out the type of keyboard in our machine with a different implementation of the interface. We can follow the same principle for the Monitor class.

Excellent! We've decoupled the dependencies and are free to test our Windows98Machine with whichever testing framework we choose.

8. Conclusion

In this tutorial, we've taken a deep dive into the SOLID principles of object-oriented design.

우리 는 SOLID의 역사와 이러한 원칙이 존재하는 이유에 대해 간략히 설명했습니다.

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

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