【问题标题】:An object that securely provides both public API (read-only) and private API (read-write)安全地提供公共 API(只读)和私有 API(读写)的对象
【发布时间】:2014-08-04 16:19:09
【问题描述】:

这是一个架构问题。程序员经常遇到这种封装问题,但我还没有看到一个完整干净的解决方案。

相关问题:

readonly class design when a non-readonly class is already in place

Controlling read/write access to fields

通常,在 OOP 范式中,对象将其数据存储在字段中。类自己的方法可以完全访问其字段。当需要返回值时,只返回一份数据,这样外部代码就无法破坏数据。

现在假设数据片段很复杂,因此它们本身封装在类对象中,并且这些对象不能轻易复制。现在,如果您从某个属性返回此类对象,则外部代码与您的内部代码具有相同的访问权限。例如,如果您返回一个List<int>,每个人都可以向它添加值。这通常是不可取的。

通常使用只读包装器解决此问题 - 在返回之前将完全访问的内部对象包装在只读包装器中。这种方法的问题在于包装器可能无法很好地替代被包装的值——包装器是一个不同的类。 (如果您从可修改类派生只读包装器(或反之亦然),那么任何人都可以将“只读”对象向上转换/向下转换为可修改对象,从而破坏保护。)

我想要这样的模式:

  • 数据(例如,int 值)具有“公共/只读 API”和“私有/可修改 API”。
  • 只有对象创建者才能访问“私有/可修改 API”。
  • 私有/公共 API 可能同时具有被动部分(例如方法、属性)和主动部分(例如事件)。
  • 除非在对象创建阶段,否则不应使用委托。所有电话都应该是直接的。
  • 从“公共/只读 API”(最好也从“私有/可修改 API”)访问内部数据应尽可能直接。在编写此类对象时,我不希望堆积一大堆包装器。

以下是示例界面:

interface IPublicApi {
    int GetValue();
}

interface IPrivateApi {
    void SetValue(int value);
}

interface IPrivateConsumer {
    void OnValueChanged(); //Callback
}

我设计了这样的方案。我希望你批评我的解决方案或给出你自己的解决方案。

