C#设计问题:两个彼此引用的不可变对象

时间:2018-03-15 16:29:00

标签: c# oop

我在C#/ WPF中编写了一个相当大的应用程序。我使用Builder模式创建始终处于一致状态的对象,并且对象是不可变的。

我在这个设计中遇到了一个问题,我不知道如何修复。

考虑以下两个类:

public class Employee {
    public string Name { get; }
    public double Salary { get; }
    public IReadOnlyList<EmployeeBonus> Bonuses { get; } // read-only list of bonuses

    public Employee(string name, double salary, IEnumerable<EmployeeBonus> bonuses) {
        Name = name;
        Salary = salary;
        Bonuses = new List<EmployeeBonus>(bonuses); // list of bonuses initialized in constructor
    }
}

public class EmployeeBonus {
    public Employee Employee { get; } // bonus has reference to Employee
    public string Description { get; }
    public double Amount { get; }

    public EmployeeBonus(Employee employee, string description, double amount) {
        Employee = employee; // employee must be initialized in constructor
        Description = description;
        Amount = amount;
    }
}

所以 - 我有一个员工类,其中包括每个员工收到的奖金列表。 EmployeeBonus类包含一个返回Employee的引用。 由于这两个类都是不可变的,因此每个类必须通过引用另一个来初始化。但这当然是不可能的,因为我无法创建一个对象,而该对象引用了另一个不存在的对象。

我想到的解决方案:

一个。我想在EmployeeBonus中没有对Employee的引用,因此EmployeeBonus只需要构造Description和Amount。但这打破了我的存储库模式:我的Repository<EmployeeBonus>有一个Add方法,它只接受一个EmployeeBonus。为了正确保存这个对象,我需要知道哪个员工拥有它,并且因为该方法只接受EmployeeBonus - 该对象必须包含Employee。

B中。我考虑过向Employee添加一个AddBonus(字符串描述,双倍金额)方法,这样奖金列表不会在构造函数中初始化,而是稍后会添加每个奖金,而Employee类会将自己附加到每个奖励 - 但这样做使员工不再是不可变的。

℃。我可以打破我的通用存储库并创建另一个方法Add(EmployeBonus bonus, Employee employee),然后从EmployeeBonus中删除Employee - 但我的EmployeeBonusRepository不会继承自Repository<EmployeeBonus>

d。我能想到的最正确的解决方案(正确但非常浪费)是:

public class Employee 
{
    public string Name { get; }
    public double Salary { get; }
    public IReadOnlyList<EmployeeBonus> Bonuses { get; }

    public Employee(string name, double salary, IEnumerable<EmployeeBonus> bonuses) {
        Name = name;
        Salary = salary;
        Bonuses = new List<EmployeeBonus>(bonuses);
    }
}

public class EmployeeBonus 
{
    public string Description { get; }
    public double Amount { get; }

    public EmployeeBonus(string description, double amount) {
        Description = description;
        Amount = amount;
    }
}

public class EmployeeBonusWithEmployee 
{
    public Employee Employee { get; }
    public EmployeeBonus Bonus { get; }

    public EmployeeBonusWithEmployee(Employee employee, EmployeeBonus bonus) 
    {
        Employee = employee;
        Bonus = bonus;
    }
}

public class EmployeeBonusWithEmployeeRepository : Repository<EmployeeBonusWithEmployee>
{
    public void Add(EmployeeBonusWithEmployee bonus)
    {
        // save complete employee bonus
    }
}

public class EmployeeRepository : Repository<Employee>
{
    //...
    public void Add(Employee employee)
    {
        // saves employee first, 
        // then creates an EmployeeBonusWithEmployee object for each EmployeeBonus in the list
        // and saves it using an EmployeeBonusWithEmployeeRepository
    }
}

这个解决方案(D)有意义吗?有更优雅的方式吗?

2 个答案:

答案 0 :(得分:1)

要在C#中创建相互引用的对象,需要延迟相互引用的代码执行 - 直到两个对象都已创建。

 public class B
 {
     private Lazy<A> _a;

     public A GetA 
     { 
         get { return _a.Value; } 
     }

     public B(Lazy<A> forLater)
     {
         _a = forLater;
     }
 }

和A班一样。 然后创建相互引用的对象:

 A a = null;
 B b = null;

 a = new A(new Lazy<B>(() => b));
 b = new B(new Lazy<A>(() => a));

Lazy<T>延迟执行代码直到稍后,允许两个构造函数完成。

这是一个可怕的解决方案 - 其他语言使这更容易 - 所以我建议一个更实际的方法。

  • 让它们变得可变,或
  • 将员工从EmployeeBonus中取出

答案 1 :(得分:1)

解决方案一:

为每位员工提供一个独特的钥匙;让我们假设它是为了争论的指导。

创建一个不可变的全局查找:

static ImmutableDictionary<Guid, Employee> employees = ... ;

然后当你创建一个新员工时:

Guid key = Guid.NewGuid(); // NOT new Guid() !
EmployeeBonus[] bs = new [] { new EmployeeBonus(key, description, amount) };
Employee e = new Employee(key, name, salary, bs);
employees = employees.Add(key, e);

现在你已经准备好了。除了变量“员工”之外,一切都是不可改变的,从奖金转到员工只需要从奖金中获取钥匙并在字典中查找。

解决方案二:

创建一个不可变的定向标记图类型。开发一个不可变的定向标记图是一个练习。这很有趣!

static Graph g = Graph.Empty;

创建新员工时,请向图表添加节点。图是不可变的,因此这会生成一个新图。

Employee e = new Employee(name, salary);
Bonus b = new Bonus(amount);
g = g.AddNode(e);
g = g.AddNode(b);
g = g.AddEdge(e, b, "Bonus");
g = g.AddEdge(b, e, "Employee");

所以现在你有了一名员工,你想知道他们有什么奖金,你可以参考图表:

// What nodes of the graph are connected to e by an edge "Bonus"?
IEnumerable<Bonuses> bs = g.GetNeighbors(e, "Bonus").OfType<Bonus>();

如果你有奖金并且想要雇员,那就相同。

Employee e = g.GetNeighbors(b, "Employee").OfType<Employee>().Single();

解决方案三:

而不是图表,维护一个不可变的行动分类帐。分类帐是一个只能从最后扩展的事务列表,从不在中间编辑。开发这种类型只是一种练习。

static Ledger ledger = Ledger.Empty;

...

Employee e = new Employee(name, salary);
Bonus b = new Bonus(amount);
ledger = ledger.AddHire(e);
ledger = ledger.AddBonus(e, b);
...

现在,当您想知道哪些奖金与员工相关联,或者员工与奖金相关联时,您过滤分类帐以获得一系列匹配添加奖励事件

请注意,所有这些解决方案基本相同。每个对象都是不可变的,但总有一个可变变量包含全局真实的来源,并且该变量随着世界的变化而变化。

这些解决方案中的每一个都具有不同的性能特征,因此请考虑您可能执行的操作。另请注意,字典和图形解决方案会丢失有关发生的订单事件的信息;能够及时运行涉及事件序列的查询对您来说可能很重要。

另请注意,这些解决方案都具有不同的垃圾收集性能特征。字典和分类帐解决方案让员工永远活着。