应该如何设计“员工”类?


11

我正在尝试创建一个用于管理员工的程序。但是,我无法弄清楚如何设计Employee课程。我的目标是能够使用Employee对象在数据库上创建和操作员工数据。

我想到的基本实现就是这样一个简单的实现:

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

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

使用此实现,我遇到了几个问题。

  1. ID雇员是由数据库给出的,所以如果我使用描述的新雇员的对象,不会有ID存储然而,当代表现有员工的对象有一个ID。因此,我有一个属性,它有时描述对象,有时却不描述(这可能表明我们违反了SRP?因为我们使用同一类来表示新员工和现有员工...)。
  2. Create方法应该在数据库上创建一个雇员,而UpdateDelete应该作用于现有雇员(再次,SRP ...)。
  3. “创建”方法应具有哪些参数?所有员工数据或Employee对象的数十个参数?
  4. 班级应该是一成不变的吗?
  5. 会如何Update工作?是否需要属性并更新数据库?还是需要两个对象-一个“旧”对象和一个“新”对象,并使用它们之间的差异更新数据库?(我认为答案与该类的可变性有关)。
  6. 建设者的责任是什么?它需要什么参数?它会使用id参数从数据库中获取员工数据并填充属性吗?

因此,如您所见,我的头有些乱,我非常困惑。您能否帮助我了解此类课程的外观?

请注意,我只是为了了解通常如何设计这种经常使用的类而不想发表意见。


3
您当前违反SRP的行为是,您有一个既代表实体又负责CRUD逻辑的类。如果将其分开,则CRUD操作和实体结构将是不同的类,则1.2.不会破坏SRP。3.应该使用一个Employee对象来提供抽象问题,问题4.5.通常无法回答,取决于您的需要,并且如果将结构和CRUD操作分为两个类,那么很明显,Employee不能获取数据的构造函数从数据库了,这样的答案6
安迪

@DavidPacker-谢谢。你能回答这个吗?
西波

5
我再说一遍,不要让您的ctor接触数据库。这样做将代码紧紧地耦合到数据库,使事情很难进行测试(甚至手动测试也变得更加困难)。查看存储库模式。仔细考虑一下,您Update是员工还是更新员工记录?你Employee.Delete()还是一个Boss.Fire(employee)
RubberDuck

1
除了已经提到的内容之外,您还需要一名员工来创建员工吗?在活动记录中,创建一个Employee然后在该对象上调用Save可能更有意义。即使这样,您现在仍然拥有一个负责业务逻辑及其自身数据持久性的类。
Cochese先生16年

Answers:


10

这是我根据您的问题发表的初步评论的形式更合理的副本。OP所解决问题的答案可以在该答案的底部找到。另外,请检查位于同一位置的重要说明


您当前正在描述的Sipo是一种称为Active record的设计模式。与所有内容一样,即使是这一类也已在程序员中找到了位置,但出于一个简单的原因,即可伸缩性,它已被废弃,转而使用存储库数据映射器模式。

简而言之,活动记录是一个对象,它:

  • 代表您域中的对象(包括业务规则,知道如何处理该对象的某些操作,例如您是否可以更改用户名等等),
  • 知道如何检索,更新,保存和删除实体。

您要解决当前设计中的几个问题,而设计的主要问题要解决的是第六点(我想是最后但并非最不重要的一点)。当您要为其设计构造函数的类时,甚至不知道构造函数应该做什么,则该类可能在做错事情。发生在您的情况下。

但是,通过将实体表示形式和CRUD逻辑分为两个(或更多)类,实际上可以很容易地确定设计。

这是您的设计现在的样子:

  • Employee-包含有关雇员结构(其属性)和方法的信息,以及如何修改实体的方法(如果您决定采用可变方式),包含Employee实体的CRUD逻辑,可以返回Employee对象列表,Employee在需要时接受对象更新员工,可以Employee通过类似的方法返回单个getSingleById(id : string) : Employee

哇,全班人数很多。

