C# – Should Constructor Parameters Be Exposed or Hidden?

cclass-designconstructorsdesign-patterns

I've a habit I just mechanically do without even thinking too much about it.

Whenever a constructor is waiting for some parameters, I consider this a public information that should be available by the calling code later on, if desired.

For example:

public class FooRepository : IFooRepository
{
    public FooRepository(IDbConnection dbConnection)
    {
        DbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection));
    }

    public IDbConnection DbConnection { get; }
}

The calling code which instantiated a FooRepository object is passing an IDbConnection object and therefore has the right to access this information later on but can't modify it anymore (no set on the DbConnection property)

The dbConnection parameter could be passed explicitly or by dependency injection, it doesn't matter. The FooRepository shouldn't be aware of such details.

However, yesterday when doing peer programming with a coworker, he told me that any class I write should expose just the minimum useful information.
He said developers shouldn't be able to analyse and mess with the internal state of an object.

I don't quite agree with him. In order to not waste too much time, I don't want to think a few minutes for each parameter to determine if this would be a good idea to expose it or not.
In my opinion, there are some use cases we simply can't think of, when we first write a new class.

Whether or not the class will finally be included in a Nuget package, doesn't really matter. I just don't want to limit users of my class from accessing information they explicitly passed when instantiating the object or could be easily retrieved from the dependency injection framework.

Could someone explain to me what is considered a good practice here?

Should I really think whether each parameter makes sense to be exposed? Or is there a design pattern I can just instinctively apply without wasting too much time?

Any resource on the subject are welcome.

Best Answer

The calling code which instantiated a FooRepository object is passing an IDbConnection object and therefore has the right to access this information later on

This is not true when you're dealing with things like the factory pattern, where the instantiator of the object is not the handler of the object. Factory patterns quite often exist specifically because the object's construction is an implementation detail that should be abstracted away.

This applies to more cases than just the factory pattern. Essentially, it applies to any object that gets passed around at least once.

but can't modify it anymore (no set on the DbConnection property)

This isn't true for reference types. It's true that you can't change which object is being referenced, but you can still alter its content. For example:

public class Foo
{
    public string Name { get; set; }
}

public class Baz
{
    public Foo Foo { get; } // allegedly: "can't modify it anymore"

    public Baz(Foo foo)
    {
        this.Foo = foo;
    }
}

var myFoo = new Foo() { Name = "Hello" };
var myBaz = new Baz(myFoo);

As per your claim, myBaz.Foo can no longer be modified. Yet this code is perfectly legal:

myBaz.Foo.Name = "a completely different name";

And that's still a risk you take.

he told me that any class I write, it should expose just the minimum useful information.

I don't want to think few minutes for each parameter to determine if this would be a good idea to expose it or not.

These two don't quite follow. It doesn't require you to think about it, it requires you to default to private instead of public like you currently do. Unless there is a valid reason to expose it, don't.

This is an oversimplification as there are cases where you shouldn't start out on private (e.g. DTO properties), but if you're still struggling with evaluating this, it's already better to default to private instead of public.

In my opinion, there are some use cases we simply can't think of when we first write a new class.

In my opinion, this is indicative of not quite understanding the class' responsibility and how it fits in the existing codebase.

In fact, that's sort of what you state in the question: you don't want to think about it. But you really should. For your example, what would ever be the purpose of a repository exposing its database connection? I can't think of any answer here that does not immediately violate good practice rules, can you?
Exposing the database connection is not part of the repository's purpose, which is all about providing access to a persistent data store.

In part, this is a matter of experience which will come over time. Every time you have to change the access modifier on an existing property/method is a time to learn why the previous choice was not the right one. Do it enough and you will improve at judging public contracts on the first design.

In my opinion, there are some use cases we simply can't think of when we first write a new class.

Don't forget OCP: "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification".

If you are inherently accounting for needing to change the internals of classes as time passes, you're taking a stance orthogonal to OCP.

That's not to say internals can't be changed when e.g. bugs are found or breaking changes are implemented; but it does mean you should try to avoid it as best as you can. Changing existing (often central) logic is a most common source of bugs, especially the crippling ones.

Whether or not the class will finally be included in a Nuget package doesn't really matter.

It really does matter. If your library is only being used in the same solution file, you can change things very quickly to your needs and can confirm it's working with a simple build.

But Nuget compounds the issue. If you change the contracts of your classes exposed in yout Nuget package, that means that every Nuget consumer will have to deal with breaking changes.
From personal experience, the issue is further compounded by Nuget servers not keeping a record of who has consumed your Nuget package, which makes it hard to figure out who all your consumers are and warn them ahead of time that breaking changes are about to be released.

Had you defaulted to making things private, and then selectively expose them, there would be less of a problem here. Adding to the contract without changing the existing parts does not break existing code.
Removing things from the contract, which is what would happen if you default to public, would always be liable to breaking code that depends on the thing you're now removing from the contract.

Should I really think for each parameter if it makes sense to expose it?

Yes. But it's not as complicated as you're making it out to be. Understanding what a certain class needs to expose or not is something you need to think about once per class. What is this class' purpose? How do I want this class to be used by its consumers?

After that, all properties/methods that you develop can easily be matched to the class' purpose, which is not a new evaluation but simply applying the decision you already made.

Or is there a design pattern I can just instinctively apply without wasting too much time?

If you were using interfaces on all your classes and using interface-based dependency injection, it would really help you in understanding how to separate a class' contract (things in the interface) from its implementation (things not in the interface).

Take for example:

public interface ISodaVendingMachine
{
    Soda GetDrink();
}

public class RegularVendingMachine : ISodaVendingMachine
{
    private Drinks drinks;

    public RegularVendingMachine(Drinks drinks)
    {
        this.drinks = drinks;
    }

    public Soda GetDrink()
    {
        return this.drinks.TakeOne();
    }
}

public class ConjuringVendingMachine : ISodaVendingMachine
{
    private PhilosophersStone philosophersStone;

    public ConjuringVendingMachine(PhilosophersStone philosophersStone)
    {
        this.philosophersStone = philosophersStone;
    }

    public Soda GetDrink()
    {
        return philosophersStone.PerformIncantation("Drinkum givum");
    }
}

The internals of each vending machine is up to them. It doesn't matter how they have access to and dispense a drink to the consumer. To the consumer, that's an irrelevant implementation detail. The customer doesn't want to know how the sausage gets made.

What matters for the public contracts is that they dispense a drink to the consumer, and thus the ISodaVendingMachine interface is built specifically for that purpose.

Notice how the interface doesn't care about anothing other than what it was designed to ensure.

When you have that interface, you can already see that anything in your class that isn't part of that interface should most likely be private as it is an implementation detail, not a contract.