An object that securely provides both public API (

2020-05-08 06:45发布

This is an architecture problem. Programmers encounter this encapsulation problem quite often, but I haven't yet seen a complete and clean solution.

Related questions:

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

Controlling read/write access to fields

Normally, in OOP paradigm, objects store their data in fields. The class' own methods have full access to its fields. When you need to return value, you just return a copy of the data, so that the outside code cannot break the data.

Now suppose that the data pieces are complex, so they're themselves encapsulated in class objects and that these objects cannot be easily copied. Now, if you return such object from some property, the outside code has the same access to it as your internal code. For example, if you return a List<int>, everyone can add values to it. This is usually undesirable.

This problem is usually worked around using read-only wrappers - you wrap your full-access internal objects in read-only wrappers before returning. The problem with this approach is that the wrapper may be a poor substitution for the wrapped value - the wrapper is a different class. (And if you derive the read-only wrapper from the modifiable class (or vise-versa), then anybody can up-cast/down-cast the "read-only" object to the modifiable object, breaking the protection.)

I want a pattern such that:

  • The data (say, an int value) has "public/read-only API" and "private/modifiable API".
  • Only the object creator has access to the "private/modifiable API".
  • The private/public APIs may have both passive parts (e.g. methods, properties) and active parts (e.g. events).
  • Delegates should not be used except at the object creation stage. All calls should be direct.
  • The access to the internal data from the "public/read-only API" (and, preferably, from the "private/modifiable API" too) should be as direct as possible. I don't want a big stack of wrappers to accumulate when composing such objects.

Here are the sample interfaces:

interface IPublicApi {
    int GetValue();
}

interface IPrivateApi {
    void SetValue(int value);
}

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

I have devised such scheme. I want you to critique my solution or give your own solution.

2条回答
女痞
2楼-- · 2020-05-08 07:19

How I do it

The question is quite interesting. I'm not in any way an expert in OOP (God! I wish I would!), but here is how I do it:

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;
        }
    }
}

It's not very secure, since you can cast IReadOnlyFoo to Foo. But my philosophy here is the following: when you cast, you take all the responsibility on yourself. So, if you shoot yourself in the foot, it's your fault.

How I would do if I were to avoid casting problem

First thing to consider here is that there are value types and reference types.

Value types

For the sake of this answer I would classify value types for pure data types (int, float, bool, etc.) and structures.

Pure data types

It is interesting that you explain your problem using int which is value type. Value types are get copied by assignment. So, you don't need any kind of wrapper or read only reference mechanics for int. This is for sure. Just make a read-only property or property with private/protected setter and that's it. End of story.

Structures

Basically, the same thing. In good designed code, you don't need any wrappers for structs. If you have some reference type values inside struct: I would say that this is a poor design.

Reference types

For reference types your proposed solution looks too complicated. I would do something like this:

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);
    }
}
查看更多
Explosion°爆炸
3楼-- · 2020-05-08 07:33

There are several sub-problems that have to be solved.

  1. How to allow the "private API" code to access the private data without allowing the outside code to call it?
  2. How to give the "private API" access to the object creator?
  3. How to establish the two-way communication between the object and the code using the private API (calling/getting called)?

My system consists of these classes:

ReadableInt is the public API

ReadableInt.PrivateApi is the raw private API proxy object

ReadableInt.IPrivateConsumer is the public-to-private callback interface

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 is some private API consumer, which may reside in another assembly.

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!");
    }
}

One can use the classes like this:

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

This is how the system works:

  • The private API (ReadableInt.PrivateApi) gains access to the main object (ReadableInt) private members by being an inner class. No up-casting/down-casting security breaches.
  • Notice that the ReadableInt.PrivateApi constructor is marked internal, so only ReadableInt can create the instances. I could not find a more elegant way to prevent anyone from creating a ReadableInt.PrivateApi from a ReadableInt object.
  • In general, ReadableInt needs a reference to the private API consumer to call it (notifications etc.). To decouple the public API from concrete private API consumers, the private API consumer is abstracted as the ReadableInt.IPrivateConsumer interface. ReadableInt receives the reference to a ReadableInt.IPrivateConsumer object through the constructor.
  • The private API controller object (ReadableInt.PrivateApi) is given to the creator (WriteableInt) via callback (Action<PrivateApi>) passed to the ReadableInt constructor. It's extremely ugly. Can anyone propose another way?
  • There is a small problem: WritableInt.OnValueChanged() method is private, but is effectively public as it's an interface method. This can be solved with a delegate or a proxy. Is there any other way?

This system works, but has some parts that I'm not proud of. I particularly dislike the initialization stage when all parts are linked together. Can this be simplified somehow?

查看更多
登录 后发表回答