Reputation: 6787
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:
int
value) has "public/read-only API" and "private/modifiable API".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.
Upvotes: 0
Views: 431
Reputation: 1938
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.
First thing to consider here is that there are value types and reference types.
For the sake of this answer I would classify value types for pure data types (int, float, bool, etc.) and structures.
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.
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.
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);
}
}
Upvotes: 0
Reputation: 6787
There are several sub-problems that have to be solved.
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:
ReadableInt.PrivateApi
) gains access to the main object (ReadableInt
) private members by being an inner class. No up-casting/down-casting security breaches.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.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.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?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?
Upvotes: 1