모든 소프트웨어 응용 프로그램은 시간이 지남에 따라 변경됩니다. 소프트웨어를 변경하면 예기치 않은 계단식 문제가 발생할 수 있습니다. 그러나 변경되지 않는 소프트웨어는 만들 수 없으므로 변경은 불가피합니다. 소프트웨어 요구 사항은 소프트웨어가 성장함에 따라 계속 변경됩니다. 우리가 할 수 있는 것은 변화에 탄력적인 방식으로 소프트웨어를 설계하는 것입니다. 소프트웨어를 제대로 설계하려면 처음에는 시간과 노력이 필요하지만 장기적으로 보면 시간과 노력을 절약할 수 있습니다. 밀접하게 결합된 소프트웨어는 취약하며 변경 시 어떤 일이 발생하는지 예측할 수 없습니다. 다음은 잘못 설계된 소프트웨어의 영향 중 일부입니다.
- 불안정을 유발합니다.
- 코드 변경은 비용이 많이 듭니다.
- 소프트웨어를 단순하게 만드는 것보다 복잡성을 추가하는 것이 더 쉽습니다.
- 코드를 관리할 수 없습니다.
- 개발자가 기능이 어떻게 작동하는지 파악하는 데는 많은 시간이 걸립니다.
- 소프트웨어의 한 부분을 변경하면 일반적으로 다른 부분이 손상되며 변경으로 인해 어떤 문제가 발생할지 예측할 수 없습니다.
디자인 원칙 및 디자인 패턴(Design Principles and Design Patterns)이라는 논문에는 소프트웨어가 썩는 다음과 같은 증상이 나열되어 있습니다.
- 강성:문제를 일으키지 않고 코드를 변경하는 것은 매우 어렵습니다. 한 부분을 변경하면 코드의 다른 부분을 변경해야 하기 때문입니다.
- 취약성:코드를 변경하면 일반적으로 소프트웨어의 동작이 중단됩니다. 변경 사항과 직접적인 관련이 없는 부분도 깨뜨릴 수 있습니다.
- 불안정성:소프트웨어 애플리케이션의 일부가 유사한 동작을 할 수 있지만 코드를 재사용할 수 없으므로 복제해야 합니다.
- 점도:소프트웨어를 변경하기 어려운 경우 소프트웨어를 개선하는 대신 복잡성을 계속 추가합니다.
변경 사항을 제어하고 예측할 수 있는 방식으로 소프트웨어를 설계해야 합니다.
SOLID 설계 원칙은 소프트웨어 프로그램을 분리하여 이러한 문제를 해결하는 데 도움이 됩니다. Robert C. Martin은 Design Principles and Design Patterns라는 제목의 논문에서 이러한 개념을 소개했으며 Michael Feathers는 나중에 약어를 생각해 냈습니다.
SOLID 디자인 원칙에는 다음 5가지 원칙이 포함됩니다.
- S 단일 책임 원칙
- 오 펜/폐쇄 원칙
- 엘 iskov 치환 원리
- 나 인터페이스 분리 원칙
- 디 ependency 반전 원리
이러한 원칙이 Ruby에서 잘 설계된 소프트웨어를 구축하는 데 어떻게 도움이 되는지 이해하기 위해 각 원칙을 살펴보겠습니다.
단일 책임 원칙 - SRP
HR 관리 소프트웨어의 경우 사용자를 생성하고 직원의 급여를 추가하고 직원의 급여 명세서를 생성하는 기능이 필요하다고 가정해 보겠습니다. 빌드하는 동안 이러한 기능을 단일 클래스에 추가할 수 있지만 이 접근 방식은 이러한 기능 간에 원치 않는 종속성을 발생시킵니다. 시작할 때는 간단하지만 상황이 변경되고 새로운 요구 사항이 발생하면 변경 사항이 중단될 기능을 예측할 수 없습니다.
<블록 인용>클래스에는 변경해야 할 단 하나의 이유가 있어야 합니다. - Robert C Martin
다음은 모든 기능이 단일 클래스에 있는 샘플 코드입니다.
class User
def initialize(employee, month)
@employee = employee
@month = month
end
def generate_payslip
# Code to read from database,
# generate payslip
# and write it to a file
self.send_email
end
def send_email
# code to send email
employee.email
month
end
end
급여 명세서를 생성하여 사용자에게 보내기 위해 클래스를 초기화하고 급여 명세서 생성 메소드를 호출할 수 있습니다.
month = 11
user = User.new(employee, month)
user.generate_payslip
이제 새로운 요구 사항이 있습니다. 급여 명세서를 생성하고 싶지만 이메일을 보내고 싶지 않습니다. 기존 기능을 그대로 유지하고 내부 제안용처럼 이메일을 보내지 않고 내부 보고를 위한 새로운 급여명세서 생성기를 추가해야 합니다. 이 단계에서 직원에게 전송된 기존 급여 명세서가 계속 작동하도록 하고 싶습니다.
이 요구 사항에 대해 기존 코드를 재사용할 수 없습니다. true이면 이메일을 보내지 않으면 생성하지 않는다는 플래그를 generate_payslip 메서드에 추가해야 합니다. 이 작업은 수행할 수 있지만 기존 코드를 변경하므로 기존 기능이 중단될 수 있습니다.
문제가 발생하지 않도록 하려면 이러한 논리를 별도의 클래스로 분리해야 합니다.
class PayslipGenerator
def initialize(employee, month)
@employee = employee
@month = month
end
def generate_payslip
# Code to read from database,
# generate payslip
# and write it to a file
end
end
class PayslipMailer
def initialize(employee)
@employee = employee
end
def send_mail
# code to send email
employee.email
month
end
end
다음으로 이 두 클래스를 초기화하고 해당 메서드를 호출할 수 있습니다.
month = 11
# General Payslip
generator = PayslipGenerator.new(employee, month)
generator.generate_payslip
# Send Email
mailer = PayslipMailer.new(employee, month)
mailer.send_mail
이 접근 방식은 책임을 분리하고 예측 가능한 변경을 보장하는 데 도움이 됩니다. 메일러 기능만 변경해야 하는 경우 보고서 생성을 변경하지 않고 변경할 수 있습니다. 또한 기능의 변경 사항을 예측하는 데 도움이 됩니다.
이메일의 월 필드 형식을 Nov
로 변경해야 한다고 가정합니다. 11
대신 . 이 경우 PayslipMailer 클래스를 수정하면 PayslipGenerator 기능이 변경되거나 중단되지 않습니다.
코드를 작성할 때마다 나중에 질문하십시오. 이 수업의 책임은 무엇입니까? 답변에 "and"가 있는 경우 클래스를 여러 클래스로 분리합니다. 작은 클래스는 항상 크고 일반적인 클래스보다 낫습니다.
개방/폐쇄 원칙 - OCP
Bertrand Meyer는 Object-Oriented Software Construction이라는 책에서 개방/폐쇄 원칙을 창안했습니다.
원칙은 "소프트웨어 엔티티(클래스, 모듈, 기능 등)는 확장을 위해 열려야 하지만 수정을 위해 닫혀 있어야 합니다 ". 이것이 의미하는 바는 엔티티를 변경하지 않고도 행동을 변경할 수 있어야 한다는 것입니다.
위의 예에서 직원을 위한 급여 명세서 전송 기능이 있지만 모든 직원에게 매우 일반적입니다. 그러나 새로운 요구 사항이 발생합니다. 직원 유형에 따라 급여명세서를 생성해야 합니다. 정규직 직원과 계약업체에 대해 서로 다른 급여 생성 논리가 필요합니다. 이 경우 기존 PayrollGenerator를 수정하고 다음 기능을 추가할 수 있습니다.
class PayslipGenerator
def initialize(employee, month)
@employee = employee
@month = month
end
def generate_payslip
# Code to read from database,
# generate payslip
if employee.contractor?
# generate payslip for contractor
else
# generate a normal payslip
end
# and write it to a file
end
end
그러나 이것은 나쁜 패턴입니다. 그렇게 함으로써 우리는 기존 클래스를 수정하고 있습니다. 직원 계약에 따라 생성 로직을 더 추가해야 하는 경우 기존 클래스를 수정해야 하지만 그렇게 하는 것은 개방/폐쇄 원칙에 위배됩니다. 클래스를 수정하면 의도하지 않은 변경이 발생할 위험이 있습니다. 무언가가 변경되거나 추가되면 기존 코드에 알 수 없는 문제가 발생할 수 있습니다. 이러한 if-else는 동일한 클래스 내의 더 많은 위치에 있을 수 있습니다. 따라서 새 직원 유형을 추가할 때 이러한 if-else가 있는 위치를 놓칠 수 있습니다. 그것들을 모두 찾아서 수정하는 것은 위험할 수 있고 문제를 일으킬 수 있습니다.
기능을 확장하여 기능을 추가할 수 있지만 엔터티를 변경하지 않도록 이 코드를 리팩토링할 수 있습니다. 따라서 이들 각각에 대해 별도의 클래스를 만들고 동일한 generate
각각의 방법:
class ContractorPayslipGenerator
def initialize(employee, month)
@employee = employee
@month = month
end
def generate
# Code to read from the database,
# generate payslip
# and write it to a file
end
end
class FullTimePayslipGenerator
def initialize(employee, month)
@employee = employee
@month = month
end
def generate
# Code to read from the database,
# generate payslip
# and write it to a file
end
end
이러한 메소드 이름이 동일한지 확인하십시오. 이제 다음 클래스를 사용하도록 PayslipGenerator 클래스를 변경합니다.
GENERATORS = {
'full_time' => FullTimePayslipGenerator,
'contractor' => ContractorPayslipGenerator
}
class PayslipGenerator
def initialize(employee, month)
@employee = employee
@month = month
end
def generate_payslip
# Code to read from database,
# generate payslip
GENERATORS[employee.type].new(employee, month).generate()
# and write it to a file
end
end
여기에 직원 유형에 따라 호출할 클래스를 매핑하는 GENERATORS 상수가 있습니다. 호출할 클래스를 결정하는 데 사용할 수 있습니다. 이제 새 기능을 추가해야 할 때 해당 클래스를 새로 만들고 GENERATORS 상수에 추가하면 됩니다. 이것은 무언가를 깨뜨리거나 기존 논리에 대해 생각할 필요 없이 클래스를 확장하는 데 도움이 됩니다. 모든 유형의 급여 명세서 생성기를 쉽게 추가하거나 제거할 수 있습니다.
리스코프 치환 원리 - LSP
Liskov 대체 원칙은 "S가 T의 하위 유형인 경우 유형 T의 객체는 유형 S의 객체로 대체될 수 있습니다" .
이 원리를 이해하려면 먼저 문제를 이해해야 합니다. 개방형/폐쇄형 원칙에 따라 확장할 수 있는 방식으로 소프트웨어를 설계했습니다. 특정 작업을 수행하는 하위 클래스 Payslip 생성기를 만들었습니다. 호출자의 경우 호출하는 클래스를 알 수 없습니다. 호출자가 차이점을 알 수 없도록 이러한 클래스는 동일한 동작을 가져야 합니다. 행동이란 클래스의 메서드가 일관되어야 함을 의미합니다. 이러한 클래스의 메서드는 다음과 같은 특성을 가져야 합니다.
- 동일한 이름
- 동일한 데이터 유형으로 동일한 수의 인수 사용
- 동일한 데이터 유형 반환
급여명세서 생성기의 예를 살펴보자. 두 개의 발전기가 있습니다. 하나는 정규직 직원을 위한 것이고 다른 하나는 계약업체를 위한 것입니다. 이제 이러한 급여 명세서가 일관된 동작을 하도록 하려면 기본 클래스에서 급여명세서를 상속해야 합니다. User
라는 기본 클래스를 정의하겠습니다. .
class User
def generate
end
end
열기/닫기 원칙의 예에서 만든 하위 클래스에는 기본 클래스가 없습니다. 기본 클래스 User
를 갖도록 수정합니다. :
class ContractorPayslipGenerator < User
def generate
# Code to generate payslip
end
end
class FullTimePayslipGenerator < User
def generate
# Code to generate payslip
end
end
다음으로 User
를 상속하는 모든 하위 클래스에 필요한 메서드 집합을 정의합니다. 수업. 기본 클래스에서 이러한 메서드를 정의합니다. 우리의 경우에는 generate라는 단일 메소드만 필요합니다.
class User
def generate
raise "NotImplemented"
end
end
여기에서 raise
가 있는 생성 메서드를 정의했습니다. 성명. 따라서 기본 클래스를 상속하는 모든 하위 클래스에는 생성 메서드가 있어야 합니다. 존재하지 않으면 메서드가 구현되지 않았다는 오류가 발생합니다. 이런 식으로 하위 클래스가 일관성이 있는지 확인할 수 있습니다. 이를 통해 호출자는 항상 generate
방법이 있습니다.
이 원칙은 많은 부분을 변경할 필요 없이 문제를 일으키지 않고 하위 클래스를 쉽게 대체하는 데 도움이 됩니다.
인터페이스 분리 원칙 - ISP
인터페이스 분리 원칙은 정적 언어에 적용되며 루비는 동적 언어이므로 인터페이스 개념이 없습니다. 인터페이스는 클래스 간의 추상화 규칙을 정의합니다.
원칙은 다음과 같이 명시합니다.
<블록 인용>클라이언트가 사용하지 않는 인터페이스에 강제로 의존해서는 안 됩니다. - 로버트 C. 마틴
이것이 의미하는 바는 모든 클래스가 사용할 수 있는 일반화된 인터페이스보다 많은 인터페이스를 갖는 것이 더 낫다는 것입니다. 일반화된 인터페이스를 정의하면 클래스는 사용하지 않는 정의에 의존해야 합니다.
Ruby에는 인터페이스가 없지만 유사한 것을 빌드하기 위해 클래스 및 하위 클래스 개념을 살펴보겠습니다.
Liskov 대체 원칙에 사용된 예에서 FullTimePayslipGenerator
하위 클래스가 일반 클래스 User에서 상속되었습니다. 그러나 User는 매우 일반적인 클래스이며 다른 메서드를 포함할 수 있습니다. Leave
와 같은 다른 기능이 있어야 하는 경우 , User의 하위 클래스여야 합니다. Leave
생성 메소드가 필요하지 않지만 이 메소드에 종속됩니다. 따라서 일반 클래스를 사용하는 대신 다음과 같은 특정 클래스를 사용할 수 있습니다.
class Generator
def generate
raise "NotImplemented"
end
end
class ContractorPayslipGenerator < Generator
def generate
# Code to generate payslip
end
end
class FullTimePayslipGenerator < Generator
def generate
# Code to generate payslip
end
end
이 생성기는 급여명세서 생성에만 해당되며 하위 클래스는 일반 User
에 의존할 필요가 없습니다. 수업.
종속성 반전 원리 - DIP
종속성 반전은 소프트웨어 모듈을 분리하는 데 적용되는 원칙입니다.
<블록 인용>고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.
위에서 설명한 원칙을 사용하는 설계는 우리를 종속성 반전 원칙으로 안내합니다. 단일 책임을 가진 모든 클래스가 작동하려면 다른 클래스의 작업이 필요합니다. 급여를 생성하려면 데이터베이스에 액세스해야 하며 보고서가 생성되면 파일에 기록해야 합니다. 단일 책임 원칙에 따라 단일 클래스에 하나의 작업만 수행하려고 합니다. 단, 데이터베이스 읽기, 파일 쓰기 등은 동일한 클래스 내에서 수행되어야 합니다.
이러한 종속성을 제거하고 주요 비즈니스 로직을 분리하는 것이 중요합니다. 이렇게 하면 코드가 변경되는 동안 유동적이며 변경을 예측할 수 있게 됩니다. 종속성은 반전되어야 하며 모듈의 호출자는 종속성을 제어해야 합니다. 급여명세서 생성기에서 종속성은 보고서의 데이터 소스입니다. 이 코드는 호출자가 소스를 지정할 수 있는 방식으로 구성되어야 합니다. 종속성의 제어는 반전되어야 하며 호출자가 쉽게 수정할 수 있습니다.
위의 예에서 ContractorPayslipGenerator
모듈은 데이터를 읽을 위치와 출력을 저장하는 방법을 결정하는 종속성을 제어합니다. 이를 되돌리려면 UserReader
를 생성하겠습니다. 사용자 데이터를 읽는 클래스:
class UserReader
def get
raise "NotImplemented"
end
end
이제 이것이 Postgres에서 데이터를 읽기를 원한다고 가정해 보겠습니다. 이를 위해 UserReader의 하위 클래스를 만듭니다.
class PostgresUserReader < UserReader
def get
# Code to read data from Postgres
end
end
마찬가지로 FileUserReader
의 리더를 가질 수 있습니다. , InMemoryUserReader
, 또는 우리가 원하는 다른 유형의 리더. 이제 FullTimePayslipGenerator
를 수정해야 합니다. PostgresUserReader
를 사용하도록 클래스 종속성으로.
class FullTimePayslipGenerator < Generator
def initialize(datasource)
@datasource = datasource
end
def generate
# Code to generate payslip
data = datasource.get()
end
end
호출자는 이제 PostgresUserReader
를 전달할 수 있습니다. 종속성:
datasource = PostgresUserReader.new()
FullTimePayslipGenerator.new(datasource)
호출자는 종속성을 제어할 수 있으며 필요할 때 소스를 쉽게 변경할 수 있습니다.
종속성을 반전시키는 것은 클래스에만 적용되는 것은 아닙니다. 또한 구성을 반전해야 합니다. 예를 들어 Postgres 서버를 연결하는 동안 DBURL, 사용자 이름 및 암호와 같은 특정 구성이 필요합니다. 클래스에서 이러한 구성을 하드코딩하는 대신 호출자로부터 전달해야 합니다.
class PostgresUserReader < UserReader
def initialize(config)
config = config
end
def get
# initialize DB with the config
self.config
# Code to read data from Postgres
end
end
호출자가 구성을 제공합니다.
config = { url: "url", user: "user" }
datasource = PostgresUserReader.new(config)
FullTimePayslipGenerator.new(datasource)
이제 호출자가 종속성을 완전히 제어할 수 있으며 변경 관리가 쉽고 덜 고통스럽습니다.
결론
SOLID 디자인은 코드를 분리하고 변경을 덜 고통스럽게 만드는 데 도움이 됩니다. 분리되고 재사용 가능하며 변화에 반응하는 방식으로 프로그램을 설계하는 것이 중요합니다. 5가지 SOLID 원칙은 모두 서로를 보완하며 공존해야 합니다. 잘 설계된 코드베이스는 유연하고 변경하기 쉽고 재미있게 작업할 수 있습니다. 새로운 개발자라면 누구나 코드를 쉽게 이해할 수 있습니다.
SOLID가 어떤 유형의 문제를 해결하고 우리가 이것을 하는 이유를 이해하는 것은 정말 중요합니다. 문제를 이해하면 설계 원칙을 수용하고 더 나은 소프트웨어를 설계하는 데 도움이 됩니다.