참조로 전달 된 객체 수정이 나쁜 습관입니까?


12

과거에는 일반적으로 객체가 생성 / 업데이트되는 기본 방법 내에서 대부분의 객체 조작을 수행했지만 요즘 다른 접근법을 취하고 있으며 나쁜 습관인지 궁금합니다.

다음은 예입니다. User엔터티 를 허용하는 리포지토리가 있지만 엔터티를 삽입하기 전에 모든 필드가 원하는 것으로 설정되도록 메서드를 호출합니다. 이제는 메서드를 호출하고 Insert 메서드 내에서 필드 값을 설정하는 대신 개체를 삽입하기 전에 모양을 만드는 일련의 준비 메서드를 호출합니다.

오래된 방법 :

public void InsertUser(User user) {
    user.Username = GenerateUsername(user);
    user.Password = GeneratePassword(user);

    context.Users.Add(user);
}

새로운 방법 :

public void InsertUser(User user) {
    SetUsername(user);
    SetPassword(user);

    context.Users.Add(user);
}

private void SetUsername(User user) {
    var username = "random business logic";

    user.Username = username;
}

private void SetPassword(User user) {
    var password = "more business logic";

    user.Password = password;
}

기본적으로 다른 방법에서 속성 값을 설정하는 것은 나쁜 습관입니까?


6
그냥 말했듯이 ... 여기서 아무것도 참조하지 않았습니다. 값으로 참조를 전달하는 것은 전혀 같은 것이 아닙니다.
cHao

4
@cHao : 그것은 차이가없는 구별입니다. 코드의 동작은 관계없이 동일합니다.
Robert Harvey

2
@JDDavis : 기본적으로 두 예제의 유일한 차이점은 설정된 사용자 이름과 암호 설정 작업에 의미있는 이름을 부여했다는 것입니다.
Robert Harvey

1
모든 전화는 여기에서 참조하십시오. 값으로 전달하려면 C #에서 값 유형을 사용해야합니다.
Frank Hileman

2
@ FrankHileman : 모든 통화는 가치가 있습니다. 이것이 C #의 기본값입니다. 참조를 전달 하고 참조를 전달 하는 것은 다른 짐승이며 구별은 중요합니다. user참조로 전달 된 경우 코드는 호출자의 손에서 코드를 빼내고 간단히 말하기 만하면 user = null;됩니다.
cHao

답변:


10

여기서 문제 User는 실제로 두 가지 다른 것을 포함 할 수 있다는 것입니다.

  1. 데이터 저장소로 전달할 수있는 완전한 사용자 엔터티입니다.

  2. 사용자 엔티티 작성 프로세스를 시작하기 위해 호출자에게 필요한 데이터 요소 세트입니다. 시스템은 위의 # 1에서와 같이 진정으로 유효한 사용자가되기 전에 사용자 이름과 암호를 추가해야합니다.

이것은 유형 시스템에서 전혀 표현되지 않은 객체 모델에 대한 문서화되지 않은 뉘앙스로 구성됩니다. 개발자로 "알아야"합니다. 그것은 훌륭하지 않으며, 당신이 직면 한 것과 같은 이상한 코드 패턴으로 이어집니다.

User클래스와 클래스 등 두 개의 엔티티가 필요하다고 제안합니다 EnrollRequest. 후자는 사용자를 만들기 위해 알아야 할 모든 것을 포함 할 수 있습니다. User 클래스처럼 보이지만 사용자 이름과 암호는 없습니다. 그럼 당신은 이것을 할 수 있습니다 :

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

호출자는 등록 정보만으로 시작하고 삽입 된 후 완료된 사용자를 다시받습니다. 이렇게하면 클래스를 변경하지 않고 삽입 된 사용자와 그렇지 않은 사용자 사이에 유형 안전 구별이 있습니다.


2
이름이 같은 방법 InsertUser은 삽입의 부작용이있을 수 있습니다. 이 문맥에서 "icky"가 무엇을 의미하는지 잘 모르겠습니다. 추가되지 않은 "사용자"를 사용하는 것은 요점을 놓친 것 같습니다. 추가되지 않은 경우 사용자가 아니라 사용자 작성 요청입니다. 물론 요청을 자유롭게 처리 할 수 ​​있습니다.
John Wu

@candiedorange 부작용은 아니지만 메소드 호출의 원하는 효과입니다. 즉시 추가하지 않으려면 유스 케이스를 만족시키는 다른 메소드를 작성하십시오. 그러나 그것은 질문에 전달 된 유스 케이스가 아니므로 우려가 중요하지 않다고 말합니다.
Andy

