paint-brush
코드를 견고하게 유지하는 방법~에 의해@oxymoron_31
4,004 판독값
4,004 판독값

코드를 견고하게 유지하는 방법

~에 의해 Nagarakshitha Ramu6m2023/03/15
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

S.O.L.I.D 원칙은 객체 지향 프로그래밍에서 깔끔한 코드를 작성하기 위한 일반적인 지침입니다. 여기에는 단일 책임 원칙, 개방형 폐쇄 원칙, 인터페이스 분리, 종속성 반전 및 대체 원칙이 포함됩니다. 이러한 원칙은 다양한 프로그래밍 언어에 적용될 수 있습니다.
featured image - 코드를 견고하게 유지하는 방법
Nagarakshitha Ramu HackerNoon profile picture
0-item

광범위하게 모든 프로그래밍 언어는 두 가지 패러다임으로 분류될 수 있습니다.

명령형/ 객체 지향 프로그래밍 - 한 줄씩 실행되는 일련의 명령을 따릅니다.

선언적/ 기능적 프로그래밍 - 순차적이지는 않지만 프로그램의 목적에 더 관심을 둡니다. 전체 프로그램은 각각 특정 작업을 수행하는 하위 기능을 추가로 포함하는 기능과 같습니다.

주니어 개발자로서 저는 이것이 단지 기능적인 코드 작성에 관한 것이 아니라 의미상 쉽고 유연한 코드 작성에 관한 것임을 뼈저리게 깨닫고 있습니다(읽기: 수천 줄의 코드를 초조하게 쳐다보고 있었습니다).

두 패러다임 모두에 깔끔한 코드를 작성하기 위한 여러 모범 사례가 있지만 저는 프로그래밍의 객체 지향 패러다임과 관련된 SOLID 설계 원칙에 대해 이야기하겠습니다.

솔리드란 무엇입니까?

S — 단일 책임
O—개방-폐쇄 원칙
L — 리스코프 대체 원리
I — 인터페이스 분리
D — 종속성 반전

이러한 개념을 이해하는 데 어려움을 겪는 주된 이유는 기술적인 깊이가 헤아릴 수 없기 때문이 아니라 객체 지향 프로그래밍에서 깔끔한 코드를 작성하기 위해 일반화된 추상적인 지침이기 때문입니다. 이러한 개념을 이해하기 위해 몇 가지 고급 클래스 다이어그램을 살펴보겠습니다.

이는 정확한 클래스 다이어그램은 아니지만 클래스에 어떤 메서드가 있는지 이해하는 데 도움이 되는 기본 청사진입니다.

기사 전반에 걸쳐 카페의 예를 고려해 보겠습니다.

단일 책임 원칙

클래스를 변경해야 하는 이유는 단 하나여야 합니다.

카페에서 받은 온라인 주문을 처리하는 이 클래스를 생각해 보세요.

무슨 문제가 있나요?
이 단일 클래스는 여러 기능을 담당합니다. 다른 결제 수단을 추가해야 한다면 어떻게 되나요? 확인을 보내는 방법이 여러 가지인 경우 어떻게 해야 합니까? 주문 처리를 담당하는 클래스의 결제 논리를 변경하는 것은 그다지 좋은 디자인이 아닙니다. 유연성이 매우 떨어지는 코드로 이어집니다.

더 좋은 방법은 아래 그림과 같이 이러한 특정 기능을 구체적인 클래스로 분리하고 해당 클래스의 인스턴스를 호출하는 것입니다.

개방-폐쇄 원칙

엔터티는 확장을 위해 열려 있어야 하고 수정을 위해 닫혀 있어야 합니다.

카페에서는 옵션 목록에서 커피에 넣을 양념을 선택해야 하며 이를 처리하는 클래스가 있습니다.

카페에서는 새로운 조미료인 버터를 추가하기로 결정했습니다. 선택한 조미료에 따라 가격이 어떻게 변하는지 확인하고 가격 계산 논리는 Coffee 클래스에 있습니다. 메인 클래스에서 가능한 코드 변경을 생성하는 새로운 조미료 클래스를 매번 추가해야 할 뿐만 아니라 로직을 매번 다르게 처리해야 합니다.

더 좋은 방법은 메서드를 재정의하는 하위 클래스를 가질 수 있는 조미료 인터페이스를 만드는 것입니다. 그리고 메인 클래스는 조미료 인터페이스를 사용하여 매개변수를 전달하고 각 주문의 수량과 가격을 얻을 수 있습니다.

여기에는 두 가지 장점이 있습니다.

1. 주문을 동적으로 변경하여 다양한 양념을 추가할 수 있습니다(모카를 곁들인 커피와 초콜릿은 정말 환상적입니다).

2. Condiments 클래스는 is-a가 아닌 Coffee 클래스와 has-a 관계를 갖게 됩니다. 따라서 귀하의 커피는 모카/버터/우유 종류의 커피가 아닌 모카/버터/우유를 가질 수 있습니다.

Liskov 대체 원칙

모든 하위 클래스 또는 파생 클래스는 기본 클래스 또는 상위 클래스를 대체할 수 있어야 합니다.

이는 하위 클래스가 상위 클래스를 직접 대체할 수 있어야 함을 의미합니다. 동일한 기능을 가지고 있어야 합니다. 나는 이것이 복잡한 수학 공식처럼 들리기 때문에 이해하기 어렵다는 것을 알았습니다. 그러나 나는 이 글에서 그 점을 분명히 하려고 노력할 것이다.

카페 직원을 생각해 보세요. 바리스타, 매니저, 서버가 있어요. 그들은 모두 비슷한 기능을 가지고 있습니다.

