단일 책임 원칙의 적용


40

최근에는 사소한 건축 문제가 발생했습니다. 내 코드에는 다음과 같은 간단한 저장소가 있습니다 (코드는 C #입니다).

var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();

SaveChanges 데이터베이스 변경 사항을 커밋하는 간단한 래퍼였습니다.

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
}

그런 다음 얼마 후에 사용자가 시스템에서 생성 될 때마다 전자 메일 알림을 보내는 새로운 논리를 구현해야했습니다. 많은 전화가 있었다 때문에 _userRepository.Add()SaveChanges시스템의 주위에, 나는 업데이트하기로 결정 SaveChanges다음과 같이 :

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
    foreach (var newUser in dataContext.GetAddedUsers())
    {
       _eventService.RaiseEvent(new UserCreatedEvent(newUser ))
    }
}

이런 식으로 외부 코드는 UserCreatedEvent를 구독하고 알림을 보내는 데 필요한 비즈니스 로직을 처리 할 수 ​​있습니다.

그러나 내가 수정 한 내용 SaveChanges이 단일 책임 원칙 을 위반 한 것으로 나타 났 으며, 이는 SaveChanges어떠한 사건도 막고 저장하지 않아야한다는 점을 지적했습니다 .

이것이 유효한 포인트입니까? 여기서 이벤트를 발생시키는 것은 로깅과 본질적으로 같은 것 같습니다. 기능에 일부 기능을 추가하는 것입니다. 그리고 SRP는 함수에서 로깅 또는 실행 이벤트를 사용하는 것을 금지하지 않으며 그러한 논리를 다른 클래스로 캡슐화해야한다고 저장소가 다른 클래스를 호출하는 것이 좋습니다.


22
당신의 레토르트는 " 좋습니다. 어떻게 SRP를 위반하지 않고 여전히 단일 수정 지점을 허용하도록 작성 하시겠습니까?"
Robert Harvey

43
내 관찰은 이벤트를 제기해도 추가 책임이 추가되지 않는다는 것입니다. 실제로는 정반대입니다. 책임을 다른 곳에서 위임합니다.
Robert Harvey

귀하의 동료가 옳다고 생각하지만 귀하의 질문은 유효하고 유용합니다.
Andres F.

16
단일 책임에 대한 결정적인 정의는 없습니다. SRP를 위반한다고 지적한 사람은 개인 정의를 사용하여 정확하며 자신의 정의를 사용하여 올바른 것입니다. 이 이벤트는 다른 유사한 기능이 다른 방식으로 수행되는 일회성 이벤트가 아니라는 점에서 디자인이 완벽하다고 생각합니다. 일관성을 유지하는 것이 SRP와 같은 모호한 지침보다주의를 기울이는 것보다 훨씬 중요합니다.
덩크

답변:


14

예, Add또는 특정 작업에서 특정 이벤트를 발생시키는 저장소를 보유하는 것이 유효한 요구 사항 일 수 있습니다. SaveChanges사용자를 추가하고 이메일을 보내는 특정 예가 조금 생각했다. 다음에서는 시스템의 맥락에서이 요구 사항이 완벽하게 정당화되었다고 가정합니다.

그래서 , 로깅뿐만 아니라 하나의 방법으로 절약뿐만 아니라 이벤트 역학을 인코딩하는 SRP에 위배 . 많은 경우, 특히 "변경 사항 저장"및 "이벤트 발생"의 유지 관리 책임을 다른 팀 / 유지 업체에게 배포하려는 사람이없는 경우에는 허용되는 위반 일 수 있습니다. 그러나 언젠가 누군가가 정확히 이것을하고 싶다고 가정 해보십시오. 아마도 그러한 우려의 코드를 다른 클래스 라이브러리에 넣음으로써 간단한 방식으로 해결할 수 있습니까?

이에 대한 해결책은 원본 리포지토리가 데이터베이스에 대한 변경 사항을 커밋하는 책임을 유지하고 다른 공용 인터페이스를 가진 프록시 리포지토리를 만들고 원본 리포지토리를 재사용하며 메서드에 추가 이벤트 메커니즘을 추가하는 것입니다.

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

프록시 클래스 a를 호출 NotifyingRepository하거나 원하는 ObservableRepository경우 @Peter의 투표권높은 답변 (실제로 SRP 위반을 해결하는 방법을 알려주지 않고 위반이 정상이라고 말함)을 따라 전화를 걸 수 있습니다.

기존의 프록시 패턴 설명같이 새 저장소 클래스와 기존 저장소 클래스는 모두 공통 인터페이스에서 파생되어야합니다 .

