'직원'수업은 어떻게 디자인해야합니까?


11

직원 관리를위한 프로그램을 만들려고합니다. 그러나 Employee수업 을 디자인하는 방법을 알 수는 없습니다 . 저의 목표는 Employee객체를 사용하여 데이터베이스에서 직원 데이터를 생성하고 조작 할 수 있도록하는 것입니다.

내가 생각한 기본 구현은 다음과 같습니다.

class Employee
{
    // Employee data (let's say, dozens of properties).

    Employee() {}
    Create() {}
    Update() {}
    Delete() {}
}

이 구현을 사용하여 몇 가지 문제가 발생했습니다.

  1. ID직원의이 나는 새로운 직원을 설명하기위한 객체를 사용 그렇다면, 데이터베이스에 의해 제공됩니다, 더 없을 것이다 ID기존 직원을 나타내는 객체가 있지만, 아직 저장 됩니다 있습니다 ID. 그래서 때로는 개체를 설명하고 때로는 설명하지 않는 속성이 있습니다 ( SRP 를 위반 한 것으로 나타낼 수 있습니까? 새로운 직원과 기존 직원을 나타내는 동일한 클래스를 사용하기 때문에 ...).
  2. Create그동안 방법은 데이터베이스에서 직원을 생성하도록되어 Update및이 Delete기존 직원 (다시 말하지만, 행동 해야하는 SRP ...).
  3. 'Create'메소드에는 어떤 매개 변수가 있어야합니까? 모든 직원 데이터 또는 Employee개체에 대한 수십 개의 매개 변수가 있습니까?
  4. 클래스가 변경 불가능해야합니까?
  5. 어떻게 Update작동합니까? 속성을 가져 와서 데이터베이스를 업데이트합니까? 아니면 "오래된"개체와 "새"개체라는 두 가지 개체를 사용하고 데이터베이스 간의 차이점으로 데이터베이스를 업데이트합니까? (나는 대답이 클래스의 가변성에 대한 대답과 관련이 있다고 생각합니다).
  6. 생성자의 책임은 무엇입니까? 필요한 매개 변수는 무엇입니까? id매개 변수를 사용하여 데이터베이스에서 직원 데이터를 가져오고 속성을 채집합니까?

보시다시피, 나는 머리에 약간의 혼란이 있고 매우 혼란스러워합니다. 그러한 수업이 어떻게 생겼는지 이해하도록 도와 주실 수 있습니까?

그런 자주 사용되는 수업이 일반적으로 어떻게 설계되는지 이해하기 위해 의견을 원하지 않습니다.


3
SRP를 현재 위반하는 것은 엔터티를 대표하고 CRUD 논리를 담당하는 클래스가 있다는 것입니다. 이를 분리하면 해당 CRUD 오퍼레이션과 엔티티 구조는 다른 클래스가되며 12 는 SRP를 중단하지 않습니다. 3.Employee 추상화, 질문 4.5. 를 제공 할 수있는 객체를 취해야합니다 . 일반적으로 대답 할 수없고, 필요에 따라 달라집니다. 구조와 CRUD 연산을 두 클래스로 분리하면 Employee데이터를 가져올 수 없습니다. 더 이상 db에서, 그래서 대답 6입니다.
Andy

@DavidPacker-감사합니다. 당신은 대답에 넣을 수 있습니까?
Sipo

5
귀하의 ctor가 데이터베이스에 연락 하지 않도록 반복 하십시오. 그렇게하면 코드가 데이터베이스에 단단히 연결되어 테스트하기가 어려워집니다 (수동 테스트조차 어렵습니다). 리포지토리 패턴을 살펴보십시오. 그것에 대해 잠깐 생각해 Update보거나 직원입니까, 아니면 직원 기록을 업데이트합니까? 당신은 Employee.Delete(), 또는 Boss.Fire(employee)합니까?
RubberDuck

1
이미 언급 한 것 외에도 직원을 만들려면 직원이 필요하다는 것이 합리적입니까? 활성 레코드에서 직원을 새로 만든 다음 해당 오브젝트에서 저장을 호출하는 것이 더 합리적 일 수 있습니다. 그럼에도 불구하고 이제는 비즈니스 논리 및 자체 데이터 지속성을 책임지는 클래스가 있습니다.
Mr Cochese

답변:


10

이것은 귀하의 질문에 따라 초기 의견을보다 잘 작성했습니다. OP가 해결 한 질문에 대한 답변은이 답변의 맨 아래에 있습니다. 또한 같은 장소에 있는 중요한 메모 를 확인하십시오 .