@candiedorange 커플 링으로 클라이언트 코드를 쉽게 만들면 괜찮습니다. 미래에 어떤 개발자 또는 의견이 생각할 수 있는지는 관련이 없습니다. 그때가되면 코드를 변경하십시오. 향후 사용 사례를 처리 할 수있는 코드를 작성하는 것보다 더 낭비가 없습니다.
Andy

5
@CandiedOrange 나는 정확하게 정의 된 구현 유형 시스템을 비즈니스의 개념적이고 평범한 영어 엔터티와 밀접하게 연결하지 말 것을 제안합니다. "사용자"라는 비즈니스 개념은 기능적으로 중요한 변형을 나타 내기 위해 두 가지 클래스로 구현할 수 있습니다. 지속되지 않은 사용자는 고유 한 사용자 이름을 보장 할 수 없으며, 트랜잭션을 연결할 수있는 기본 키가 없으며 사인온조차 할 수 없으므로 중요한 변형으로 구성됩니다. 물론 평신도에게 EnrollRequest와 User는 모두 모호한 언어로 "사용자"입니다.
John Wu

5

부작용이 예상치 않는 한 괜찮습니다. 따라서 저장소에 사용자를 허용하는 메소드가 있고 사용자의 내부 상태를 변경하는 경우 일반적으로 아무런 문제가 없습니다. 그러나 IMHO와 같은 메소드 이름 InsertUser 은 이것을 명확하게 전달하지 않으므로 오류가 발생하기 쉽습니다. 당신의 저장소를 사용할 때, 나는 같은 전화를 기대합니다

 repo.InsertUser(user);

사용자 객체의 상태가 아닌 저장소 내부 상태를 변경합니다. 이 문제는 구현 모두 에 존재하며 InsertUser내부적으로는 전혀 관련이 없습니다.

이 문제를 해결하려면 다음 중 하나를 수행하십시오.

  • 삽입과 초기화를 분리합니다. 따라서 호출자 InsertUser는 완전히 초기화 된 User객체 를 제공해야합니다 .

  • 사용자 객체의 구성 프로세스에 초기화를 작성하거나 (다른 답변 중 일부에서 제안한대로)

  • 단순히 그것이하는 일을보다 명확하게 표현 하는 방법에 대한 더 나은 이름찾으십시오 .

그래서 같은 방법 이름을 선택 PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPassword또는 부작용이 더 명확하게하는 것을 선호 뭐든간에.

물론, 메소드 이름이 길면 메소드가 "너무 많이"(SRP 위반) 수행 중일 수 있지만 때로는이를 해결하기를 원치 않거나 쉽게 해결할 수없는 경우도 있습니다.


3

나는 당신이 선택한 두 가지 옵션을보고 있으며, 제안 된 새로운 방법보다 오래된 방법을 훨씬 선호한다고 말해야합니다. 기본적으로 동일한 작업을 수행하더라도 몇 가지 이유가 있습니다.

두 경우 모두 user.UserName및을 설정합니다 user.Password. 비밀번호 항목에 대한 예약이 있지만 해당 예약은 해당 주제와 밀접한 관련이 없습니다.

참조 객체 수정의 의미

  • 동시 프로그래밍이 더 어려워 지지만 모든 응용 프로그램이 다중 스레드는 아닙니다.
  • 이러한 수정은 놀랍습니다. 특히 방법에 대해 아무 것도 제안하지 않는 경우
  • 이러한 놀라움으로 인해 유지 관리가 더 어려워 질 수 있습니다.

기존 방식과 새로운 방식

이전 방법으로 테스트하기가 더 쉬워졌습니다.

  • GenerateUserName()독립적으로 테스트 할 수 있습니다. 해당 방법에 대한 테스트를 작성하고 이름이 올바르게 생성되었는지 확인할 수 있습니다
  • 이름에 사용자 개체의 정보가 필요한 경우 서명을 변경 GenerateUserName(User user)하고 해당 테스트 가능성을 유지할 수 있습니다

새로운 방법은 돌연변이를 숨 깁니다.

  • 당신은 User2 층 깊이가 될 때까지 객체가 바뀌고 있다는 것을 모른다
  • 이 경우 User객체 의 변경 사항 이 더 놀랍습니다.
  • SetUserName()사용자 이름을 설정하는 것 이상을 수행합니다. 새로운 개발자가 애플리케이션에서 일이 어떻게 작동하는지 발견하기 어렵게 만드는 광고는 사실이 아닙니다.