그런 다음 원래 코드 _userRepository에서 새 EventFiringUserRepo클래스 의 객체로 초기화하십시오 . 이렇게하면 원래 저장소를 이벤트 메커니즘과 분리하여 유지합니다. 필요한 경우 이벤트 발생 저장소와 원본 저장소를 나란히두고 호출자가 이전 또는 후자를 사용할지 결정할 수 있습니다.

주석에 언급 된 한 가지 우려를 해결하기 위해 : 프록시 위에 프록시 위에 프록시가 생기지 않습니까? 실제로 이벤트 메커니즘을 추가하면 단순히 이벤트를 구독하여 "전자 메일 보내기"유형의 추가 요구 사항을 추가 할 수있는 기반이 만들어 지므로 추가 프록시없이 해당 요구 사항을 SRP에 고수 할 수 있습니다. 그러나 여기에 한 번 추가해야 할 것은 이벤트 메커니즘 자체입니다.

이러한 종류의 분리가 실제로 시스템의 맥락에서 가치가 있다면 귀하와 검토자는 스스로 결정해야합니다. 아마도 로깅을 리스너 이벤트에 추가하지 않고 다른 프록시를 사용하여 원본 코드와 로깅을 분리하지는 않을 것입니다.


3
이 답변 외에도. AOP 와 같은 프록시에 대한 대안이 있습니다 .
Laiv

1
이벤트를 올리는 것이 SRP를 어기는 것이 아니라 "새로운"사용자에 대해서만 이벤트를 발생시키는 것은 Repo가 "Newly Added Me "사용자
Ewan

@ 완 : 질문을 다시 읽으십시오. 그것은 코드의 한 장소에 관한 것이며, 그 행동의 책임 이외의 다른 행동과 결합되어야하는 특정 행동을 수행했습니다. 그리고 동료 검토자가 SRP를 위반하는 것으로 조치와 이벤트 제기를 한 곳에 두는 것이 의문의 여지가있었습니다. "새 사용자 저장"의 예는 데모 목적으로 만 사용되며 원하는 경우 예를 들어 설명하는 것이지만 이는 문제의 핵심이 아닙니다.
Doc Brown

2
이것이 최선의 정답 IMO입니다. SRP를 유지 관리 할뿐만 아니라 개방 / 폐쇄 원칙도 유지합니다. 또한 클래스 내에서 변경 될 수있는 모든 자동화 된 테스트를 생각해보십시오. 새로운 기능이 추가 될 때 기존 테스트를 수정하는 것은 큰 냄새입니다.
Keith Payne

1
진행중인 트랜잭션이있는 경우이 솔루션은 어떻게 작동합니까? 그이 진행되면, SaveChanges()실제로 데이터베이스 레코드를 생성하지 않으며, 압연 등을 치울 수있다. AcceptAllChangesTransactionCompleted 이벤트를 재정의 하거나 구독 해야 할 것 같습니다 .
John Wu

29

영구 데이터 저장소가 변경되었다는 알림을 보내는 것은 저장시 수행해야 할 합리적인 것 같습니다.

물론 Add를 특별한 경우로 취급해서는 안됩니다. Modify 및 Delete 이벤트도 실행해야합니다. 냄새가 나는 "추가"사례의 특수 처리 방식으로 독자가 냄새가 나는 이유를 설명하도록 강요하며 궁극적으로 코드의 일부 독자가 SRP를 위반해야한다는 결론을 내립니다.

변경, 조회, 변경 및 이벤트를 발생시킬 수있는 "알림"저장소는 완벽하게 정상적인 객체입니다. 적절한 규모의 프로젝트에서 다양한 변형을 찾을 수 있습니다.


그러나 실제로 "알림"저장소가 필요한 것입니까? 당신은 C #을 언급했습니다. 많은 사람들이 후자가 필요할 때 System.Collections.ObjectModel.ObservableCollection<>대신에 대신 사용하는 System.Collections.Generic.List<>것이 모든 종류의 나쁘고 잘못이지만 SRP를 즉시 지적하는 사람은 거의 없다는 데 동의합니다 .

당신이 지금하고있는 일은와를 바꾸는 것 UserList _userRepository입니다 ObservableUserCollection _userRepository. 이것이 최선의 조치인지 아닌지는 응용 프로그램에 따라 다릅니다. 그러나 의심 할 여지없이 _userRepository상당히 덜 가벼워 지지만 겸손한 견해로는 SRP를 위반하지 않습니다.


사용의 문제점 ObservableCollection이 경우에 그것이없는 호출에서 해당 이벤트를 트리거한다 SaveChanges하지만 호출의 Add예에 도시 된 것과 매우 다른 동작이 발생할 것이다. 내 대답을 참조하여 원래 repo를 경량으로 유지하고 의미를 그대로 유지하여 SRP를 고수하십시오.
Doc Brown

