2008-10-19 3 views
37

간단한 오브젝트 지향 설계 문제점에 어떻게 접근 할 것인가에 대해 질문하고 싶습니다. 이 시나리오를 다루는 가장 좋은 방법에 대해 필자는 몇 가지 아이디어를 가지고 있지만 Stack Overflow 커뮤니티의 의견을 듣고 싶습니다. 관련 온라인 기사에 대한 링크도 환영합니다. 나는 C#을 사용하고 있지만 질문은 특정 언어가 아닙니다.오브젝트 지향 우수 사례 - 상속 v 합성 v 인터페이스

내가 PersonId, Name, DateOfBirthAddress 필드와 그 데이터베이스 Person 테이블이있는 비디오 가게 응용 프로그램을 쓰고 있어요 가정하자. 테이블 과 테이블 PersonId에 연결되는 Customer 테이블도 있습니다. 모든 고객에게 이메일을 보낼 수

class Person { 
    public int PersonId { get; set; } 
    public string Name { get; set; } 
    public DateTime DateOfBirth { get; set; } 
    public string Address { get; set; } 
} 

class Customer : Person { 
    public int CustomerId { get; set; } 
    public DateTime JoinedDate { get; set; } 
} 

class Staff : Person { 
    public int StaffId { get; set; } 
    public string JobTitle { get; set; } 
} 

이제 우리는 함수를 쓸 수있는 말 :

간단한 객체 지향 접근 방식은 CustomerPerson "는 것입니다"라고 때문에이 같은 비트가 클래스를 생성하는 것입니다 :

static void SendEmailToCustomers(IEnumerable<Person> everyone) { 
    foreach(Person p in everyone) 
     if(p is Customer) 
      SendEmail(p); 
} 

이 시스템은 고객과 직원의 한 사람이 될 때까지 정상적으로 작동합니다.

class StaffCustomer : Customer { ... 

class StaffCustomer : Staff { ... 
: 우리가 정말 우리 사이의 임의의 선택을 할, 한 번 Customer로하고 한 번 Staff으로 두 번에 같은 사람이 우리의 everyone 목록을 원하지 않는다고 가정하면

분명히이 두 가지 중 첫 번째 만 SendEmailToCustomers 기능을 손상시키지 않습니다.

그럼 어떻게 하시겠습니까?

  • Person 클래스를 만들기는 StaffDetailsCustomerDetails 클래스에 대한 선택적 참조가?
  • 및 선택 사항 인 StaffDetailsCustomerDetails을 포함하는 새 클래스를 만드시겠습니까?
  • 모든 것을 인터페이스로 설정하고 (예 : IPerson, IStaff, ICustomer) 적절한 인터페이스를 구현 한 세 가지 클래스를 만드시겠습니까?
  • 완전히 다른 접근 방식을 선택하십시오.

답변

47

마크 이것은 흥미로운 질문입니다. 이것에 관해서 많은 의견을 찾을 수 있습니다. 나는 '옳은'대답이 있다고 생각하지 않는다. 이것은 시스템을 구축 한 후에 엄격한 계층 적 객체 디자인이 실제로 문제를 일으킬 수있는 훌륭한 예입니다.

예를 들어 "고객"및 "직원"수업을 들었다고 가정 해 보겠습니다. 시스템을 배포하면 모든 것이 행복합니다.몇 주 후, 누군가는 "직원"과 "고객"이며 고객 이메일을받지 못한다고 지적합니다. 이 경우 코드를 많이 변경해야합니다 (재 설계, 재 계수 제외).

모든 순열과 사람과 역할의 조합을 구현하는 파생 클래스 집합을 사용하려고하면 지나치게 복잡하고 유지 관리가 어려울 것이라고 생각합니다. 위의 예는 매우 간단합니다. 대부분의 실제 응용 프로그램에서는 상황이 더욱 복잡해집니다.

예를 들어 여기서는 "완전히 다른 접근 방법을 선택하십시오."라고 말합니다. Person 클래스를 구현하고 "역할"컬렉션을 포함시킬 것입니다. 각 사용자는 "고객", "직원"및 "공급 업체"와 같은 하나 이상의 역할을 가질 수 있습니다.

이렇게하면 새로운 요구 사항이 발견 될 때 역할을 쉽게 추가 할 수 있습니다. 예를 들어, 단순히 "Role"클래스를 가지고 새로운 역할을 파생시킬 수 있습니다.

10

순수한 접근 방식은 다음과 같습니다. 모든 것을 인터페이스로 만듭니다. 구현 세부 사항으로 선택적으로 다양한 형태의 구성 또는 구현 - 상속을 사용할 수 있습니다. 구현 세부 사항이므로 공용 API에 관계가 없으므로 가장 간단한 방법으로 자유롭게 선택할 수 있습니다.

+0

예, 이제 한 가지 구현을 선택하고 나중에 다른 코드를 위반하지 않고 마음을 바꿀 수 있습니다. –

16

당신은 유형의 고객 또는 직원이 될 수 있습니다 소명 책임의 컬렉션을 갖게됩니다 Party and Accountability patterns

이런 식으로 사람을 사용하는 것이 좋습니다.

나중에 관계 유형을 추가하면 모델이 더 간단 해집니다.

1

우리는 지난해 대학에서이 문제를 연구하고 우리는 에펠을 배우므로 다중 상속을 사용했습니다. 어쨌든 Foredecker 역할의 대안은 충분히 유연한 것 같습니다.

3

나는 "is"체크 (자바에서는 "instanceof")를 피할 것이다. 한 가지 해결책은 Decorator Pattern을 사용하는 것입니다. Person을 장식하는 EmailablePerson을 만들 수 있습니다. EmailablePerson은 Person의 개인 인스턴스를 보유하기 위해 composition을 사용하고 모든 비 이메일 메소드를 Person 객체에 위임합니다.

1

스탭 멤버 인 고객에게 이메일을 보내는 것은 무슨 문제입니까? 고객 인 경우 전자 메일을 보낼 수 있습니다. 나는 그렇게 생각하는 것이 틀린가? 그리고 왜 "모두"를 전자 메일 목록으로 가져야합니까? 우리가 "sendEmailToEveryone"메소드가 아닌 "sendEmailToCustomer"메소드를 다루고 있기 때문에 고객 목록을 갖는 것이 더 낫지는 않습니까? "모든 사용자"목록을 사용하려는 경우에도 해당 목록에서 중복을 허용 할 수 없습니다.

많은 redisgn으로이 중 어느 것도 달성 할 수없는 경우, 나는 Foredecker의 첫 번째 답변을 살펴보고 각자에게 할당 된 역할을해야 할지도 모릅니다.

+0

주어진 예에서 Person은 고객과 직원이 될 수 없습니다. 그것이 그 질문에 관한 것입니다. – OregonGhost

+0

안녕하세요, 질문은 "한 사람이 고객과 직원 모두 일 경우 여러 개의 이메일을 보내고 싶지 않습니다"라는 질문에 대한 것입니다. 1) "Everyone"은 중복을 허용해서는 안됩니다. 2) 중복을 허용하는 경우 Person 클래스는 Foredecker – vj01

