업데이트 방법에 반환 유형을 추가하면 "단일 책임 원칙"을 위반합니까?


37

데이터베이스의 직원 데이터를 업데이트하는 방법이 있습니다. Employee"업데이트"개체 수단이 실제로 새로운 객체를 인스턴스화 있도록 클래스는 불변이다.

내가 원하는 Update방법의 새로운 인스턴스를 반환하는 Employee업데이트 된 데이터와 함께,하지만 지금부터 나는 방법의 책임이 말할 수있는 직원 데이터를 업데이트 할 수 새로운 데이터베이스에서 가져 Employee객체를 가 위반 않고, 단일 책임 원칙을 ?

DB 레코드 자체가 업데이트됩니다. 그런 다음이 레코드를 나타 내기 위해 새 객체가 인스턴스화됩니다.


5
여기에 약간의 의사 코드가 먼 길을 갈 수 있습니다. 실제로 새로운 DB 레코드가 생성되거나 DB 레코드 자체가 업데이트되지만 "클라이언트"코드에서 클래스가 불변으로 모델링되어 새 객체가 생성됩니까?
Martin Ba

Martin Bra가 요청한 것 외에도 데이터베이스를 업데이트 할 때 왜 Employee 인스턴스를 반환합니까? Employee 인스턴스의 값은 Update 메서드에서 변경되지 않았으므로 반환하는 이유는 무엇입니까 (호출자는 이미 인스턴스에 액세스 할 수 있습니다 ...). 아니면 update 메소드가 데이터베이스에서 (잠재적으로 다른) 값을 검색합니까?
Thorsal

1
@Thorsal : 데이터 엔터티가 변경 불가능한 경우 업데이트 된 데이터가 포함 된 인스턴스를 반환하는 것은 거의 SOP입니다. 그렇지 않으면 수정 된 데이터의 인스턴스화를 직접 수행해야하기 때문입니다.
mikołak

21
Compare-and-swaptest-and-set멀티 스레드 프로그래밍 이론의 기본적인 작업이다. 둘 다 리턴 유형의 업데이트 메소드이며 그렇지 않으면 작동하지 않습니다. 이것이 명령 쿼리 분리 또는 단일 책임 원칙과 같은 개념을 위반합니까? 예, 그게 요점 입니다. SRP는 보편적으로 좋은 것이 아니며 실제로 적극적으로 해로울 수 있습니다.
MSalters

3
@MSalters : 맞습니다. 명령 / 쿼리 분리는 dem 등원으로 인식 될 수있는 쿼리를 발행하고 응답을 기다릴 필요없이 명령을 발행 할 수 있어야하지만, 원자 읽기-수정-쓰기는 세 번째 범주의 조작으로 인식되어야합니다.
supercat

답변:


16

다른 규칙과 마찬가지로, 여기서 중요한 것은 규칙의 목적과 정신을 고려하고 일부 교과서에서 규칙이 어떻게 표현되었는지 와이 사건에 적용하는 방법을 정확하게 분석하는 데 미치 지 않는 것입니다. 우리는 변호사처럼 이것에 접근 할 필요가 없습니다. 규칙의 목적은 우리가 더 나은 프로그램을 작성하도록 돕는 것입니다. 프로그램을 작성하는 목적이 규칙을 지키는 것과는 다릅니다.

단일 책임 규칙의 목적은 각 기능이 하나의 독립적이고 일관된 작업을 수행하도록하여 프로그램을 이해하고 유지하기 쉽게 만드는 것입니다.

예를 들어, 한 번 주문이 보류 중인지, 배송되었는지, 이월 주문인지 등을 확인한 코드를 반환하는 "checkOrderStatus"와 같은 함수를 작성했습니다. 그런 다음 다른 프로그래머가 와서이 기능을 수정하여 주문이 운송 될 때 수량을 갱신했습니다. 이것은 단일 책임 원칙을 심각하게 위반했습니다. 나중에 해당 코드를 읽는 다른 프로그래머는 함수 이름을보고 리턴 값이 사용 된 방법을보고 데이터베이스 업데이트를 수행 한 것으로 의심하지 않을 수 있습니다. 주문 수량을 업데이트하지 않고 주문 상태를 얻어야하는 사람은 어색한 위치에있을 것입니다. 주문 상태 부분을 복제하는 새 함수를 작성해야합니까? db 업데이트 수행 여부를 알려주는 플래그를 추가 하시겠습니까? 기타.