@DocBrown 나는 알려진 클래스 ObservableCollection<>List<>비교 및 컨텍스트를 호출했습니다 . 내부 구현 또는 외부 인터페이스에 실제 클래스 사용을 권장하지는 않았습니다.
피터

그러나 OP가 "수정"및 "삭제"에 이벤트를 추가하더라도 (간단하게하기 위해 OP가 질문을 간결하게 유지하기 위해 생략 한 것으로 생각됨) 검토자가 쉽게 결론을 내릴 수 있다고 생각합니다. SRP 위반. 아마도 허용되는 것이지만 필요한 경우 해결할 수없는 것은 없습니다.
Doc Brown

16

그렇습니다. 이는 단일 책임 원칙을 위반하고 유효한 사항입니다.

더 나은 디자인은 별도의 프로세스가 저장소에서 '새 사용자'를 검색하고 이메일을 보내는 것입니다. 이메일, 실패, 재전송 등을 보낸 사용자 추적

이 방법으로 오류, 충돌 등을 처리 할 수있을뿐만 아니라 "데이터베이스에 무언가가 커밋 될 때"이벤트가 발생한다는 아이디어가있는 모든 요구 사항을 파악하는 리포지토리를 피할 수 있습니다.

리포지토리는 추가 한 사용자가 새로운 사용자라는 것을 모릅니다. 그 책임은 단순히 사용자를 저장하는 것입니다.

아래 주석을 확장하는 것이 좋습니다.

리포지토리는 추가 한 사용자가 새로운 사용자라는 것을 알지 못합니다. 예, 추가라는 메소드가 있습니다. 그 의미는 추가 된 모든 사용자가 새로운 사용자라는 것을 의미합니다. Save를 호출하기 전에 Add에 전달 된 모든 인수를 결합하면 모든 새로운 사용자가 생깁니다.

잘못되었습니다. "리포지토리에 추가됨"과 "신규"를 혼동하고 있습니다.

"리포지토리에 추가됨"은 그것이 말하는 것을 의미합니다. 다양한 리포지토리에 사용자를 추가 및 제거하고 다시 추가 할 수 있습니다.

"신규"는 비즈니스 규칙에 의해 정의 된 사용자의 상태입니다.

현재 비즈니스 규칙은 "신규 == 방금 저장소에 추가되었습니다"일 수 있지만 해당 규칙을 알고 적용하는 별도의 책임이 아님을 의미하지는 않습니다.

이런 종류의 데이터베이스 중심 사고를 피하기 위해주의해야합니다. 새로운 사용자가 아닌 사용자를 저장소에 추가하는 엣지 케이스 프로세스가 있으며 이메일을 보낼 때 모든 비즈니스에 "물론 '새로운'사용자가 아닙니다! 실제 규칙은 X입니다."

이 대답은 IMHO가 요점을 놓친 것입니다. repo는 코드에서 새로운 사용자가 추가되는시기를 정확히 알 수있는 중심 위치입니다.

잘못되었습니다. 위의 이유로, 단순히 이벤트를 발생시키는 것이 아니라 수업에 이메일 전송 코드를 포함시키지 않는 한 중앙 위치가 아닙니다.

리포지토리 클래스를 사용하지만 전자 메일을 보낼 코드가없는 응용 프로그램이 있습니다. 해당 응용 프로그램에서 사용자를 추가하면 이메일이 전송되지 않습니다.


11
리포지토리는 추가 한 사용자가 새로운 사용자라는 것을 알지 못합니다. 그렇습니다 Add. 그 의미는 추가 된 모든 사용자가 새로운 사용자라는 것을 의미합니다. Add전화 하기 전에 전달 된 모든 인수를 결합 Save하면 모든 새로운 사용자를 확보 할 수 있습니다.
Andre Borges

나는이 제안을 좋아한다. 그러나 실용주의는 순결보다 우세합니다. 상황에 따라 기존 애플리케이션에 완전히 새로운 아키텍처 계층을 추가하는 것은 사용자가 추가 될 때 문자 그대로 단일 이메일을 보내는 것만으로 정당화하기 어려울 수 있습니다.
Alexander

그러나 이벤트는 사용자가 추가되었다고 말하지 않습니다. 사용자가 만들었습니다. 이름을 올바르게 지정하는 것을 고려하고 add와 create의 의미상의 차이점에 동의하면 스 니펫의 이벤트 이름이 잘못되었거나 잘못 지정되었습니다. 나는 리뷰어가 notyfing 저장소에 대해 아무것도 가지고 있다고 생각하지 않습니다. 아마도 그것은 사건의 종류와 그 부작용에 대해 염려했습니다.
Laiv

