4

假设我有存储库:

interface IRepo
{
    Foo Get(int id);
    void Add(Foo f);
}

现在,要求我只能拥有一个Foo具有给定属性的人,比如说Id...。显然,如果我IRepo使用一些 SQL 后端实现并映射Foo.Id到主键,我会免费获得类似的东西——我可能会以某种PKViolationException. 但这正在变得特定于实现,所以无论我在哪里使用这个IRepo实现并开始捕获这些异常,我都会失去松散耦合的好处。

现在,我将如何解决这个问题?我是否应该添加某种服务层,首先检查对象是否存在,然后抛出一些独立于存储库的异常?

class Service
{
    IRepo repo;

    public void AddFoo(Foo foo)
    {
        if(repo.Get(foo.Id) != null)
            repo.Add(foo);
        else
            throw new FooAlreadyExistsException(foo.Id);
    }
}

这个解决方案似乎很糟糕,因为它repo.Add(foo)仍然会引发一些异常,尤其是在检查之后(通过其他一些活动)添加了具有给定 ID 的对象时,所以我只是向我的基础架构添加了一个额外的调用,而几乎没有好处。

似乎我应该小心并IRepo在考虑到这种异常的情况下实现(可以捕获PKViolationException并将其转换FooAlreadyExistsException为示例 SQL 实现),但是如何确保每个IRepo实现都遵循这样的规范?

您一般如何解决这些问题?

4

3 回答 3

4

“看来我应该小心并在考虑到这种异常的情况下实现 IRepo(可以捕获 PKViolationException 并将其转换为 FooAlreadyExistsException 在示例 SQL 实现中)”

你在这个钱上是对的。抛出的异常成为接口契约的一部分,实现必须遵守这个契约。编译器不会为您强制执行此操作,因此您必须绝对清楚预期。

“但是如何确保每个 IRepo 实现都遵循这样的规范呢?”

作为接口编写者,您不能对实现它的类负责。如果其他类有缺陷并暴露了泄漏的抽象,那就是它们的缺陷。您所能做的就是绝对清楚您正在定义的合同。

存储库的目的是抽象出数据库的实现细节,包括它的异常。因此,您还应该抽象出实现特定的异常......

interface IRepo
{
    /// <exception cref="FooAlreadyExistsException">Thrown if the specified Foo object already exists in the database.</exception>
    void Add(Foo f);
}

class ConcreteRepo
{
    public void Add(Foo f)
    {
        try
        {
            // Database stuff...
        }
        catch (PKViolationException e)
        {
            throw new FooAlreadyExistsException(e);
        }
    }
}
于 2012-10-29T10:49:08.500 回答
3

首先,考虑以下问题:您更改存储库使用的底层后备存储的概率是多少(即从具有主键的数据库转到一些没有唯一 ID 概念的未知数据存储)?我猜这个概率非常接近于 0。YAGNI 在这里适用。不要开始实现代码,以防发生一些不太可能发生的未知更改。当实际发生更改时,再进行必要的更改。

然后,关于您的示例,您为什么不为要添加的实体生成一个新 ID(或让底层数据存储为您完成),而不是强制客户端代码之前执行它?ID 应该使用数据库序列、UUID 或 n 自动增量列自动生成。

于 2012-10-28T12:28:26.077 回答
2

我首先要说的是

repo.Add(foo) 仍然可以抛出一些异常,尤其是在检查后立即添加了具有给定 ID 的对象时(通过其他一些活动)

是过度工程的一部分。

如果您强烈认为您的 Add 方法必须进行大量验证,则将您的方法命名为 TryAdd ,例如TryParse。这样,您的 Add 方法的调用者就知道 Add 方法在添加值之前会进行验证。

于 2012-10-29T10:41:06.603 回答