1

John Wu의 답변에 전적으로 동의합니다. 그의 제안은 좋은 것입니다. 그러나 그것은 당신의 직접적인 질문을 약간 놓칩니다.

기본적으로 다른 방법에서 속성 값을 설정하는 것은 나쁜 습관입니까?

본질적으로 아닙니다.

예기치 않은 동작이 발생하기 때문에 너무 멀리 갈 수는 없습니다. 예를 들어 PrintName(myPerson)사람 객체를 변경해서는 안됩니다.이 방법 은 기존 값 을 읽는 것에 만 관심이 있기 때문 입니다. 그러나 SetUsername(user)그것은 값을 설정한다는 것을 강력하게 암시하기 때문에 귀하의 경우와 다른 주장 입니다.


이것은 실제로 단위 / 통합 테스트에 자주 사용하는 접근 방식으로, 테스트하려는 특정 상황으로 값을 설정하기 위해 객체를 변경하는 방법을 만듭니다.

예를 들면 다음과 같습니다.

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

ArrrangeContractDeletedStatus메소드가 myContract객체 의 상태를 변경하기를 명시 적으로 기대 합니다.

주요 이점은이 방법을 통해 다른 초기 계약으로 계약 삭제를 테스트 할 수 있다는 것입니다. 예를 들어, 상태 기록이 긴 계약 또는 이전 상태 기록이없는 계약, 의도적으로 잘못된 상태 기록이있는 계약, 테스트 사용자가 삭제할 수없는 계약.

나는이 합병 한 경우 CreateEmptyContractArrrangeContractDeletedStatus하나의 방법으로, 삭제 된 상태에서 테스트하려는 모든 계약마다이 방법의 여러 변형을 작성해야합니다.

그리고 내가 할 수있는 동안 :

myContract = ArrrangeContractDeletedStatus(myContract);

이것은 중복 적이거나 (어쨌든 객체를 변경하고 있기 때문에) 이제 myContract객체를 깊게 복제하도록 강요하고 있습니다 . 모든 사례를 다루고 싶을 때 지나치게 어렵다 (여러 레벨의 탐색 속성을 원한다면 상상해보십시오. 모두 복제해야합니까? 최상위 실체 만? )

객체를 변경하는 것은 원칙적으로 객체를 변경하지 않기 위해 더 많은 작업을 수행하지 않고도 원하는 것을 얻는 가장 쉬운 방법입니다.


따라서 귀하의 질문에 대한 직접적인 대답 은 메소드가 전달 된 객체를 변경하기 쉽다는 것을 모호하게하지 않는 한 본질적으로 나쁜 습관 이 아니라는 것입니다. 현재 상황에서는 메소드 이름을 통해 명확하게 알 수 있습니다.


0

나는 새로운 문제뿐만 아니라 오래된 것을 발견합니다.

옛날 방식 :

1) InsertUser(User user)

IDE에서 팝업되는이 메소드 이름을 읽을 때 가장 먼저 떠오르는 것은

사용자는 어디에 삽입됩니까?

메소드 이름은이어야합니다 AddUserToContext. 적어도 이것은 방법이 결국 수행하는 것입니다.

2) InsertUser(User user)

이것은 가장 놀랍지 않은 원칙을 분명히 위반합니다. 예를 들어, 나는 지금까지 일을하고 새로 인스턴스를 만들어 User그에게 a name를 설정하고 password놀라게했습니다.

a) 이것은 단지 사용자를 무언가에 삽입하지 않습니다

b) 또한 사용자를 그대로 삽입하려는 의도를 왜곡시킨다. 이름과 비밀번호가 무시되었습니다.

c) 이것은 단일 책임 원칙에 위배된다는 것을 나타낸다.

새로운 방식:

1) InsertUser(User user)

여전히 SRP 위반 및 가장 놀랍지 않은 원칙

2) SetUsername(User user)

질문

사용자 이름을 설정 하시겠습니까? 무엇을?

더 나은 : SetRandomName또는 의도를 반영하는 것.

삼) SetPassword(User user)

질문

암호를 설정하세요? 무엇을?

더 나은 : SetRandomPassword또는 의도를 반영하는 것.


암시:

내가 읽는 것을 선호하는 것은 다음과 같습니다.

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

초기 질문과 관련하여 :

참조로 전달 된 객체 수정이 나쁜 습관입니까?

아니요. 맛의 문제입니다.

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