7
@Andre는 레포에 처음이지만 비즈니스 의미에서 반드시 "새로운"것은 아닙니다. 언뜻보기에 추가 책임을 숨기고있는이 두 가지 아이디어의 혼란. 오래된 저장소 사용자를 새 저장소로 가져 오거나 사용자를 제거했다가 다시 추가 할 수 있습니다. "새 사용자"가 "dB에 추가 된 것"을 넘어서는 비즈니스 규칙이 있습니다.
Ewan

1
중재자 참고 : 귀하의 답변은 저널리즘 인터뷰가 아닙니다. 수정 사항이있는 경우 전체 "속보"효과를 만들지 않고 자연스럽게 답변에 포함시킵니다. 우리는 토론 포럼이 아닙니다.
Robert Harvey

7

이것이 유효한 포인트입니까?

예, 코드의 구조에 따라 크게 다르지만 나는 완전한 맥락을 가지고 있지 않으므로 일반적으로 이야기하려고 노력할 것입니다.

여기서 이벤트를 발생시키는 것은 로깅과 본질적으로 같은 것 같습니다. 기능에 일부 기능을 추가하는 것입니다.

절대 그렇지 않습니다. 로깅은 비즈니스 흐름의 일부가 아니며, 비활성화 될 수 있으며, (비즈니스) 부작용을 일으키지 않아야하며, 어떤 이유로 든 로깅 할 수없는 경우에도 애플리케이션의 상태 및 상태에 영향을 미치지 않아야합니다. 더 이상. 이제 추가 한 논리와 비교하십시오.

그리고 SRP는 함수에서 로깅 또는 실행 이벤트를 사용하는 것을 금지하지 않으며 그러한 논리를 다른 클래스로 캡슐화해야한다고 저장소가 다른 클래스를 호출하는 것이 좋습니다.

SRP는 ISP (SOLID의 S 및 I)와 함께 작동합니다. 당신은 매우 특정한 일을하고 다른 것을하지 않는 많은 클래스와 메소드로 끝납니다. 그것들은 매우 집중적이고 업데이트 나 교체가 쉽고 일반적으로 테스트하기가 쉽습니다. 물론 실제로 오케스트레이션을 처리하는 몇 가지 더 큰 클래스가 있습니다. 수많은 종속성이 있으며 원자화 조치가 아니라 비즈니스 조치에 중점을 두어 여러 단계가 필요할 수 있습니다. 비즈니스 컨텍스트가 명확하다면 단일 책임이라고도 할 수 있지만 코드가 커짐에 따라 올바르게 말하면 코드 중 일부를 새로운 클래스 / 인터페이스로 추상화 할 수 있습니다.

이제 특정 예로 돌아갑니다. 당신이 절대적으로 사용자가 어쩌면 다른 더 전문 작업을 수행 생성 될 때마다 알림을 전송해야하는 경우, 당신은이 요구 사항을 캡슐화하는 별도의 서비스, 같은 것을 만들 수있는 UserCreationService하나의 방법, 노출, Add(user)핸들 저장 모두 (전화를 단일 비즈니스 조치로 알림) 또는 원래 스 니펫에서 수행하십시오._userRepository.SaveChanges();


2
로깅은 비즈니스 흐름의 일부가 아닙니다. 이는 SRP와 관련하여 어떻게 관련이 있습니까? 내 이벤트의 목적이 새로운 사용자 데이터를 Google 애널리틱스에 전송하는 것이라면이를 사용 중지하면 로깅을 사용 중지하는 것과 동일한 비즈니스 효과를 얻을 수 있습니다. 함수에 새로운 로직을 추가 / 추가하지 않는 경험의 규칙은 무엇입니까? "비활성화하면 주요 비즈니스 부작용이 발생합니까?"
Andre Borges

2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting . 가짜 "뉴스"를 유발하는 조기 이벤트를 시작하면 어떻게 될까요? 분석에서 DB 트랜잭션 오류로 인해 생성되지 않은 "사용자"를 고려하면 어떻게됩니까? 회사가 부정확 한 데이터를 바탕으로 허위 건물에 대해 의사 결정을하는 경우 어떻게됩니까? 문제의 기술적 측면에 너무 집중하고 있습니다. "때로는 나무의 나무를 볼 수 없습니다"
Laiv

@ Laiv, 당신은 유효한 요점을 만들고 있지만 이것은 내 질문이나 대답의 요점이 아닙니다. 문제는 이것이 SRP와 관련하여 유효한 솔루션인지 여부이므로 DB 트랜잭션 오류가 없다고 가정 해 봅시다.
Andre Borges