현재 설명하고있는 Sipo는 Active record 라는 디자인 패턴 입니다. 모든 것과 마찬가지로, 이것조차도 프로그래머들 사이에서 자리를 잡았지만 단순한 이유 인 확장 성으로 인해 저장소데이터 매퍼 패턴을 선호하여 버려졌습니다 .

즉, 활성 레코드는 다음과 같은 개체입니다.

  • 도메인의 개체를 나타냅니다 (비즈니스 규칙 포함, 사용자 이름 등을 변경할 수 있거나없는 경우 등 개체에 대한 특정 작업을 처리하는 방법을 알고 있음),
  • 엔터티를 검색, 업데이트, 저장 및 삭제하는 방법을 알고 있습니다.

현재 디자인과 관련된 몇 가지 문제를 해결하고 디자인의 주요 문제는 마지막 6 번째 지점에서 해결됩니다 (마지막으로 추측합니다). 생성자를 디자인하는 클래스가 있고 생성자가 무엇을해야하는지조차 모르는 경우 클래스가 잘못한 것일 수 있습니다. 그것은 당신의 경우에 일어났습니다.

그러나 엔터티 표현과 CRUD 논리를 두 개 이상의 클래스로 분할하여 디자인을 수정하는 것은 실제로 매우 간단합니다.

디자인이 지금 다음과 같이 보입니다.

  • Employee-직원 구조에 대한 정보 (속성)와 엔티티를 수정하는 방법 (변경 가능한 방식으로 결정하는 경우), Employee엔티티에 대한 CRUD 논리를 포함하고 , Employee오브젝트 목록을 리턴하고 , Employee원하는 경우 오브젝트를 승인합니다. 직원을 업데이트하고 다음 Employee과 같은 방법을 통해 단일 을 반환 할 수 있습니다getSingleById(id : string) : Employee

와우, 수업이 엄청나 네요.

이것이 제안 된 해결책입니다.

  • Employee -직원 구조에 대한 정보 (속성) 및 엔티티 수정 방법 (변경 가능한 방식으로 결정한 경우)
  • EmployeeRepository- Employee엔티티에 대한 CRUD 로직을 포함하고 , Employee오브젝트 목록을 Employee리턴 할 수 있으며, 직원을 업데이트 할 때 오브젝트를 승인하고 , 다음 Employee과 같은 메소드를 통해 단일 을 리턴 할 수 있습니다.getSingleById(id : string) : Employee

우려분리에 대해 들어 보셨습니까 ? 아냐, 이제 할거야 이것은 단일 책임 원칙의 덜 엄격한 버전으로, 클래스는 실제로 하나의 책임 만 갖거나 Bob 아저씨가 말한 것처럼 :

모듈에는 변경해야 할 단 하나의 이유가 있어야합니다.

초기 클래스를 명확하게 둥근 인터페이스를 가진 두 개의 클래스로 명확하게 나눌 수 있다면 초기 클래스가 너무 많은 일을하고 있었을 것입니다.

리포지토리 패턴의 장점은 데이터베이스 (파일, noSQL, SQL, 객체 지향 데이터베이스) 사이의 중간 계층을 제공하는 추상화 역할을 할뿐만 아니라 구체적 일 필요는 없습니다. 수업. 많은 OO 언어에서 인터페이스를 실제 interface(또는 C ++ 인 경우 순수 가상 메소드가있는 클래스) 로 정의한 후 여러 구현을 가질 수 있습니다.

이것은 저장소가 실제 구현인지 여부에 대한 결정을 완전히 해제합니다 interface. 키워드 로 구조에 실제로 의존함으로써 인터페이스에 의존하고 있습니다. 리포지토리는 데이터 계층 추상화, 즉 데이터를 도메인에 매핑하거나 그 반대로 매핑하는 것을 가리키는 멋진 용어입니다.

(적어도) 두 개의 클래스로 분리 할 때의 또 다른 큰 장점은 이제 클래스가 Employee다른 어려운 일을 처리 할 필요가 없기 때문에 자신의 데이터를 명확하게 관리하고 매우 잘 수행 할 수 있다는 것입니다.

질문 6 : 새로 생성 된 Employee클래스 에서 생성자는 어떻게해야 합니까? 이건 간단하다. 인수를 가져 와서 유효한지 확인하십시오 (예 : 연령이 음수가 아니거나 이름이 비어 있지 않아야 함). 데이터가 유효하지 않을 때 오류가 발생하고 유효성 검사가 통과되면 인수를 개인 변수에 할당하십시오 실체의. 데이터베이스를 사용하는 방법을 전혀 모르기 때문에 이제 데이터베이스와 통신 할 수 없습니다.