5

Foredecker의 대답을 올바르게 이해했는지 알려주세요. 여기에 내 코드가있다 (파이썬에서, 미안하지만, 나는 C#을 모른다). 유일한 차이점은 사람이 "고객"이라면 무언가를 알리지 않을 것이고, 그의 역할 중 하나가 그 것에 관심이 있다면 그렇게 할 것입니다. 충분히 유연합니까?

# --------- PERSON ---------------- 

class Person: 
    def __init__(self, personId, name, dateOfBirth, address): 
     self.personId = personId 
     self.name = name 
     self.dateOfBirth = dateOfBirth 
     self.address = address 
     self.roles = [] 

    def addRole(self, role): 
     self.roles.append(role) 

    def interestedIn(self, subject): 
     for role in self.roles: 
      if role.interestedIn(subject): 
       return True 
     return False 

    def sendEmail(self, email): 
     # send the email 
     print "Sent email to", self.name 

# --------- ROLE ---------------- 

NEW_DVDS = 1 
NEW_SCHEDULE = 2 

class Role: 
    def __init__(self): 
     self.interests = [] 

    def interestedIn(self, subject): 
     return subject in self.interests 

class CustomerRole(Role): 
    def __init__(self, customerId, joinedDate): 
     self.customerId = customerId 
     self.joinedDate = joinedDate 
     self.interests.append(NEW_DVDS) 

class StaffRole(Role): 
    def __init__(self, staffId, jobTitle): 
     self.staffId = staffId 
     self.jobTitle = jobTitle 
     self.interests.append(NEW_SCHEDULE) 

# --------- NOTIFY STUFF ---------------- 

def notifyNewDVDs(emailWithTitles): 
    for person in persons: 
     if person.interestedIn(NEW_DVDS): 
      person.sendEmail(emailWithTitles) 

+0

에 의해 지적 된 "역할"을 정의해야합니다. 이는 훌륭한 솔루션이며 매우 확장 성이 있습니다. –

1

클래스는 데이터 구조 일뿐입니다. 아무 것도 작동하지 않으며 getters와 setter 만 있습니다. 상속은 여기에 부적합합니다.

7

사람은 인간이지만, 고객은 사람이 수시로 채택 할 수있는 역할에 불과합니다. 남자와 여자는 사람을 물려받을 후보자가되지만 고객은 다른 개념입니다.

Liskov 대체 원칙에 따르면 기본 클래스에 대한 참조가있는 경우 파생 클래스를 사용할 수 있어야합니다. 고객이 상속받는 것은이 조항을 위반하게됩니다. 고객은 아마도 조직이 수행하는 역할 일 수도 있습니다.

+0

조직은 종종 일종의 사람, 즉 사법 기관의 자격을 얻습니다. – ProfK

1

완전히 다른 접근법을 취하십시오. StaffCustomer 클래스의 문제점은 직원이 직원으로 시작하여 나중에 고객이 될 수 있으므로 직원으로 지우고 StaffCustomer의 새 인스턴스를 만들어야한다는 것입니다 수업. 아마도 'isCustomer'의 직원 클래스에있는 간단한 부울은 직원 목록에 고객이 이미 포함되어 있음을 알 수 있으므로 모든 고객 목록 (모든 고객과 모든 직원을 적절한 테이블로 가져 오는 것으로 컴파일 된 것으로 추정)을 허용합니다.

1

는 여기에 몇 가지 더 팁입니다 : 의 범주에서 "도 할 생각하지 않습니다이"여기가 발생 코드의 나쁜 예 :

찾기 메서드가 반환하는 객체는

문제 : 수에 따라 occurrence의 발견은 finder 메소드가 발생 횟수를 나타내는 숫자를 반환합니다 - 또는! 오직 하나만 발견하면 실제 객체를 반환합니다.

Do not do this! 이것은 최악의 코딩 관행 중 하나이며 모호성을 유발하고 다른 개발자가 게임에 참여할 때 코드를 혼란스럽게 만듭니다.

해결 방법 : 이러한 2 가지 기능이 필요한 경우 : 인스턴스 계산 및 가져 오기는 개수를 반환하는 메소드와 인스턴스를 반환하는 메소드 중 하나를 만들지 만 두 가지 방법 모두를 수행하는 단일 메소드는 생성하지 않습니다.

문제점 : 파생 메소드가 하나의 단일 어커런스 (하나의 어커런스가 두 개 이상 발견되면 어레이나 오아시스 배열을 찾음)를 반환 할 때 파생되는 나쁜 방법입니다. 이 게으른 프로그래밍 스타일은 일반적으로 이전의 프로그래머가 많이 수행합니다.

해결 방법 : 내 손에 이것을 사용하면 하나의 어커런스 만 발견되면 길이 1 (하나) 배열을 반환하고 더 많은 어커런스가 발견되면 길이가 1보다 큰 배열을 반환합니다. 게다가, 발생을 전혀 발견하지 않으면, 어플리케이션에 응해, null 또는 길이 0의 배열을 돌려줍니다.

인터페이스에 프로그래밍 및 공변 반환 형식을

문제 사용 : 인터페이스에 프로그래밍과 공변 반환 형식을 사용하고 호출 코드에 캐스팅합니다.

해결 방법 : 인터페이스에 정의 된 동일한 수퍼 유형 대신 반환 값을 가리키는 변수를 정의하십시오. 이렇게하면 프로그래밍 방식을 인터페이스 접근 방식으로 유지하고 코드를 깨끗하게 유지할 수 있습니다.

1000 개 이상의 줄이있는 클래스는 숨어있는 위험이 있습니다. 100 개가 넘는 줄이있는 메서드는 너무 위험합니다.

문제점 : 일부 개발자는 한 클래스/메소드에 너무 많은 기능을 넣고 기능을 해독하기에는 너무 게으르다. 이는 OOP에서 매우 중요한 원칙의 반대 인 낮은 결합력과 높은 결합력을 초래합니다! 해결책 : 내부/중첩 된 클래스를 너무 많이 사용하지 마십시오. 이러한 클래스는 필요에 따라서만 사용되며,이를 사용하는 습관을 가질 필요가 없습니다. 그것들을 사용하면 상속을 제한하는 것과 같은 더 많은 문제가 발생할 수 있습니다. 중복 된 코드를 찾아보십시오! 동일한 또는 비슷한 코드가 이미 일부 상위 유형 구현에 있거나 다른 클래스에있을 수 있습니다. 상위 유형이 아닌 다른 클래스에 있다면 당신은 또한 응집 규칙을 위반했습니다. 정적 메소드에주의하십시오. 추가하려면 유틸리티 클래스가 필요합니다.
기타 수신자 : http://centraladvisor.com/it/oop-what-are-the-best-practices-in-oop

0

아마 상속을 사용하고 싶지 않을 것입니다. 대신 다음 코드를 사용해보십시오.

class Person { 
    public int PersonId { get; set; } 
    public string Name { get; set; } 
    public DateTime DateOfBirth { get; set; } 
    public string Address { get; set; } 
} 

class Customer{ 
    public Person PersonInfo; 
    public int CustomerId { get; set; } 
    public DateTime JoinedDate { get; set; } 
} 

class Staff { 
    public Person PersonInfo; 
    public int StaffId { get; set; } 
    public string JobTitle { get; set; } 
} 
+2

왜? 대답은 더 많은 설명이 포함 된 경우 더 의미가 있습니다. 또한 이것이 기존 답변과 어떻게 다른가요? – Leigh