다른 한편으로, 나는 "두 가지"를 구성하는 것을 nitpick하지 않을 것입니다. 최근에 우리 시스템에서 고객의 시스템으로 고객 정보를 보내는 기능을 작성했습니다. 이 기능은 요구 사항을 충족시키기 위해 데이터를 일부 재 포맷합니다. 예를 들어, 데이터베이스에 널 (null) 일 수있는 필드가 있지만 널을 허용하지 않으므로 "지정되지 않음"이라는 더미 텍스트를 채워야합니다. 그렇지 않으면 정확한 단어를 잊어 버립니다. 이 기능은 데이터를 다시 포맷하고 전송하는 두 가지 작업을 수행합니다. 그러나 나는 이것을 다시 포맷하지 않고 보내지 않기를 원하기 때문에 이것을 "재 포맷"하고 "보내기"하는 것이 아니라 하나의 함수에 의도적으로 넣었습니다. 누군가 새 전화를 걸고 싶지 않아서 다시 포맷하고 전화를해야한다는 사실을 알지 못합니다.

귀하의 경우 데이터베이스를 업데이트하고 작성된 레코드의 이미지를 논리적이고 불가피하게 함께 두 가지처럼 보이는 것처럼 반환하십시오. 나는 당신의 응용 프로그램의 세부 사항을 알지 못하므로 이것이 좋은 생각인지 아닌지 확실하게 말할 수는 없지만 그럴듯하게 들립니다.

레코드에 대한 모든 데이터를 보유하는 오브젝트를 메모리에 작성하고이를 호출하기 위해 데이터베이스 호출을 수행 한 다음 오브젝트를 리턴하는 경우 이는 의미가 있습니다. 당신은 당신의 손에 물건이 있습니다. 그냥 돌려주지 그래? 객체를 반환하지 않으면 발신자가 어떻게 얻을 수 있습니까? 방금 쓴 개체를 얻으려면 데이터베이스를 읽어야합니까? 다소 비효율적 인 것 같습니다. 그는 어떻게 그 기록을 찾습니까? 기본 키를 알고 있습니까? 누군가가 쓰기 기능이 기본 키를 반환하여 레코드를 다시 읽을 수있는 것이 "법적"이라고 선언하면 왜 전체 레코드를 반환해야합니까? 차이점이 뭐야?

반면에 객체를 만드는 것이 데이터베이스 레코드를 쓰는 것과 별개의 작업이고 호출자가 쓰기를 원하지만 객체를 만들지 않는 것이 좋으면 낭비가 될 수 있습니다. 호출자가 객체를 원하지만 쓰기는하지 않을 경우 객체를 얻는 다른 방법을 제공해야합니다. 이는 중복 코드 작성을 의미 할 수 있습니다.

그러나 시나리오 1이 더 가능성이 높다고 생각하므로 아무 문제가 없습니다.


모든 답변에 감사드립니다.하지만이 답변이 가장 도움이되었습니다.
Sipo

다시 포맷하지 않고 보내지 않고 나중에 데이터를 보내는 것 외에 다시 포맷하는 데 사용하지 않는 경우 두 가지가 아니라 한 가지입니다.
Joker_vD

public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
CJ Dennis

트윗 담아 가기 사실 그것은 내가 한 일입니다. 서식을 지정하는 기능, 실제 전송을 수행하는 기능 및 여기에 들어 가지 않을 두 가지 다른 기능이 있습니다. 그런 다음 하나의 최상위 함수가 모두 적절한 순서로 호출합니다. "포맷 및 전송"은 하나의 논리 연산이므로 하나의 기능으로 올바르게 결합 될 수 있습니다. 그것이 2라고 주장한다면, 합리적 인 일은 여전히 ​​모든 것을 수행하는 하나의 최상위 기능을 갖는 것입니다.
Jay

67

SRP의 개념은 모듈을 개별적으로 수행 할 때 2 가지 다른 일을하는 모듈을 중지시켜 유지 보수를 개선하고 시간에 스파게티를 줄이는 것입니다. SRP가 "변경해야 할 이유"라고 말합니다.

귀하의 경우, "업데이트 된 개체를 업데이트하고 반환하는"루틴이있는 경우 여전히 개체를 한 번 변경하여 변경해야 할 이유가 한 가지 있습니다. 객체를 바로 돌려주는 것은 여기도없고 거기도 없으며, 여전히 단일 객체를 조작하고 있습니다. 코드는 단 하나의 일에 대해서만 책임이 있습니다.