질문 4 : 대답은 정확히 필요한 것이 무엇인지에 따라 크게 달라지기 때문에 일반적으로가 아니라 전혀 대답 할 수 없습니다.


질문 5 : 이제 두에 비 대한 클래스를 분리 한 것을, 당신이 직접 여러 업데이트 방법을 가질 수 Employee클래스, 같은 changeUsername, markAsDeceased,의 데이터 조작되는 Employee클래스 에만 RAM의를 다음과 같은 방법을 소개 할 수 registerDirty로부터 저장소 클래스에 대한 작업 단위 (UOW) 패턴으로, 저장소를 통해이 오브젝트가 특성을 변경했음을 알리고 commit메소드 를 호출 한 후 업데이트해야합니다 .

분명히, 업데이트를 위해서는 객체에 ID가 있어야하고 이미 저장되어 있어야하며,이를 감지하고 기준이 충족되지 않을 때 오류를 발생시키는 것은 저장소의 책임입니다.


질문 3 : 작업 단위 패턴을 사용하기로 결정하면 create방법은 다음과 같습니다 registerNew. 당신이하지 않으면, 아마 그것을 save대신 호출합니다 . 리포지토리의 목표는 도메인과 데이터 계층 사이에 추상화를 제공하는 것입니다.이 때문에이 방법 ( registerNew또는 save)이 Employee객체를 허용 하고 리포지토리 인터페이스를 구현하는 클래스에 따라 달라지는 것이 좋습니다. 그들은 개체를 꺼내기로 결정합니다. 전체 객체를 전달하는 것이 더 좋으므로 많은 선택적 매개 변수가 필요하지 않습니다.


질문 2 : 두 방법 모두 이제 저장소 인터페이스의 일부가되며 단일 책임 원칙을 위반하지 않습니다. 리포지토리의 책임은 Employee개체 에 대한 CRUD 작업을 제공 하는 것입니다. 즉, 읽기 및 삭제 외에도 CRUD는 만들기 및 업데이트로 변환됩니다. 분명히, EmployeeUpdateRepository등등 을 사용하여 저장소를 더 분할 할 수 는 있지만 거의 필요하지 않으며 단일 구현에는 일반적으로 모든 CRUD 작업이 포함될 수 있습니다.


질문 1 : 당신 Employee은 이제 (다른 속성들 중에서) ID 를 가진 간단한 클래스로 끝났습니다 . ID가 채워 졌는지 비어 null있는지 (또는 ) 여부는 객체가 이미 저장되었는지 여부에 따라 다릅니다. 그럼에도 불구하고, ID는 여전히 엔티티가 소유 한 속성이며 엔티티의 책임 Employee은 해당 속성을 관리해야하므로 ID를 관리해야합니다.

엔티티에 ID가 있는지 없는지 여부는 지속성 논리를 시도 할 때까지 일반적으로 중요하지 않습니다. 질문 5에 대한 답변에서 언급했듯이 이미 저장된 엔터티를 저장하지 않거나 ID가없는 엔터티를 업데이트하려고한다는 것을 감지하는 것은 저장소의 책임입니다.


중요 사항

우려의 분리는 훌륭하지만 실제로 기능적 리포지토리 계층을 설계하는 것은 매우 지루한 작업이며 제 경험상 실제 레코드 방식보다 조금 더 어려워집니다. 그러나 훨씬 더 유연하고 확장 가능한 디자인으로 마무리 할 수 ​​있습니다.


흠, 내 대답과 동일하지만 '에지' 가 그늘에
Ewan

2
@ Ewan 나는 당신의 대답을 downvote하지 않았지만, 일부는 왜 그런지 알 수 있습니다. OP의 질문 중 일부에 직접 대답하지 않으며 일부 제안은 근거가없는 것으로 보입니다.
Andy

1
훌륭하고 포괄적 인 답변. 관심있는 분리로 머리에 못을 박습니다. 그리고 나는 완벽한 복잡한 디자인과 멋진 타협 사이에서 중요한 선택을하는 경고를 좋아합니다.
Christophe

사실, 귀하의 답변이 우수합니다
Ewan

새 직원 객체를 처음 만들면 ID에 가치가 없습니다. id 필드는 null 값을 남길 수 있지만 직원 개체가 잘못된 상태입니까?
Susantha7

2

먼저 개념적 직원의 속성을 포함하는 직원 구조체를 만듭니다.

그런 다음 일치하는 테이블 구조로 데이터베이스를 작성하십시오 (예 : mssql).