这将是建议的解决方案:

  • Employee -包含有关员工结构(其属性)以及如何修改实体的方法的信息(如果您决定采用可变方式)
  • EmployeeRepository-包含Employee实体的CRUD逻辑,可以返回Employee对象列表,Employee要更新员工时接受对象,可以Employee通过类似方法返回单个getSingleById(id : string) : Employee

您听说过关注点分离吗?不,现在就可以。它是“单一责任原则”的较不严格的版本,它说一堂课实际上应该只承担一个责任,或者像鲍勃叔叔说的那样:

一个模块应该只有一个更改理由。

很显然,如果我能够将您的初始班级清晰地分为两个仍具有完善的接口的类,那么初始班级可能做得太多,而且确实如此。

存储库模式的优点是,它不仅充当了提供数据库之间中间层(可以是任何东西,文件,noSQL,SQL,面向对象的中间层)的抽象,而且甚至不需要是一个具体的类。在许多OO语言中,您可以将接口定义为实际接口interface(如果使用C ++,则可以将接口定义为纯虚拟方法的类),然后可以实现多种实现。

这完全解除了存储库是否是您的实际实现的决策,而实际上只依赖于带有interface关键字的结构,因此仅依赖于接口。而存储库正是这样,它是数据层抽象的一个奇特术语,即将数据映射到您的域,反之亦然。

将其分为(至少)两个类的另一个Employee好处是,该类现在可以清楚地管理自己的数据并做得很好,因为它不需要处理其他困难的事情。

问题6:那么,构造函数应该在新创建的Employee类中做什么?很简单。它应该接受参数,检查它们是否有效(例如,年龄不应该为负数或名称不为空),在数据无效时传递错误,并且是否通过验证将参数分配给私有变量实体的 现在,它无法与数据库通信,因为它根本不知道如何执行该操作。


问题4:根本无法回答,也不能一概而论,因为答案很大程度上取决于您的确切需求。


问题5:现在你已经分离臃肿类一分为二,你可以直接有多个更新方法Employee类,如changeUsernamemarkAsDeceased,这将操纵的数据Employee仅在RAM中,然后你可以介绍的方法,例如registerDirty从存储库类的工作单元模式,通过该模式,您可以让存储库知道该对象已更改属性,并且在调用该commit方法后需要对其进行更新。

显然,对于更新而言,一个对象需要具有一个id并因此已经被保存,并且它是存储库的责任,可以检测到该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的实体。


重要的提示

请注意,尽管关注点分离非常好,但实际上设计功能性存储库层是一项繁琐的工作,以我的经验,与主动记录方法相比,正确起来要困难得多。但是最终您将获得一个更加灵活和可扩展的设计,这可能是一件好事。


嗯同我的答案,而不是作为“埃奇” 上的阴影看跌期权
伊万

2
@Ewan我没有拒绝您的回答,但我知道为什么有些人会这样。它不能直接回答OP的某些问题,并且您的某些建议似乎没有根据。
安迪

1
漂亮而全面的答案。用关注的分离器击中头部。我喜欢警告,指出了在完美的复杂设计和不错的折衷之间做出重要选择的精确选择。
Christophe

没错,您的答案很棒
Ewan 2016年

首次创建新员工对象时,ID将没有任何值。id字段可以保留为空值,但会导致员工对象处于无效状态?
Susantha19年

2

首先创建一个包含概念性雇员属性的雇员结构。

然后创建一个具有匹配表结构的数据库,例如mssql

然后使用所需的各种CRUD操作为该数据库EmployeeRepoMsSql创建一个雇员存储库。

然后创建一个IEmployeeRepo接口,以显示CRUD操作

然后将Employee结构扩展为具有IEmployeeRepo构造参数的类。添加所需的各种Save / Delete等方法,并使用注入的EmployeeRepo实施它们。

当建议使用Id时,建议您使用GUID,该GUID可以通过构造函数中的代码生成。

要使用现有对象,您的代码可以在调用其Update方法之前通过存储库从数据库中检索它们。

另外,您可以选择皱着眉头的(但在我看来更好)Anemic Domain Object模型,在该模型中,您无需向对象添加CRUD方法,只需将对象传递给要更新/保存/删除的仓库即可

