31

Let's say, for example, you have an application with a widely shared class called User. This class exposes all information about the user, their Id, name, levels of access to each module, timezone etc.

The user data are obviously widely referenced throughout the system, but for whatever reason, the system is set up so that instead of passing this user object into classes that depend on it, we're just passing in individual properties from it.

A class that requires the user id, will simply require the GUID userId as a parameter, sometimes we might need the username as well, so that is passed in as a separate parameter. In some cases, this is passed to individual methods, so the values are not held at the class level at all.

Every single time I need access to a different piece of information from the User class, I have to make changes by adding parameters and where adding a new overload is not appropriate, I have to change every reference to the method or class constructor as well.

The user is just one example. This is widely practiced in our code.

Am I right in thinking this is a violation of the Open/Closed principle? Not just the act of changing existing classes, but setting them up in the first place so that widespread changes are very likely to be required in the future?

If we just passed in the User object, I could make a small change to the class I am working with. If I have to add a parameter, I might have to make dozens of changes to references to the class.

Are any other principles broken by this practice? Dependency inversion perhaps? Although we're not referencing an abstraction, there is only one kind of user, so there isn't any real need to have a User interface.

Are there other, non-SOLID principles being violated, such as basic defensive programming principles?

Should my constructor look like this:

MyConstructor(GUID userid, String username)

Or this:

MyConstructor(User theUser)

Post Edit:

It has been suggested that the question is answered in "Pass ID or Object?". This does not answer the question of how the decision to go either way affects an attempt to follow the SOLID principles, which is at the core of this question.

BartoszKP
  • 248
Jimbo
  • 428
  • 11
    @gnat: This is definitely not a duplicate. The possible duplicate is about method chaining to reach deep into an object hierarchy. This question doesn't appear to be asking about that at all. – Greg Burghardt Jun 26 '18 at 11:15
  • 2
    The second form is often used when the number of parameters being passed has become unwieldy. – Robert Harvey Jun 26 '18 at 13:19
  • 12
    The one thing I don't like about the first signature is that there's no guarantee that the userId and username are actually originating from the same user. It's a potential bug that's avoided by passing around User everywhere. But the decision really depends on what the called methods are doing with the arguments. – 17 of 26 Jun 26 '18 at 13:26
  • 9
    The word “parse” makes no sense in the context in which you’re using it. Did you mean “pass” instead? – Konrad Rudolph Jun 26 '18 at 14:21
  • Both are valid. The cost of creating a valid instance of User may not be worth it in some cases where you just need the id and username. The cost of creating a valid instance of User and passing it around may be worth it. These costs may be maintenance, performance, readability, etc. – bitsoflogic Jun 26 '18 at 15:32
  • 2
  • 6
    What about the I in SOLID? MyConstructor basically says now "I need a Guid and a string". So why not have an interface providing a Guid and a string, let User implement that interface and let MyConstructor depend on an instance implementing that interface? And if the needs of MyConstructor change, change the interface. -- It helped me greatly to think of interfaces to "belong" to the consumer rather than the provider. So think "as a consumer I need something that does this and that" instead of "as a provider I can do this and that". – Corak Jun 27 '18 at 07:37
  • It's hard to say with any certainty without a real concrete example of a class with MyConstructor but to me this looks like the problem might be higher level - i.e. you can avoid this problem altogether with better encapsulation. With good encapsulation and adherence to the "tell, don't ask" principle, you shouldn't really need to pass property values of a User to a constructor. In practice, if that isn't feasible, everything else is going to be a compromise. – Ant P Jun 27 '18 at 08:21
  • Pass ID or Object? does not in any way address how the decision to go either way can be judged to abide by or break software design principles, which is at the core of this question. – Jimbo Jun 27 '18 at 09:25

9 Answers9

31

There is absolutely nothing wrong with passing an entire User object as a parameter. In fact, it might help clarify your code, and make it more obvious to programmers what a method takes if the method signature requires a User.

Passing simple data types is nice, until they mean something other than what they are. Consider this example:

public class Foo
{
    public void Bar(int userId)
    {
        // ...
    }
}

And an example usage:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user.Id);

Can you spot the defect? The compiler can't. The "user Id" being passed is just an integer. We name the variable user but initialize its value from the blogPostRepository object, that presumably returns BlogPost objects, not User objects - yet the code compiles and you end up with a wonky runtime error.

Now consider this altered example:

public class Foo
{
    public void Bar(User user)
    {
        // ...
    }
}

Maybe the Bar method only uses the "user Id" but the method signature requires a User object. Now let's go back to the same example usage as before, but amend it to pass the entire "user" in:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user);

Now we have a compiler error. The blogPostRepository.Find method returns a BlogPost object, which we cleverly call "user". Then we pass this "user" to the Bar method and promptly get a compiler error, because we can't pass a BlogPost to a method that accepts a User.

The Type System of the language is being leveraged to write correct code quicker, and identify defects at compile time, rather than run time.