SRP는 실제로 모든 작업을 단일 통화로 줄이기위한 것이 아니라 운영중인 작업과 운영 방식을 줄이는 것입니다. 따라서 업데이트 된 객체를 반환하는 단일 업데이트로도 충분합니다.


7
또는 "모든 작업을 단일 호출로 줄이기 위해 노력하는 것"이 ​​아니라 문제가있는 항목을 사용할 때 고려해야 할 여러 가지 사례를 줄이는 것입니다.
jpmc26

46

항상 그렇듯이 이것은 정도의 문제입니다. SRP는 외부 데이터베이스에서 레코드를 검색하고 빠른 푸리에 변환을 수행하며 결과로 글로벌 통계 레지스트리를 업데이트하는 메소드 작성을 중지해야합니다. 저는 거의 모든 사람들이이 일들이 다른 방법으로 이루어져야한다는 데 동의 할 것이라고 생각합니다. 각 방법에 대한 단일 책임을 가정하는 것은 그 시점에서 가장 경제적이고 기억에 남는 방법입니다.

스펙트럼의 다른 끝에는 물체의 상태에 대한 정보를 산출하는 방법이 있습니다. 일반적 isActive으로이 정보는 단일 책임으로 제공됩니다. 아마 모든 사람들은 이것이 괜찮다는 데 동의합니다.

이제 일부는 성공 플래그를 리턴하는 것이 성공이보고되는 조치를 수행하는 것과 다른 책임을 고려할 수 있도록 원칙을 확장합니다. 매우 엄격한 해석 하에서 이것은 사실이지만, 대안은 성공 상태를 얻기 위해 두 번째 방법을 호출해야하므로 호출자를 복잡하게 만듭니다. 많은 프로그래머는 부작용이있는 방법에서 성공 코드를 반환하는 것으로 완벽합니다.

새로운 책임을 돌려주는 것은 여러 책임으로 나아가는 한 걸음 더 나아간 것입니다. 호출자가 전체 오브젝트에 대해 두 번째 호출을하도록 요구하는 것은 첫 번째 호출이 성공했는지 여부를 확인하기 위해 두 번째 호출을 요구하는 것보다 약간 더 합리적입니다. 아직도 많은 프로그래머들은 업데이트 결과를 완벽하게 반환하는 것을 고려할 것입니다. 이것은 약간 다른 두 가지 책임으로 해석 될 수 있지만, 처음부터 그 원칙에 영감을 준 것은 심각한 학대가 아닙니다.


15
첫 번째 방법이 성공했는지 확인하기 위해 두 번째 방법을 호출 해야하는 경우 두 번째 방법의 결과를 얻기 위해 세 번째 방법을 호출하지 않아도됩니까? 그럼 당신이 실제로 결과를 원한다면 , 잘 ...

3
SRP를 과도하게 적용하면 수많은 작은 클래스가 생겨서 부담이된다고 덧붙일 수 있습니다. 부담은 환경, 컴파일러, IDE / 헬퍼 툴 등에 따라 다릅니다.
Erik Alapää

8

단일 책임 원칙을 위반합니까?

반드시 그런 것은 아닙니다. 무엇이든 이것은 명령 쿼리 분리 원칙을 위반합니다 .

책임은

직원 데이터를 업데이트

이 책임에는 작업 상태가 포함된다는 암묵적인 이해; 예를 들어, 작업이 실패하면 예외가 발생하고 성공하면 업데이트 직원이 반환됩니다.

다시, 이것은 모두 정도와 주관적인 판단의 문제입니다.


명령 쿼리 분리는 어떻습니까?

글쎄, 그 원칙이 존재하지만 업데이트 결과를 반환하는 것은 실제로 매우 일반적입니다.

(1) Java Set#add(E)는 요소를 추가하고 세트에 이전 포함을 반환합니다.

if (visited.add(nodeToVisit)) {
    // perform operation once
}

이것은 두 가지 조회를 수행해야하는 CQS 대안보다 효율적입니다.

if (!visited.contains(nodeToVisit)) {
    visited.add(nodeToVisit);
    // perform operation once
}

(2) Compare-and-swap , fetch-and-addtest-and-set 은 동시 프로그래밍이 가능하도록하는 일반적인 기본 요소입니다. 이러한 패턴은 낮은 수준의 CPU 명령에서 높은 수준의 동시 수집에 이르기까지 자주 나타납니다.


2

단일 책임은 여러 가지 이유로 변경하지 않아도되는 수업에 대한 것입니다.

