一个安全地提供公共API(只读)和私有API(读写)的对象

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

相关问题:

当一个非只读类已经到位时,只读类设计

控制对字段的读/写访问

通常,在OOP范例中,对象将其数据存储在字段中。 该类自己的方法可以完全访问其字段。 当您需要返回值时,您只需返回数据的副本,以便外部代码不会破坏数据。

现在假设数据块很复杂,因此它们本身就封装在类对象中,并且这些对象不容易被复制。 现在,如果从某个属性返回此类对象,则外部代码对您的内部代码具有相同的访问权限。 例如,如果返回List ,则每个人都可以向其添加值。 这通常是不合需要的。

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

我想要一个模式:

  • 数据(例如, int值)具有“public / read-only API”和“private / modifiable API”。
  • 只有对象创建者才能访问“私有/可修改的API”。
  • 私有/公共API可以具有被动部分(例如方法,属性)和活动部分(例如事件)。
  • 除了在对象创建阶段,不应使用代理。 所有电话都应该是直接的。
  • 从“公共/只读API”(以及最好来自“私有/可修改的API”)访问内部数据应该尽可能直接。 在编写这样的对象时,我不希望堆积大量的包装器。

以下是示例界面:

 interface IPublicApi { int GetValue(); } interface IPrivateApi { void SetValue(int value); } interface IPrivateConsumer { void OnValueChanged(); //Callback } 

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

有几个子问题需要解决。

  1. 如何允许“私有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 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
  • 通常, ReadableInt需要引用私有API使用者来调用它(通知等)。 要将公共API与具体的私有API使用者分离,私有API使用者将被抽象为ReadableInt.IPrivateConsumer接口。 ReadableInt通过构造函数接收对ReadableInt.IPrivateConsumer对象的引用。
  • 私有API控制器对象( ReadableInt.PrivateApi )通过传递给ReadableInt构造函数的回调( Action )提供给创建者( WriteableInt )。 这非常难看。 谁能提出另一种方式?
  • 有一个小问题: WritableInt.OnValueChanged()方法是私有的,但实际上是公共的,因为它是一个接口方法。 这可以通过代理或代理来解决。 还有别的办法吗?

这个系统有效,但有些部分我并不自豪。 当所有部件链接在一起时,我特别不喜欢初始化阶段。 这可以以某种方式简化吗?

我是怎么做的

这个问题非常有趣。 我不是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,您不需要任何类型的包装器或只读参考机制。 这是肯定的。 只需使用私有/受保护的setter创建只读属性或属性即可。 故事结局。

结构

基本上,同样的事情。 在设计良好的代码中,您不需要任何结构包装器。 如果你在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); } }