당신은 기본적으로 당신이 듣고 싶은 것을 말하도록 요구하고 있습니다. 난 그냥 당신에게 범위를 제공합니다. 적절한 상황이 없으면 SRP가 쓸모 없기 때문에 SRP가 중요한지 여부를 결정할 수있는 더 넓은 범위. IMO는 기술 솔루션에만 초점을 맞추기 때문에 문제에 접근하는 방식이 잘못되었습니다. 전체 상황과 충분히 관련이 있어야합니다. 그리고 예, DB는 실패 할 수 있습니다. 이 일이 당신이 알고, 때문에, 그 생략해서는 안 수있는 기회가있어 이런 일을 하고 이런 것들이 SRP 또는 다른 좋은 관행에 대한 의문에 대한 당신의 마음을 바꿀 수는.
Laiv

1
즉, 원칙은 돌로 작성된 규칙이 아니라는 점을 기억하십시오. 그들은 투과성입니다 (적응성). 보시다시피, 그들은 해석에 개방적입니다. 검토자가 해석을하고 다른 해석자가 있습니다. 당신이 보는 것을 보거나, 의심과 우려를 해결하거나, 당신을 해결하도록하십시오. 여기서 "올바른"답변을 찾을 수 없습니다. 정답은 먼저 프로젝트의 요구 사항 (기능적 및 비 기능적)을 요구하는 사용자와 검토 자에게 달려 있습니다.
11

4

SRP는 이론적 으로 Bob 삼촌이 그의 단일 책임 원칙 (The Single Responsibility Principle) 기사에서 설명했듯이 사람들 에 관한 입니다. 귀하의 의견에 제공 한 Robert Harvey에게 감사드립니다.

올바른 질문은 다음과 같습니다.

어떤 "이해 관계자"가 "이메일 보내기"요구 사항을 추가 했습니까?

해당 이해 관계자가 데이터 지속성을 담당 할 경우 (아마도 가능하지만) SRP를 위반하지 않습니다. 그렇지 않으면 그렇습니다.


2
흥미 롭습니다. 저는 SRP에 대한이 해석에 대해 들어 본 적이 없습니다. 이 해석에 대한 자세한 정보 / 문헌에 대한 조언이 있습니까?
썰매

2
@sleske : Bob 삼촌 자신으로부터 : " 이것은 단일 책임 원칙의 핵심에 도달합니다. 이 원칙은 사람에 관한 것입니다. 소프트웨어 모듈을 작성할 때 변경이 요청 될 때 이러한 변경이 시작될 수 있도록해야합니다. 한 사람 또는 오히려 하나의 좁게 정의 된 비즈니스 기능을 나타내는 단단하게 결합 된 한 그룹의 사람들로부터 "
Robert Harvey

고마워 Robert. IMO, "단일 책임 원칙"이라는 이름은 단순하게 들리기 때문에 끔찍하지만, "책임"의 의도 된 의미를 따르는 사람은 너무 적습니다. OOP가 그것의 많은 원래 개념들로부터 어떻게 변이 되었는가와 비슷하며, 이제는 의미가없는 용어입니다.
user949300

1
네. 이것이 REST라는 용어에서 일어난 일입니다. Roy Fielding조차도 사람들이 그것을 잘못 사용하고 있다고 말합니다.
Robert Harvey

인용이 관련되어 있지만 "이메일 보내기"요구 사항이 SRP 위반 질문과 관련된 직접적인 요구 사항이 아니라는 점이이 답변을 놓치고 있다고 생각합니다. 그러나 "이해 관계자"가 "이벤트 발생"요구 사항을 추가 한 경우이 답변은 실제 질문과 더 관련이 있습니다. 나는 이것을 더 명확하게하기 위해 내 자신의 대답을 약간 수정했다.
Doc Brown

2

기술적으로 이벤트를 알리는 리포지토리에는 아무런 문제가 없지만 편의상 일부 우려가 제기되는 기능적 관점에서 볼 것을 제안합니다.

새로운 사용자를 결정하고 그 지속성을 결정하는 사용자 생성은 3 가지 입니다.

내 전제

저장소가 SRP에 관계없이 비즈니스 이벤트를 알리는 적절한 장소인지 결정하기 전에 이전 전제를 고려하십시오. 참고 나는 말했다 비즈니스 이벤트 때문에 나에게가 UserCreated아닌 다른 의미를 내포이 UserStoredUserAdded 1 . 또한 각 행사가 다른 청중에게 전달되는 것으로 간주합니다.

한편으로 사용자를 만드는 것은 지속성을 포함하거나 포함하지 않는 비즈니스 별 규칙입니다. 더 많은 데이터베이스 / 네트워크 작업과 관련된 더 많은 비즈니스 작업이 필요할 수 있습니다. 지속성 계층이 알지 못하는 작업. 지속성 계층에는 사용 사례가 성공적으로 종료되었는지 여부를 결정하기에 충분한 컨텍스트가 없습니다.