그런 다음 필요한 다양한 CRUD 조작을 사용하여 해당 데이터베이스의 EmployeeRepoMsSql에 대한 직원 저장소를 작성하십시오.

그런 다음 CRUD 조작을 노출시키는 IEmployeeRepo 인터페이스를 작성하십시오.

그런 다음, IEmployeeRepo의 구성 매개 변수를 사용하여 Employee struct를 클래스로 확장하십시오. 필요한 다양한 저장 / 삭제 방법을 추가하고 주입 된 EmployeeRepo를 사용하여 구현하십시오.

ID와 관련이 있으면 생성자의 코드를 통해 생성 할 수있는 GUID를 사용하는 것이 좋습니다.

기존 객체로 작업하려면 코드에서 Update Methods를 호출하기 전에 리포지토리를 통해 데이터베이스에서 객체를 검색 할 수 있습니다.

또는 CRUD 메소드를 객체에 추가하지 않고 단순히 객체를 저장소에 전달하여 업데이트 / 저장 / 삭제할 수있는 찌푸린 얼굴 (그러나보기에는 우수함) Anemic Domain Object 모델을 선택할 수 있습니다.

불변성은 패턴과 코딩 스타일에 따라 디자인 선택입니다. 모든 기능을 수행하려면 불변도 시도하십시오. 그러나 변경 가능한 객체가 확실하지 않은 경우 구현하기가 더 쉬울 것입니다.

Create () 대신 Save ()를 사용합니다. 불변성 개념으로 작품을 만들지 만, 항상 '저장되지 않은'객체를 구성 할 수있는 것이 항상 유용합니다. 예를 들어 직원 객체 또는 객체를 채운 다음 이전에 규칙을 다시 확인할 수있는 UI가 있습니다. 데이터베이스에 저장.

***** 예제 코드

public class Employee
{
    public string Id { get; set; }

    public string Name { get; set; }

    private IEmployeeRepo repo;

    //with the OOP approach you want the save method to be on the Employee Object
    //so you inject the IEmployeeRepo in the Employee constructor
    public Employee(IEmployeeRepo repo)
    {
        this.repo = repo;
        this.Id = Guid.NewGuid().ToString();
    }

    public bool Save()
    {
        return repo.Save(this);
    }
}

public interface IEmployeeRepo
{
    bool Save(Employee employee);

    Employee Get(string employeeId);
}

public class EmployeeRepoSql : IEmployeeRepo
{
    public Employee Get(string employeeId)
    {
        var sql = "Select * from Employee where Id=@Id";
        //more db code goes here
        Employee employee = new Employee(this);
        //populate object from datareader
        employee.Id = datareader["Id"].ToString();

    }

    public bool Save(Employee employee)
    {
        var sql = "Insert into Employee (....";
        //db logic
    }
}

public class MyADMProgram
{
    public void Main(string id)
    {
        //with ADM don't inject the repo into employee, just use it in your program
        IEmployeeRepo repo = new EmployeeRepoSql();
        var emp = repo.Get(id);

        //do business logic
        emp.Name = TextBoxNewName.Text;

        //save to DB
        repo.Save(emp);

    }
}

1
빈혈 도메인 모델은 CRUD 논리와 거의 관련이 없습니다. 도메인 계층에 속하지만 기능이 없으며 모든 기능이이 도메인 모델이 매개 변수로 전달되는 서비스를 통해 제공되는 모델입니다.
Andy

정확히이 경우 리포지토리는 서비스이고 기능은 CRUD 작업입니다.
Ewan

@DavidPacker Anemic Domain Model이 좋은 것입니까?
candied_orange

1
@CandiedOrange 나는 의견에 내 의견을 표명하지 않았지만, 한 논리가 비즈니스 논리에만 책임이있는 계층으로 응용 프로그램을 다이빙하기로 결정한 경우, 나는 파울러와 함께 빈혈 도메인 모델입니다. 실제로 반 패턴입니다. 메소드를 클래스에 직접 추가 할 수있을 때 메소드 가있는 UserUpdate서비스가 필요한 이유는 무엇입니까? 이를 위해 서비스를 만드는 것은 의미가 없습니다. changeUsername(User user, string newUsername)changeUsernameUser
Andy

1
이 경우 모델에 CRUD 논리를 넣는 것만으로 repo를 주입하는 것이 최적이 아니라고 생각합니다.
Ewan

1

디자인 검토

귀하는 Employee실제로 데이터베이스에 지속적으로 관리되는 개체에 대한 프록시의 일종이다.