Really, having to refactor lots of code because user information changes is merely a symptom of other problems. By passing an entire User object you gain the benefits above, in addition to the benefits of not having to refactor all method signatures that accept user information when something about the User class changes.

  • 6
    I'd say your reasoning by itself actually points towards passing fields but having the fields be trivial wrappers around the real value. In this example, a User has a field of type UserID and a UserID has a single integer-valued field. Now the declaration of Bar tells you immediately that Bar doesn't use all the information about the User, only their ID, but you still can't make any silly mistakes like passing an integer that didn't come from a UserID into Bar. – Ian Jun 26 '18 at 14:10
  • (Cont.) Of course this kind of programming style is quite tedious, especially in a language that doesn't have nice syntactic support for it (Haskell is nice for this style for example, since you can just match on "UserID id"). – Ian Jun 26 '18 at 14:12
  • 5
    @Ian: I think wrapping an Id in its own type skates around the original issue brought up by the OP, which is structural changes to the User class make it necessary to refactor many method signatures. Passing the entire User object solves this problem. – Greg Burghardt Jun 26 '18 at 15:33
  • @Ian: Although to be honest, even working in C# I've been very tempted to wrap Ids and the sort in a Struct just to give a little more clarity. – Greg Burghardt Jun 26 '18 at 15:34
  • Minor side point -- if User is stored on the stack, and thus would have to be copied in all of these places, there's nothing wrong with passing a pointer around in its place. You get all the same advantages, and if your User class is large, you also don't end up copying unnecessary data all the time. That's something you'd want to profile to see if it makes a difference, though, unless your User class is ridiculously big. –  Jun 26 '18 at 21:35
  • @NickHartley: In the context of Java or C# those objects would be allocated on the heap and garbage collected. Surely something to consider with the likes of C++, though. – Greg Burghardt Jun 26 '18 at 22:42
  • 1
    "there's nothing wrong with passing a pointer around in its place." Or a reference, to avoid all of the issues with pointers that you could run into. – Yay295 Jun 26 '18 at 23:57
  • What if I were to add that the class I am working with is serialized and sent to the client side regularly. So there is an incentive to keep it light. – Jimbo Jun 27 '18 at 09:16
17

Am I right in thinking this is a violation of the Open/Closed principle?

No, it isn't a violation of that principle. That principle is concerned with not changing User in ways that affect other parts of the code that use it. Your changes to User could be such a violation, but it's unrelated.

Are any other principles broken by this practice? Dependency inversion perahaps?

No. What you describe - only injecting the required parts of a user object into each method - is the opposite: it's pure dependency inversion.

Are there other, non-SOLID principles being violated, such as basic defensive programming principles?

No. This approach is a perfectly valid way of coding. It isn't violating such principles.

But dependency inversion is only a principle; it's not an unbreakable law. And pure DI can add complexity to the system. If you find that only injecting the needed user values into methods, rather than passing the whole user object into either the method or constructor, creates problems, then don't do it that way. It's all about getting a balance between principles and pragmatism.

To address your comment:

There are problems with having to unnecessarily parse a new value down five levels of the chain and then change all of the references to all five of those existing methods...

Part of the issue here is that you clearly don't like this approach, as per the "unnecessarily [pass]..." comment. And that's fair enough; there's no right answer here. If you find it burdensome then don't do it that way.

However, with respect to the open/closed principle, if you follow that strictly then "...change all of the references to all five of those existing methods..." is an indication that those methods were modified, when they should be closed to modification. In reality though, the open/closed principle makes good sense for public APIs, but doesn't make much sense for the internals of an app.

...but surely a plan to abide by that principle as far as it is practicable to do so would include strategies to reduce the need for future change?

But then you wander into YAGNI territory and it still would be orthogonal to the principle. If you have method Foo that takes a user name and then you want Foo to take a a date of birth too, following the principle, you add a new method; Foo remains unchanged. Again that's good practice for public APIs, but it's a nonsense for internal code.

As previously mentioned, it's about balance and common sense for any given situation. If those parameters often change, then yes, use User directly. It'll save you from the large-scale changes you describe. But if they don't often change, then passing in only what's needed is a good approach too.

David Arno
  • 39,270
  • There are problems with having to unnecessarily parse a new value down five levels of the chain and then change all of the references to all five of those existing methods.

    Why would the Open/Closed principle apply only to the User class and not also to the class I am currently editing, which is also consumed by other classes? I'm aware that the principle is specifically about avoiding change, but surely a plan to abide by that principle as far as it is practicable to do so would include strategies to reduce the need for future change?

    – Jimbo Jun 26 '18 at 12:19
  • @Jimbo, I've updated my answer to try and address your comment. – David Arno Jun 26 '18 at 13:13
  • I appreciate your contribution. BTW. Not even Robert C Martin accepts the Open/Closed principle has a hard rule. It's a rule of thumb that will inevitably be broken. Applying the principle is an exercise in attempting to abide it as much as is practicable. Which is why I used the word "practicable" previously. – Jimbo Jun 26 '18 at 13:46
  • It's not dependency inversion to pass the parameters of User rather than User itself. – James Ellis-Jones Jun 27 '18 at 10:06
  • @JamesEllis-Jones, Dependency invertion flips the dependencies from "ask", to "tell". If you pass in a User instance and then query that object to get a parameter, then you are only partially inverting the dependencies; there's still some asking going on. True dependency inversion is 100% "tell, don't ask". But it comes at a complexity price. – David Arno Jun 27 '18 at 10:09
  • @DavidAmo I'd certainly agree that this is similar to dependency inversion in that both things are techniques to limit the scope of the class i.e. restrict what it knows to the minimum, and that there's a complexity cost to doing this. Just in terms of how dependency inversion is usually defined, I'd still have to disagree as it involves replacing a concrete dependency with an abstract one. – James Ellis-Jones Jun 27 '18 at 11:15