반대로, _dataContext.SaveChanges();사용자를 성공적으로 유지 했다고해서 반드시 사실 인 것은 아닙니다 . 데이터베이스의 트랜잭션 범위에 따라 다릅니다. 예를 들어, 트랜잭션이 원자적인 MongoDB와 같은 데이터베이스의 경우에는 사실 일 수 있지만 기존 RDBMS는 더 많은 트랜잭션이 관련되어 있지만 아직 커밋되지 않은 ACID 트랜잭션을 구현할 수 없습니다.

이것이 유효한 포인트입니까?

그것은 수. 그러나 나는 그것이 SRP (기술적으로 말하기)의 문제 일뿐 만 아니라 편의성 (기능적으로 말하기)의 문제라고 감히 말할 것입니다.

  • 진행중인 비즈니스 운영을 인식하지 못하는 컴포넌트에서 비즈니스 이벤트를 발생시키는 것이 편리합니까?
  • 올바른 순간만큼 올바른 장소를 대표합니까?
  • 이러한 구성 요소가 이와 같은 알림을 통해 비즈니스 로직을 조정하도록 허용해야합니까?
  • 조기 이벤트로 인한 부작용을 무효화 할 수 있습니까? 2

여기서 이벤트를 올리는 것은 로깅과 본질적으로 같은 것 같습니다.

절대적으로하지. 그러나 이벤트 UserCreated로 인해 다른 비즈니스 작업이 발생할 수 있다고 제안 했으므로 로깅에는 부작용이 없습니다 . 알림처럼.

그것은 단지 그러한 논리가 다른 클래스에서 캡슐화되어야한다고 말하고 저장소가 다른 클래스를 호출하는 것이 좋습니다.

반드시 사실은 아닙니다. SRP는 클래스 별 관심사가 아닙니다. 계층, 라이브러리 및 시스템과 같은 다른 수준의 추상화에서 작동합니다! 그것은 같은 이해 당사자들의 손에 의해 같은 이유로 어떤 변화가 있는지를 유지하는 것에 관한 응집력에 관한 입니다. 사용자 생성 ( 유스 케이스 )이 변경되면 해당 순간 및 이벤트가 발생하는 이유도 변경 될 수 있습니다.


1 : 이름 지정도 적절합니다.

2 : UserCreated이후 _dataContext.SaveChanges();에 보냈지 만 연결 문제 또는 제약 조건 위반으로 인해 전체 데이터베이스 트랜잭션이 나중에 실패했다고 가정합니다. 부작용이 실행 취소하기 어려울 수 있으므로 (가능한 경우) 이벤트를 조기에 브로드 캐스트하는 데주의하십시오.

3 : 제대로 처리되지 않은 알림 프로세스는 실행 취소 / 실행할 수없는 알림을 발생시킬 수 있습니다.>


1
+1 거래 범위에 대한 아주 좋은 지적. 롤백이 발생할 수 있기 때문에 사용자가 생성되었다고 주장하는 것이 시기상조 일 수 있습니다. 로그와 달리 앱의 다른 부분은 이벤트와 관련이 있습니다.
Andres F.

2
바로 그거죠. 사건은 확실성을 나타냅니다. 무슨 일이 있었지만 끝났습니다.
20:50에 Laiv

1
@Laiv : 그렇지 않은 경우를 제외하고. Microsoft는 모든 종류의 이벤트를 접두사로 사용 Before하거나 Preview확실성을 보장하지 않습니다.
Robert Harvey

1
@ jpmc26 : 대안이 없으면 제안이 도움이되지 않습니다.
Robert Harvey

1
@ jpmc26 : 당신의 대답은 "완전히 다른 툴과 성능 특성을 가진 완전히 다른 개발 생태계로의 변화"입니다. 반대로 말하지만, 이것이 대부분의 개발 노력으로는 불가능하다고 상상할 수 있습니다.
Robert Harvey

1

없음 이는 SRP를 위반하지 않습니다.

많은 사람들이 단일 책임 원칙은 기능이 "한 가지"만 수행 한 다음 "한 가지"를 구성하는 것에 대한 토론에서 따라 잡아야한다고 생각하는 것 같습니다.

그러나 이것이 원칙의 의미는 아닙니다. 비즈니스 수준의 관심사에 관한 것입니다. 수업은 비즈니스 수준에서 독립적으로 변경 될 수있는 여러 가지 우려 사항이나 요구 사항을 구현해서는 안됩니다. 클래스가 사용자를 저장하고 하드 코딩 된 환영 메시지를 이메일을 통해 전송한다고 가정 해 봅시다. 여러 개의 독립적 인 우려로 인해 해당 클래스의 요구 사항이 변경 될 수 있습니다. 디자이너는 메일의 html / stylesheet를 변경하도록 요구할 수 있습니다. 통신 전문가는 메일 문구를 변경하도록 요구할 수 있습니다. 그리고 UX 전문가는 온 보딩 흐름의 다른 지점에서 실제로 메일을 보내야한다고 결정할 수있었습니다. 따라서이 클래스는 독립적 인 소스에서 여러 요구 사항이 변경 될 수 있습니다. 이것은 SRP를 위반합니다.