【问题讨论】:

    标签: c# oop architecture encapsulation


    【解决方案1】:

    有几个子问题需要解决。

    1. 如何让“private API”代码访问私有数据而不让外部代码调用?
    2. 如何向对象创建者授予“私有 API”访问权限?
    3. 如何使用私有API(调用/被调用)建立对象和代码之间的双向通信?

    我的系统由这些类组成:

    ReadableInt 是公共 API

    ReadableInt.PrivateApi 是原始私有 API 代理对象

    ReadableInt.IPrivateConsumer是公私回调接口

    public sealed class ReadableInt {
        int _value;
        IPrivateConsumer _privateConsumer;
    
        public ReadableInt(IPrivateConsumer privateConsumer, Action<PrivateApi> privateConsumerInitializer) {
            _privateConsumer = privateConsumer;
            var proxy = new PrivateApi(this);
            privateConsumerInitializer(proxy);
        }
        public int GetValue() {
            return _value;
        }        
        private void SetValue(int value) {
            _value = value;
            _privateConsumer.OnValueChanged();
        }
    
        public interface IPrivateConsumer {
            void OnValueChanged();
        }
    
        public class PrivateApi {
            ReadableInt _readableInt;
    
            internal PrivateApi(ReadableInt publicApi) {
                _readableInt = publicApi;
            }
    
            public void SetValue(int value) {
                _readableInt.SetValue(value);
            }
        }
    }
    

    WritableInt 是一些私有 API 使用者,它可能驻留在另一个程序集中。

    public sealed class WritableInt : ReadableInt.IPrivateConsumer {
        ReadableInt _readableInt;
        ReadableInt.PrivateApi _privateApi;
    
        public WritableInt() {
            _readableInt = new ReadableInt(this, Initialize);
        }
    
        void Initialize(ReadableInt.PrivateApi privateApi) {
            _privateApi = privateApi;
        }
    
        public ReadableInt ReadOnlyInt { get { return _readableInt; } }
    
        public void SetValue(int value) {
            _privateApi.SetValue(value);
        }
    
        void ReadableInt.IPrivateConsumer.OnValueChanged() {
            Console.WriteLine("Value changed!");
        }
    }
    

    可以像这样使用类:

    var writeableInt = new WritableInt();
    var readableInt = writeableInt.ReadOnlyInt;
    

    系统是这样工作的:

    • 私有 API (ReadableInt.PrivateApi) 通过成为内部类来访问主对象 (ReadableInt) 私有成员。没有向上转换/向下转换的安全漏洞。
    • 注意ReadableInt.PrivateApi 构造函数被标记为internal,所以只有ReadableInt 可以创建实例。我找不到更优雅的方法来阻止任何人从 ReadableInt 对象创建 ReadableInt.PrivateApi
    • 一般来说,ReadableInt 需要引用私有 API 使用者才能调用它(通知等)。为了将公共 API 与具体的私有 API 消费者分离,私有 API 消费者被抽象为ReadableInt.IPrivateConsumer 接口。 ReadableInt 通过构造函数接收对 ReadableInt.IPrivateConsumer 对象的引用。
    • 私有 API 控制器对象 (ReadableInt.PrivateApi) 通过传递给 ReadableInt 构造函数的回调 (Action&lt;PrivateApi&gt;) 提供给创建者 (WriteableInt)。 非常难看。谁能提出另一种方式?
    • 有一个小问题:WritableInt.OnValueChanged() 方法是私有的,但实际上是公共的,因为它是一个接口方法。这可以通过委托或代理解决。有没有其他办法?

    这个系统可以工作,但有一些我并不引以为豪的部分。我特别不喜欢所有部分都链接在一起的初始化阶段。这可以以某种方式简化吗?

    【讨论】:

      【解决方案2】:

      我是怎么做的

      这个问题很有趣。我绝不是 OOP 方面的专家(上帝!我希望我会这样做!),但我是这样做的:

      public interface IReadOnlyFoo
      {
          int SomeValue
          {
              get;
          }
      }
      
      public class Foo: IReadOnlyFoo
      {
          public int SomeValue
          {
              get;
              set;
          }
      }
      
      public class Bar
      {
          private Foo foo;
      
          public IReadOnlyFoo Foo
          {
              get
              {
                  return foo;
              }
          }
      }
      

      它不是很安全,因为您可以将 IReadOnlyFoo 转换为 Foo。但我的理念是:当你选角时,你自己承担所有责任。所以,如果你朝自己的脚开枪,那是你的错。

      如果我要避免投射问题,我会怎么做

      这里首先要考虑的是值类型和引用类型。

      值类型

      为了回答这个问题,我将对纯数据类型(int、float、bool 等)和结构的值类型进行分类。

      纯数据类型

      有趣的是,您使用值类型 int 来解释您的问题。值类型通过赋值复制。因此,您不需要任何类型的包装器或 int 的只读参考机制。这是肯定的。只需使用私有/受保护的设置器创建一个只读属性或属性,就是这样。故事结束。

      结构

      基本上,同样的事情。在设计良好的代码中,您不需要任何结构包装器。如果你在 struct 中有一些引用类型值:我会说这是一个糟糕的设计。

      引用类型

      对于引用类型,您提出的解决方案看起来太复杂了。我会这样做:

      public class ReadOnlyFoo
      {
          private readonly Foo foo;
      
          public ReadOnlyFoo(Foo foo)
          {
              this.foo = foo;
          }
      
          public SomeReferenceType SomeValue
          {
              get
              {
                  return foo.SomeValue;
              }
          }
      }
      
      public class Foo
      {
          public int SomeValue
          {
              get;
              set;
          }
      }
      
      public class Bar
      {
          private Foo foo;
      
          public readonly ReadOnlyFoo Foo;
      
          public Bar()
          {
              foo = blablabla;
              Foo = new ReadOnlyFoo(foo);
          }
      }
      

      【讨论】:

      • "它不是很安全,因为您可以将 IReadOnlyFoo 转换为 Foo。" - 这就是问题所在。我提到它:“如果您从可修改类(或反之亦然)派生只读包装器,那么任何人都可以将“只读”对象向上转换/向下转换为可修改对象,从而破坏保护。”
      • 你很快。我刚刚完成我的结论部分:D
      • 我认为这更多的是哲学。您想在保护方面走多远(我会说过度保护)?您的代码仍然可以通过反射破坏。你会说这是作弊。然后我会说,在这种特殊情况下选角也是作弊。
      • 好吧,如果没有其他解决方案没有这个缺陷......私人反思是不可阻挡的。铸造是。
      • 好的。我得到了你的位置。那么让我们切换到您的解决方案:)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-22
      • 2017-05-13
      • 2022-09-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多