10

Yes, changing an existing function is a violation of the Open/Closed Principle. You’re modifying something that should be closed to modification due to requirements change. A better design (to not change when requirements change) would be to pass in the User to things that should work on users.

But that might run afoul of the Interface Segregation Principle, since you could be passing along way more information than the function needs to do its work.

So, as with most things - it depends.

Using just a username let’s the function be more flexible, working with usernames regardless of where they come from and without the need to make a fully functioning User object. It provides resilience to change if you think that the source of data will change.

Using the whole User makes it clearer about the usage and makes a firmer contract with its callers. It provides resilience to change if you think that more of the User will be needed.

Telastyn
  • 109,398
  • +1 but I am not sure about your phrase "you could be passing along way more information". When you pass (User theuser) you pass the very minimum of information, a reference to one object. True that reference can be used to get more information, but it means the calling code doesn't have to get it. When you pass (GUID userid, string username) the called method can always call User.find(userid) to find the object's public interface, so you don't really hide anything. – dcorking Jun 26 '18 at 15:04
  • 5
    @dcorking, "When you pass (User theuser) you pass the very minimum of information, a reference to one object". You pass the maximum information related to that object: the entire object. "the called method can always call User.find(userid)...". In a well designed system, that would not be possible as the method in question would have no access to User.find(). In fact there shouldn't even be a User.find. Finding a user should never be the responsibility of User. – David Arno Jun 26 '18 at 15:11
  • 2
    @dcorking - enh. That you're passing a reference that happens to be small is technical coincidence. You're coupling the entire User to the function. Maybe that makes sense. But maybe the function should only care about the user's name - and passing along stuff like the User's join date, or address is improper. – Telastyn Jun 26 '18 at 15:21
  • @DavidArno perhaps that is key for a clear answer to OP. Whose responsibility should finding a User be? Is there a name for the design principle of separating the finder/factory from the class? – dcorking Jun 26 '18 at 16:05
  • 1
    @dcorking I would say that is one implication of the Single Responsibility Principle. Knowing where Users are stored and how to retrieve them by ID are separate responsibilities a User-class should not have. There may be a UserRepository or similar that deals with such things. – Hulk Jun 27 '18 at 10:32
3

This design follows the Parameter Object Pattern. It solves problems which arise from having many parameters in the method signature.

Am I right in thinking this is a violation of the Open/Closed principle?

No. Applying this pattern enables the Open/close principle (OCP). For instance derivative classes of User can be provided as parameter which induce a different behavior in the consuming class.

Are any other principles broken by this practice?

It can happen. Let me explain on the basis of the SOLID principles.

The Single responsibility principle (SRP) can be violated if it has the design as you have explained:

This class exposes all information about the user, their Id, name, levels of access to each module, timezone etc.

The problem is with all information. If the User class has many properties, it becomes a huge Data Transfer Object which transports unrelated information from the perspective of the consuming classes. Example: From the perspective of a consuming class UserAuthentication the property User.Id and User.Name are relevant, but not User.Timezone.

The Interface segregation principle (ISP) is also violated with a similiar reasoning but adds another perspective. Example: Suppose a consuming class UserManagement requires the property User.Name to be split up to User.LastName and User.FirstName the class UserAuthentication must be also modified for this.

Fortunately ISP also gives you a possible way out of the problem: Usually such Parameter Objects or Data Transport Objects start small and grow by time. If this becomes unwieldy consider following approach: Introduce interfaces tailored to the needs of the consuming classes. Example: Introduce interfaces and let the User class derive from it:

class User : IUserAuthenticationInfo, IUserLocationInfo { ... }

Each interface should expose a subset of related properties of the User class needed for a consuming class to fullfill its operation. Look for clusters of properties. Try to reuse the interfaces. In the case of the consuming class UserAuthentication use IUserAuthenticationInfo instead of User. Then if possible break up the User class into multiple concrete classes using the interfaces as "stencil".

  • 1
    Once User gets complicated, there is a combinatorial explosion of possible subinterfaces for example, if User has just 3 properties, there are 7 possible combinations. Your proposal sounds nice but is unworkable. – user949300 Jun 27 '18 at 18:51
  • Analytically you are right. However depending on how the domain is modeled bits of related information tend to cluster. So practically it is not necessary to deal with all possible combinations of interfaces and properties. 2. The outlined approach was not intended to be a universal solution, but maybe I should add some more 'possible' and 'can' into the answer.
  • – Theo Lenndorff Jun 27 '18 at 19:20