그러나 이벤트 발생은 SRP를 위반하지 않습니다. 이벤트는 다른 관심사가 아닌 사용자 저장에만 의존하기 때문입니다. 저장소가 메일의 영향을받지 않거나 메일에 대해 알지 않고도 저장으로 인해 이메일이 트리거 될 수 있으므로 이벤트는 실제로 SRP를 유지하는 좋은 방법입니다.


1

단일 책임 원칙에 대해 걱정하지 마십시오. 특정 개념을 "책임"으로 주관적으로 선택할 수 있기 때문에 여기에서 올바른 결정을 내리는 데 도움이되지 않습니다. 클래스의 책임이 데이터베이스에 대한 데이터 지속성을 관리한다고 말하거나 사용자 작성과 관련된 모든 작업을 수행하는 책임을 말할 수 있습니다. 이들은 응용 프로그램 동작의 다른 수준 일 뿐이며 "단일 책임"이라는 유효한 개념 표현입니다. 따라서이 원칙은 문제를 해결하는 데 도움이되지 않습니다.

이 경우 가장 유용한 원칙 은 가장 놀랄만 한 원칙입니다 . 따라서 질문을 해보자 : 데이터베이스에 데이터를 유지하는 주요 역할을하는 저장소가 전자 메일을 보내는 것도 놀라운 일 입니까?

예, 정말 놀랍습니다. 이들은 완전히 별개의 두 외부 시스템이며, 이름 SaveChanges이 알림을 보내는 것을 의미하지는 않습니다. 코드를 읽는 사람이 더 이상 어떤 추가 동작이 호출되는지 쉽게 알 수 없기 때문에이를 이벤트에 위임하면 동작이 더욱 놀라워집니다. 간접적 인 가독성을 손상시킵니다. 때로는 이점이 가독성 비용의 가치가 있지만 최종 사용자가 관찰 할 수있는 추가 외부 시스템을 자동으로 호출 할 때는 그렇지 않습니다. (로깅은 그 효과가 디버깅 목적으로 기본적으로 기록을 유지이기 때문에 여기에서 제외 할 수 있습니다. 사용자가 그래서 항상 로그인에 전혀 해가없는 로그를 소비하지 않습니다 종료합니다.) 더 나쁜, 이것은 유연성을 감소 타이밍 전자 메일을 보내면 저장과 알림간에 다른 작업을 인터리브 할 수 없습니다.

사용자가 성공적으로 생성 될 때 일반적으로 코드에서 알림을 보내야하는 경우 다음과 같은 방법을 만들 수 있습니다.

public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
    repo.Add(user);
    repo.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

그러나 이것이 부가 가치인지 여부는 응용 프로그램의 사양에 따라 다릅니다.


실제로 SaveChanges방법 의 존재를 권장하지 않습니다 . 이 메소드는 아마도 데이터베이스 트랜잭션을 커미트하지만 다른 저장소가 동일한 트랜잭션에서 데이터베이스를 수정했을 수 있습니다 . 이 SaveChanges사용자 저장소 인스턴스에 특별히 연결되어 있기 때문에 모든 것을 커밋한다는 사실은 다시 놀랍습니다 .

데이터베이스 트랜잭션을 관리하기위한 가장 간단한 패턴은 외부 using블록입니다.

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    context.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

이를 통해 프로그래머는 모든 리포지토리에 대한 변경 사항 이 저장되는 시점을 명시 적으로 제어하고 , 커밋 전에 발생해야하는 일련의 이벤트를 코드에 명시 적으로 문서화하고, 롤백이 발생한다고 가정하고 (롤백이 발생한다고 가정 DataContext.Dispose) 숨겨진 코드를 피하도록합니다. 상태 저장 클래스 간의 연결

또한 요청시 직접 이메일을 보내지 않기를 원합니다. 대기열에 알림 의 필요성 을 기록하는 것이 더 강력합니다 . 이를 통해 더 나은 오류 처리가 가능합니다. 특히, 이메일 전송 중 오류가 발생하면 사용자 저장을 방해하지 않고 나중에 다시 시도 할 수 있으며 사용자가 작성되었지만 사이트에서 오류가 리턴되는 경우를 피할 수 있습니다.

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    _emailNotificationQueue.AddUserCreateNotification(user);
    _emailNotificationQueue.Commit();
    context.SaveChanges();
}