不变性是一种设计选择,将取决于您的模式和编码样式。如果您要使用所有功能,那么也请尝试使其不可变。但是如果您不确定可变对象可能更容易实现。

我将使用Save()代替Create()。Create具有不变性概念,但是我总是发现能够构造尚未“保存”的对象很有用,例如,您拥有一些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逻辑关系不大。它是一个模型,尽管它属于域层,但不具有任何功能,并且所有功能都通过服务提供,该域模型作为参数传递给该服务。
安迪

确实,在这种情况下,存储库是服务,功能是CRUD操作。
伊万

@DavidPacker您是说Anemic域模型是一件好事吗?
candied_orange

1
@CandiedOrange我没有在评论中表达我的意见,但是不,如果您决定将应用程序扩展到仅由一层负责业务逻辑的各层,我与Fowler先生一道,认为是贫血的领域模型实际上是一种反模式。当我可以将方法直接添加到类中时,为什么还需要UserUpdate带有changeUsername(User user, string newUsername)方法的服务。为此创建服务是胡说八道。changeUsernameUser
安迪

1
我认为在这种情况下注入回购只是为了将CRUD逻辑放在Model上不是最优的。
伊万

1

审查您的设计

Employee实际上,您是对数据库中持久管理的对象的一种代理。

因此,我建议考虑该ID,就像它是对数据库对象的引用一样。考虑到这种逻辑,您可以像对待非数据库对象一样继续进行设计,该ID使您可以实现传统的组合逻辑:

  • 如果设置了ID,则您具有相应的数据库对象。
  • 如果未设置ID,则没有相应的数据库对象:Employee可能尚未创建,或者可能刚刚被删除。
  • 您需要某种机制来启动关系以消除现有员工和消除尚未加载到内存中的数据库记录。

您还需要管理对象的状态。例如:

  • 当员工尚未通过创建或数据检索与数据库对象链接时,您将不能执行更新或删除操作
  • 对象中的Employee数据是否与数据库同步?是否进行了更改?

考虑到这一点,我们可以选择:

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
}

为了能够以可靠的方式管理对象状态,必须通过将属性设置为私有来确保更好的封装,并且只能通过设置程序更新状态的getter和setter进行访问。

你的问题

  1. 我认为ID属性不会违反SRP。它的唯一职责是引用数据库对象。

  2. 您的员工整体上不符合SRP,因为它负责与数据库的链接,还负责临时更改以及该对象发生的所有事务。

    另一种设计可能是将可变字段保留在仅当需要访问字段时才加载的另一个对象中。

    您可以使用命令模式在Employee上实现数据库事务。通过隔离特定于数据库的习惯用法和API,这种设计还可以简化业务对象(员工)与基础数据库系统之间的分离。

  3. 我不会在中添加许多参数Create(),因为业务对象可能会演变并使得所有这些都很难维护。并且代码将变得不可读。您在这里有2种选择:传递在数据库中创建员工绝对必需的一组简约参数(不超过4个),并通过更新执行其余更改,或者传递一个对象。顺便说一句,在您的设计中,我了解到您已经选择: my_employee.Create()

  4. 班级应该是一成不变的吗?请参阅上面的讨论:在您的原始设计中。我会选择一个不变的ID,而不是一个不变的Employee。员工在现实生活中发展(新的工作岗位,新的地址,新的婚姻状况,甚至新的名字...)。我认为,至少在业务逻辑层中,考虑到这一现实将更加容易和自然。

  5. 如果考虑使用更新命令和(GUI?)的不同对象来保存所需的更改,则可以选择旧方法/新方法。在所有其他情况下,我都会选择更新可变对象。注意:更新可能会触发数据库代码,因此您应确保更新后该对象仍与DB保持真正同步。

  6. 我认为在构造函数中从数据库获取雇员不是一个好主意,因为获取可能会出错,并且在许多语言中,很难应对失败的构造。构造函数应初始化对象(尤其是ID)及其状态。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.