단일 책임 원칙의 적용 _dataContext.SaveChanges();

최근에는 사소한 건축 문제가 발생했습니다. 내 코드에는 다음과 같은 간단한 저장소가 있습니다 (코드는 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는 함수에서 로깅 또는 실행 이벤트를 사용하는 것을 금지하지 않으며 그러한 논리를 다른 클래스로 캡슐화해야한다고 저장소가 다른 클래스를 호출하는 것이 좋습니다.



답변

예, 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에 고수 할 수 있습니다. 그러나 여기에 한 번 추가해야 할 것은 이벤트 메커니즘 자체입니다.

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


답변

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

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

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


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

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


답변

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

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

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

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

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

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

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

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

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

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

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

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

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

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


답변

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

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

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

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

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

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

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


답변

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

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

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

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


답변

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

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

내 전제

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

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

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

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

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

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

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

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

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

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


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

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

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


답변

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

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

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

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