예를 들어 직원에게는 전화 번호 목록이 있습니다. 전화 번호를 처리하는 방식이 직원 등급을 전혀 바꾸지 않아야하는 국가 전화 코드를 추가 할 수 있습니다.

직원 클래스가 데이터베이스에 어떻게 저장되는지 알아야 할 필요가 없습니다. 직원의 변경 사항과 데이터 저장 방법의 변경 사항에 따라 변경되기 때문입니다.

마찬가지로 직원 클래스에는 CalculateSalary 메서드가 없어야합니다. 급여 재산은 괜찮지 만 세금 등의 계산은 다른 곳에서 수행해야합니다.

그러나 Update 메소드는 방금 업데이트 한 것을 반환합니다.


2

Wrt. 구체적인 경우 :

Employee Update(Employee, name, password, etc) (매우 많은 매개 변수가 있기 때문에 실제로 빌더 사용).

보인다Update 방법은 기존의 소요 Employee(?) 기존의 직원이 직원에 대한 변화에 파라미터 세트를 식별하기 위해 첫 번째 매개 변수로.

내가 이 생각 할 수 완료 청소기. 두 가지 경우가 있습니다.

(a) Employee 실제로 데이터베이스에서 식별 할 수있는 데이터베이스 / 고유 ID를 포함합니다. 즉 , DB에서 찾기 위해 설정된 전체 레코드 값이 필요 하지 않습니다 .

이 경우 void Update(Employee obj)ID로 기존 레코드를 찾은 다음 전달 된 객체에서 필드를 업데이트 하는 방법을 선호 합니다. 아니면void Update(ID, EmployeeBuilder values)

내가 찾은 이것의 변형은 void Put(Employee obj)(ID 별) 레코드의 존재 여부에 따라 삽입하거나 업데이트 하는 방법 만 사용하는 것입니다.

(b) DB 조회에는 기존의 전체 레코드가 필요합니다.이 경우 여전히 다음과 같은 것이 더 합리적 일 수 있습니다 void Update(Employee existing, Employee newData).

여기에서 볼 수있는 한, 실제로 저장하고 실제로 저장하기 위해 새 객체 (또는 하위 객체 값)를 작성하는 책임은 직교하므로 분리 할 수 ​​있습니다.

다른 답변 (원자 세트 및 검색 / 비교 스왑 등)에서 언급 한 동시 요구 사항은 지금까지 작업 한 DB 코드에서 문제가되지 않았습니다. DB와 대화 할 때 개별 문 수준이 아닌 트랜잭션 수준에서 처리해야한다고 생각합니다. ( '원자'라는 Employee[existing data] Update(ID, Employee newData)말이 의미가없는 디자인이 없을 수도 있지만, DB에 액세스하면 일반적으로 볼 수있는 것이 아닙니다.)


1

지금까지 모든 사람들이 수업에 대해 이야기하고 있습니다. 그러나 인터페이스 관점에서 생각하십시오.

인터페이스 메소드가 리턴 유형 및 / 또는 리턴 값에 대한 내재적 약속을 선언하면 모든 구현에서 업데이트 된 오브젝트를 리턴해야합니다.

그래서 당신은 요청할 수 있습니다 :

  • 새로운 객체를 반환하고 싶지 않은 가능한 구현을 생각할 수 있습니까?
  • 업데이트 된 직원이 반환 값으로 필요하지 않은 인터페이스 (인스턴스 주입을 통해)에 의존하는 구성 요소를 생각할 수 있습니까?

단위 테스트를위한 모의 객체에 대해서도 생각해보십시오. 분명히 반환 값없이 조롱하는 것이 더 쉬울 것입니다. 또한 주입 된 종속성의 인터페이스가 더 간단한 경우 종속 구성 요소를 테스트하기가 더 쉽습니다.

이러한 고려 사항에 따라 결정을 내릴 수 있습니다.

나중에 후회하더라도 두 어댑터 사이에 어댑터가있는 두 번째 인터페이스를 계속 도입 할 수 있습니다. 물론 이러한 추가 인터페이스는 구성 복잡성을 증가 시키므로 모든 절충점입니다.


0

당신의 접근은 괜찮습니다. 불변성은 강력한 주장이다. 내가 묻는 유일한 것은 : 객체를 구성하는 다른 장소가 있습니까? 객체가 변경 불가능한 경우 "상태"가 도입되었으므로 추가 질문에 대답해야합니다. 그리고 객체의 상태 변화는 다른 이유로 발생할 수 있습니다. 그런 다음 사례를 알아야하며 중복되거나 분할되어서는 안됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.