context.SaveChanges()호출이 실패한 경우 큐 소비자는 전자 우편을 보내기 전에 사용자가 존재하는지 확인할 수 있으므로 알림 큐를 먼저 커미트하는 것이 좋습니다 . (그렇지 않으면, heisenbugs를 피하기 위해 본격적인 2 단계 커밋 전략이 필요합니다.)


결론은 실용적입니다. 실제로 코드 작성의 결과 (위험 및 이익 측면에서)를 통해 특정 방식으로 생각하십시오. "단일 책임 원칙"이 그 일을하는 데 자주 도움이되지는 않지만 "최소한 놀람의 원칙"은 종종 다른 개발자의 머리에 들어가서 어떤 일이 일어날 지 생각하는 데 도움이됩니다.


4
데이터베이스에 데이터를 유지하는 주요 역할을 가진 저장소도 전자 메일을 보내는 것이 놀랍 습니다. 내 질문의 요점을 놓친 것 같습니다. 내 저장소가 이메일을 보내지 않습니다. 그것은 단지 이벤트를 일으키고,이 이벤트가 어떻게 처리되는지 – 외부 코드의 책임입니다.
Andre Borges

4
기본적으로 "이벤트를 사용하지 마십시오"라는 주장을하고 있습니다.
Robert Harvey

3
[shrug] 이벤트는 대부분의 UI 프레임 워크의 중심입니다. 이벤트를 제거하면 해당 프레임 워크가 전혀 작동하지 않습니다.
Robert Harvey

2
@ jpmc26 : ASP.NET Webforms라고합니다. 짜증나
Robert Harvey

2
My repository is not sending emails. It just raises an event원인-효과. 저장소가 알림 프로세스를 트리거하고 있습니다.
Laiv

0

현재 SaveChanges않는 가지 : 그것은 변경 사항을 저장 하고 그렇게한다는 것을 기록합니다. 이제 이메일 알림을 보내십시오.

이벤트에 이벤트를 추가하는 현명한 아이디어가 있었지만, 이는 이미 위반 된 사실을 알지 못하고 SRP (Single Responsibility Principle)를 위반 한 것에 대한 비판을 받았습니다.

순수한 SRP 솔루션을 얻으려면 먼저 이벤트를 트리거 한 다음 해당 이벤트에 대한 모든 후크를 호출하십시오. 이제 해당 세 가지가 있습니다 : 저장, 로깅 및 이메일 전송.

먼저 이벤트를 트리거하거나에 추가해야 SaveChanges합니다. 귀하의 솔루션은 둘 사이의 하이브리드입니다. 기존 위반을 해결하지는 않지만 세 가지 이상으로 증가하지 않도록 권장합니다. SRP를 준수하기 위해 기존 코드를 리팩터링하려면 꼭 필요한 것보다 많은 작업이 필요할 수 있습니다. 그들이 SRP를 얼마나 멀리하고 싶은지는 당신의 프로젝트에 달려 있습니다.


-1

이 코드는 이미 SRP를 위반했습니다. 동일한 클래스가 데이터 컨텍스트와 통신하고 로깅을 담당했습니다.

3 가지 책임으로 업그레이드하면됩니다.

일을 하나의 책임으로 되 돌리는 한 가지 방법은 _userRepository; 그것을 명령 방송사로 만드십시오.

여기에는 일련의 명령과 리스너 세트가 있습니다. 명령을 받아서 리스너에게 브로드 캐스트합니다. 아마도 그 청취자들은 명령을 받았을 수도 있고, 명령이 실패했다고 말할 수도 있습니다 (이는 이미 통보받은 청취자에게 방송됩니다).

이제 대부분의 명령에는 하나의 리스너 (데이터 컨텍스트) 만있을 수 있습니다. SaveChanges는 변경하기 전에 2-데이터 컨텍스트와 로거를 갖습니다.

그런 다음 변경 사항은 다른 리스너를 추가하여 변경 사항을 저장합니다. 이는 이벤트 서비스에서 새 사용자 작성 이벤트를 발생시키는 것입니다.

여기에는 몇 가지 이점이 있습니다. 이제 나머지 코드 관리없이 로깅 코드를 제거, 업그레이드 또는 복제 할 수 있습니다. 저장 변경 사항에 더 많은 트리거를 추가하여 필요한 사항을 더 많이 추가 할 수 있습니다.

이 모든 _userRepository것이 생성되고 연결될 때 결정됩니다 (또는 추가 기능이 즉시 추가 / 제거 될 수 있습니다. 응용 프로그램 실행 중에 로깅을 추가 / 향상시킬 수 있음).

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