Provide access to a class without access to the constructor

W

Weeble

Suppose I have a class A that works with instances of class B. I would
like it so that only an instance of A can generate instances of B, but
I would still like users of A to be able to handle instances of B,
including passing them as arguments to methods on A. Is there any way
to achieve this? Ideas so far:

Make B public, but make its constructor internal. This is okay, but
not great. It stops code in other assemblies from constructing
instances of B, but I'd rather prevent *everything* outside of A from
constructing instances of B.

Have a public interface, IB, and make B a private class inside of A.
This is not good because other code can still create instances of IB,
which might not behave at all like B, and pass them as arguments to
methods on A.

Give B a private constructor, and put A inside B. This works, but it's
*weird*, and becomes horrible if I want to have multiple classes like
B, because it requires A to be inside all of them, so they end up like
russian dolls.

I feel sure there must be a simple way to do this, but I can't think
of it. Any help would be much appreciated.

Regards,
Weeble.
 
P

Peter Ritchie [C# MVP]

One way is to put A and B in one assembly and make the constructor for B
internal.

Another is with nested classes:
public class B
{
private B()
{
}

public class A
{
public A()
{
}
public B CreateB()
{
return new B();
}
}
}

But, then the class A would resolve through B:

B.A a = new A();
B b = a.CreateB();
 
I

Ignacio Machin ( .NET/ C# MVP )

Suppose I have a class A that works with instances of class B. I would
like it so that only an instance of A can generate instances of B, but
I would still like users of A to be able to handle instances of B,
including passing them as arguments to methods on A. Is there any way
to achieve this? Ideas so far:

Make B public, but make its constructor internal. This is okay, but
not great. It stops code in other assemblies from constructing
instances of B, but I'd rather prevent *everything* outside of A from
constructing instances of B.

Have a public interface, IB, and make B a private class inside of A.
This is not good because other code can still create instances of IB,
which might not behave at all like B, and pass them as arguments to
methods on A.

Give B a private constructor, and put A inside B. This works, but it's
*weird*, and becomes horrible if I want to have multiple classes like
B, because it requires A to be inside all of them, so they end up like
russian dolls.

Not really, if the class B is declared as private you do not have that
problem:
public class A {

public IB GetB() { return new B(); }
class B: IB {
public string getIt() { return "hello"; }
}

}
public interface IB { string getIt();}
 
W

Weeble

I'm assuming this is the solution you're referring to, since it's the
only one that uses an interface.
Not really, if the class B is declared as private you do not have that
problem:
public class A {

public IB GetB() { return new B(); }
class B: IB {
public string getIt() { return "hello"; }
}

}
public interface IB { string getIt();}

What if it's more like this?

public sealed class A {
public IB MakeB() { return new B(); }
public void ConsumeB(IB b) { ... }
class B: IB { ... }
}

And what if B makes some guarantees about its behaviour that
A.ConsumeB relies on? A could then be broken by a bad implementation
of IB. I would like to guarantee that every B passed to ConsumeB was
created by A.MakeB. (It doesn't matter which instance of A made the
instance of B.)
 
W

Weeble

[...]
What if it's more like this?
public sealed class A {
public IB MakeB() { return new B(); }
public void ConsumeB(IB b) { ... }
class B: IB { ... }
}

What's the difference? In this particular context, the fact that A is
sealed doesn't change anything, and everything else looks the same.

The difference is that A has ConsumeB, which takes an IB as a
parameter. It can't take a B as a parameter, because B is private to
A. So it only has strong guarantees about the methods that are
available, not what they do.

Well, in the rather more complicated example that inspired this, B
contains a transformation matrix which, at least when constructed by
A, is known to be a rotation. It's quite awkward (though certainly
possible) to verify if a given matrix is a pure rotation, but it's
quite easy to compose one out of other known rotations. I had hoped
that the type system could give A the guarantee that it needs that a
given matrix is a rotation, and thus avoid verifying the matrix every
time. In this instance, I can just give B a public constructor that
creates a rotation given an axis and an angle, and methods for
composing rotations, and it's not a big deal if other code constructs
Bs in this way. But it does mean that if, later on, A has some other
way to construct a matrix that it knows to be a rotation, it has no
way to easily get this into a B.
The interface should clearly define any behavior that A relies on. The
best interface would codify that behavior in the interface itself, thus
making it impossible for any implementor of the interface to fail to
implement the behavior A relies on.


You can't really guarantee that. But you can, as I mentioned in my
previous reply to your message, at least check the type of the
implementing instance to make sure it's B. Presumably that's really all
you care about anyway.

I certainly *could* do, but that feels very against-the-grain. It
seems like it should be possible to solve this at compile-time. I was
inspired by recent articles by Eric Lippert, such as:
http://blogs.msdn.com/ericlippert/a...-from-a-derived-class-part-two-why-can-i.aspx
as well as his previous series on immutable data types. It made me
think that I could and should be using the type system and access
controls to provide stronger guarantees about the behaviour of my
classes.
 
W

Weeble

That's not a difference. The code example you compared to has the
implication that A will use the IB instance passed back from the GetB
method. After all, that's why the return type for GetB is IB, rather than
B.

I don't quite follow you here. GetB returns an IB, but we do know it's
really a B. GetB is a public method, and B is private, so it could not
have a return type of B. In contrast, the IB passed into ConsumeB
really could be some non-B class.

[Rotation stuff]
In that case, you need to write an interface that represents itself only
in terms of rotation. It can return a matrix (cloned, so that you don't
allow external mutation of internal state) as a convenience, but the API
should be strictly in terms of rotating. Even better is for the interface
to be immutable.

I do like immutable classes. :)

That said, I have the impression that you're not all that enthusiastic
about using interfaces to address this. And that's fine.

In the end I didn't use interfaces for this one. Rotations have a
public constructor that guarantees to make a proper rotation, and a
private constructor that is used by the methods that compose or
transpose rotations.
You might need to adjust your thinking, so that you're willing to treat
assemblies as legitimate compilation divisions. But it seems like they
offer the separation you're looking for here.

Actually, I was originally going to use assemblies for these kind of
divisions, but apparently it causes some sort of problems to have too
many assemblies, so I've been discouraged from doing this. (I can see
that it's annoying to have to add lots of references to each project,
though.) And when I have one huge assembly with lots of classes, some
of which need access to others' internal methods, it gets hard to make
it clear which classes should be using the internal methods and which
shouldn't.

Thanks for all the advice! It's been an interesting puzzle, but I
think I've finally dodged the nasty parts and found a solution that's
going to be at least moderately idiot-proof.

Weeble.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top