따라서 이름, 위치, getName, getPostion, takeOrder(), Serve()를 사용하여 기본 Staff 클래스를 만들 수 있습니다.

각각의 구체적인 클래스인 Waiter, Barista 및 Manager는 이 클래스에서 파생될 수 있으며 동일한 메서드를 재정의하여 해당 위치에 필요한 대로 구현할 수 있습니다.

이 예에서 Liskov 대체 원칙(LSP)은 Staff의 파생 클래스가 코드의 정확성에 영향을 주지 않고 기본 Staff 클래스와 상호 교환적으로 사용될 수 있도록 보장하여 사용됩니다.

예를 들어, Waiter 클래스는 Staff 클래스를 확장하고 takeOrder 및serveOrder 메서드를 재정의하여 웨이터 역할과 관련된 추가 기능을 포함합니다. 그러나 더 중요한 것은 기능의 차이에도 불구하고 Staff 클래스의 개체를 기대하는 모든 코드가 Waiter 클래스의 개체에서도 올바르게 작동할 수 있다는 것입니다.

이것을 이해하기 위해 예를 들어보자

 public class Cafe {    public void serveCustomer (Staff staff) { staff.takeOrder(); staff.serveOrder(); } } public class Main {    public static void main (String[] args) { Cafe cafe = new Cafe(); Staff staff1 = new Staff( "John" , "Staff" ); Waiter waiter1 = new Waiter( "Jane" ); restaurant.serveCustomer(staff1); // Works correctly with Staff object
 restaurant.serveCustomer(waiter1); // Works correctly with Waiter object
 } }

여기에서 Cafe 클래스의 ServeCustomer() 메소드는 Staff 객체를 매개변수로 사용합니다. ServeCustomer() 메서드는 Staff 개체의 takeOrder() 및 ServeOrder() 메서드를 호출하여 고객에게 서비스를 제공합니다.

Main 클래스에서는 Staff 객체와 Waiter 객체를 생성합니다. 그런 다음 Cafe 클래스의 ServeCustomer() 메서드를 두 번 호출합니다. 한 번은 Staff 객체로, 한 번은 Waiter 객체로 호출합니다.

Waiter 클래스는 Staff 클래스에서 파생되므로 Staff 클래스의 개체를 기대하는 모든 코드는 Waiter 클래스의 개체에서도 올바르게 작동할 수 있습니다. 이 경우, Waiter 객체에 웨이터 역할과 관련된 추가 기능이 있더라도 Cafe 클래스의serveCustomer() 메서드는 Staff 객체와 Waiter 객체 모두에서 올바르게 작동합니다.

인터페이스 분리

클래스가 사용하지 않는 메서드에 의존하도록 강요해서는 안 됩니다.

그래서 카페에는 커피, 차, 스낵, 탄산음료를 판매할 수 있는 매우 다재다능한 자판기가 있습니다.

뭐가 문제야? 기술적으로는 아무것도 없습니다. 커피 내리기와 같은 기능에 대한 인터페이스를 구현해야 한다면 차, 탄산음료, 스낵을 위한 다른 메서드도 구현해야 합니다. 이는 불필요하며 이러한 기능은 서로 기능과 관련이 없습니다. 이러한 각 기능은 기능 간의 응집력이 매우 낮습니다.

응집력이란 무엇입니까? 인터페이스의 메서드가 서로 얼마나 강력하게 관련되어 있는지를 결정하는 요소입니다.

그리고 자동판매기의 경우 방법은 거의 상호의존적이지 않습니다. 응집력이 매우 낮기 때문에 메소드를 분리할 수 있습니다.

이제 한 가지를 구현하려는 인터페이스는 모든 기능에 공통적인 takeMoney()만 구현해야 합니다. 이는 인터페이스에서 관련되지 않은 기능을 분리하므로 인터페이스에서 관련되지 않은 기능을 강제로 구현하는 것을 방지합니다.

의존성 반전

높은 수준의 모듈은 낮은 수준의 모듈에 의존해서는 안 됩니다. 세부 사항은 추상화에 따라 달라져야 합니다.

카페의 에어컨(냉각기)을 생각해 보세요. 그리고 당신이 나와 같다면 그곳은 항상 얼어붙을 것입니다. 리모콘과 AC 클래스를 살펴보겠습니다.

여기서, RemoteControl은 하위 레벨 구성 요소인 AC에 의존하는 상위 레벨 모듈입니다. 투표를 받으면 히터도 원할 것입니다 :P 따라서 쿨러가 아닌 일반적인 온도 조절 기능을 갖기 위해 원격 제어와 온도 제어를 분리하겠습니다. 그러나 RemoteControl 클래스는 구체적인 구현인 AC와 긴밀하게 결합되어 있습니다. 종속성을 분리하기 위해 예를 들어 45-65F 범위 내에서 증가 온도() 및 감소 온도() 기능만 갖는 인터페이스를 만들 수 있습니다.

최종 생각

보시다시피, 상위 수준 모듈과 하위 수준 모듈 모두 온도 증가 또는 감소 기능을 추상화하는 인터페이스에 의존합니다.

구체적인 클래스인 AC는 적용 가능한 온도 범위로 메서드를 구현합니다.

이제 히터라고 하는 다른 콘크리트 클래스에서 다양한 온도 범위를 구현하려는 히터를 얻을 수 있을 것입니다.

상위 레벨 모듈인 remoteControl은 런타임 중에 올바른 메소드를 호출하는 것에 대해서만 걱정하면 됩니다.