따라서 ID는 데이터베이스 객체에 대한 참조 인 것처럼 생각하는 것이 좋습니다. 이 논리를 염두에두고 데이터베이스 이외의 오브젝트에 대해 설계를 계속할 수 있습니다.이 ID를 사용하면 기존 컴포지션 논리를 구현할 수 있습니다.

  • ID가 설정되면 해당 데이터베이스 오브젝트가있는 것입니다.
  • ID가 설정되지 않은 경우 해당 데이터베이스 오브젝트가 없습니다. Employee아직 작성되지 않았거나 삭제되었을 수 있습니다.
  • 아직 메모리에로드되지 않은 직원과 데이터베이스 레코드를 확장하기위한 관계를 시작하려면 몇 가지 메커니즘이 필요합니다.

또한 객체의 상태를 관리해야합니다. 예를 들면 다음과 같습니다.

  • 직원이 생성 또는 데이터 검색을 통해 DB 개체와 아직 연결되어 있지 않은 경우 업데이트 또는 삭제를 수행 할 수 없습니다
  • 개체의 직원 데이터가 데이터베이스와 동기화되어 있거나 변경 되었습니까?

이를 염두에두고 다음을 선택할 수 있습니다.

class Employee
{
    ...
    Employee () {}       // Initialize an empty Employee
    Load(IDType ID) {}   // Load employee with known ID from the database
    bool Create() {}     // Create an new employee an set its ID 
    bool Update() {}     // Update the employee (can ID be changed?)
    bool Delete() {}     // Delete the employee (and reset ID because there's no corresponding ID. 
    bool isClean () {}   // true if ID empty or if all properties match database
}

객체 상태를 안정적인 방식으로 관리하려면 속성을 비공개로 설정하여 캡슐화를 개선하고 상태를 업데이트하는 게터 및 세터를 통해서만 액세스를 제공해야합니다.

당신의 질문

  1. ID 속성이 SRP를 위반하지 않는다고 생각합니다. 단일 책임은 데이터베이스 오브젝트를 참조하는 것입니다.

  2. 직원은 전체적으로 SRP를 준수하지 않습니다. 데이터베이스와의 연결뿐만 아니라 임시 변경 사항 및 해당 개체에 발생하는 모든 트랜잭션을 담당하기 때문입니다.

    또 다른 디자인은 변경 가능한 필드를 필드에 액세스해야하는 경우에만로드되는 다른 객체에 유지하는 것입니다.

    명령 패턴을 사용하여 Employee에서 데이터베이스 트랜잭션을 구현할 수 있습니다. 이러한 종류의 디자인은 또한 데이터베이스 특정 관용구와 API를 분리함으로써 비즈니스 오브젝트 (직원)와 기본 데이터베이스 시스템 간의 분리를 용이하게합니다.

  3. Create()비즈니스 객체가 진화하고이 모든 것을 유지하기가 매우 어려울 수 있기 때문에 수십 개의 매개 변수를 추가하지 않을 것 입니다. 그리고 코드를 읽을 수 없게됩니다. 데이터베이스에서 직원을 작성하는 데 절대적으로 필요한 최소의 매개 변수 세트 (4 개 이하)를 전달하고 업데이트를 통해 나머지 변경을 수행하거나 오브젝트를 전달하는 두 가지 선택 사항이 있습니다. 그건 그렇고, 당신의 디자인에서 나는 당신이 이미 선택한 것을 이해합니다 : my_employee.Create().

  4. 클래스가 불변이어야합니까? 위의 토론을 참조하십시오 : 원래 디자인 번호. 불변의 ID를 선택하지만 불변의 직원은 선택하지 않습니다. 직원은 실생활 (새로운 직책, 새로운 주소, 새로운 결혼 상황, 심지어 새로운 이름 등)에서 진화합니다. 적어도 비즈니스 로직 계층에서는이 현실을 염두에두고 작업하는 것이 더 쉽고 자연 스럽다고 생각합니다.

  5. 원하는 명령을 유지하기 위해 업데이트 명령과 (GUI?)에 대한 고유 한 개체를 사용하는 것을 고려할 경우 이전 / 새 방법을 선택할 수 있습니다. 다른 모든 경우에는 변경 가능한 객체를 업데이트하도록 선택했습니다. 주의 : 업데이트는 데이터베이스 코드를 트리거 할 수 있으므로 업데이트 후에도 개체가 실제로 DB와 동기화되어 있는지 확인해야합니다.

  6. 페치가 잘못 될 수 있기 때문에 생성자에서 DB에서 직원을 가져 오는 것은 좋은 생각이 아니며 많은 언어에서 실패한 구성에 대처하기가 어렵습니다. 생성자는 객체 (특히 ID)와 상태를 초기화해야합니다.

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