Click here to Skip to main content
Click here to Skip to main content

Attributes vs. Single Responsibility Principle

By , 9 Nov 2012
 

Background 

Attributes are a great resource that allows developers to put extra information on types and members.  

They are great in the sense that we put all we need together and they are completely refactoring friendly, as when a member is renamed all its attributes stay there. But sometimes (or should I say many times) that initial benefit ends up causing more trouble than they really solve.

Single Responsibility Principle

In Wikipedia I found this definition:

In object-oriented programming, the single responsibility principle states that every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.

Even if the idea is great, it does not really tell me when a class (or better, a type as we have structs too) has a single responsibility or not.

Let's see the int (System.Int32) type.

An int variable is a simple value holder. But the struct has methods to parse a string and return an int, to convert that int to a string, to compare an int with another, to do math operations, and so on. It is OK to me, but I can already say that it is a little counter intuitive that some math operations are in the int type itself while others are in the Math class. Shouldn't methods like Math.Max be in the original types (int.Max, double.Max, and so on)?

Well, maybe, but this article is not really to discuss that point. The point I want to discuss are attributes and how they violate the Single Responsibility Principle.

So, let's see some attribute usages:

[Serializable]
[DisplayName("Non-Empty String")]
[Description("This struct simple holds a string value, which must be null or have a non-empty content.")]
public struct NonEmptyString
{
  public NonEmptyString(string value)
  {
    if (value.Length == 0)
      throw new ArgumentException("value must be null or should have a non-empty content.", "value");

    _value = value;
  }

  private readonly string _value;
  [Description("This property should be null or have a non-empty content.")]
  public string Value
  {
    get
    {
      return _value;
    }
  }
}

The struct was created with a single responsibility in mind (store a non-empty string). You may not like the idea of creating a struct for such a simple validation, but that's not the point here. The point is its responsibility that was altered to:

  • Make it [Serializable], so it can be used in ASP.NET external sessions (or serialized for any other reason);
  • Put a better name and description, in case it is used by some kind of editor.

See the problem?

If not, I will tell you. The type should not be worried about the text description to be used by a possible editor. It should not care about a Serializable attribute. It is the responsibility of a serializer to know if it is serializable or not. It is the responsibility of an editor to know some way of finding a better name and description.

Also, I can say that there are other problems, like, how do we support non-English descriptions with this model?

To make the code conform to the Single Responsibility Principle, it should look like this:

public struct NonEmptyString
{
  public NonEmptyString(string value)
  {
    if (value.Length == 0)
      throw new ArgumentException("value must be null or should have a non-empty content.", "value");

    _value = value;
  }

  private readonly string _value;
  public string Value
  {
    get
    {
      return _value;
    }
  }
}

That's OK from the Single Responsibility Principle idea. But then how do we solve the [Serializable] thing? How can an editor find the right DisplayName and Description?

Well... that can be a little tricky with the old code. Surely we can create yet another type, like SerializableNonEmptyString that holds a NonEmptyString and adds serialization support. But if we want to serialize an object that already holds a NonEmptyString we will end-up needing to copy the object to a similar one with serializable types. So, for those cases, maybe it is better to violate the Single Responsibility Principle. Or if we have the option, we can recreate the serialization process (or use an alternative one) that allows us to add specific serialization code for already existing types.

Effectively, the idea is: Every type will have a single responsibility. So the NonEmptyString has the single responsibility of holding a string that is either null or non-empty. Then, a serializer type will know how to serialize a NonEmptyString. It is enough to inform a serialization mechanism that we have the serializer for such a type.

In fact, with this model, any type that already exists on a third-party DLL can be serialized if it is possible to access all needed properties to store them and reconstruct (or find) the instance later. So, the data type can exist in an assembly and the serializer for it can exist in another assembly. I can say that I use that to serialize WPF colors, for example.

What about the DisplayName and Description?

Well... I can say that a Dictionary<MemberInfo, string> will do the job. Surely there should be some code available to load descriptions for MemberInfos, but with such a dictionary we can already bind a DisplayName or Description to any existing member (in fact, or we will have two different classes with a similar structure, or we should create another type to store the DisplayName and the Description together).

The best thing with this approach is that we can, again, do it in different assemblies. So, if we are dealing with 3rd party classes, we can still add a display name or a description to the types found there. With a little more work we can support different languages, and everything without changing the original types.

Sample Code

The sample code in this article is a small Binary Serialization library that works using the Single Responsibility Principle. ConfigurableBinarySerializer must be configured (that is, the serializer for each item type must be added by the user). Also, there is a very small sample with it that only shows how to register the serializers, how to serialize and deserialize. If everything works OK, the data is serialized to and deserialized from a memory stream, and the console application finishes. So, only use it to see the code.

I don't expect that people simple use this serializer as a replacement for normal serialization techniques, but I hope it gives a better understanding on the Single Responsibility Principle

One way versus the way

I already received two messages from people that say that attributes don't violate the Single Responsibility Principle. They in fact have valid points. But let's look from an outside view.

Someone (a user, a chef, another programmer) asks for a component that:

  • Has two properties.
  • Has two more properties that are calculated from the first ones (P1+P2 and P1*P2).

The programmer easily does something like this:

public class SomeClass
{
  public int P1 { get; set; }
  public int P2 { get; set; }

  public int Sum
  {
    get
    {
      return P1 + P2;
    }
  }
  public int Multiplication
  {
    get
    {
      return P1 * P2;
    }
  }
}

In fact, the code works. But then the developer receives a "warning" that his code is not right. The fact is:

  • He must put the [Serializable] attribute in his class, or else it will not work
  • He must put a Description on the calculated properties.

But to him, that was not what was asked. You may consider him lazy, but he did what he was asked to do.

Is he really wrong?

My answer is no. Not because someone forgot to say that the class was serializable, but because the purpose of the class is to store data. Being serializable is another "trait" of such a class... but it is better to put that trait somewhere else and let the one responsible for seeing what's serializable to say: Hey, that's a class that can be serialized without problems!

Don't put the need on the programmer that did the class. And, consequently, don't put that need in the class itself. If someone else, at a later time, can figure out that it's possible, allow that. That means: Allow that to be put at run-time, instead of only allowing that at compile-time. And that's where the real question is:

Are you using attributes as hints or are you using it as needs? As hints, that may be a small violation. But as needs, that's a problem.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Paulo Zemek
Canada Canada
Member
I started to program computers when I was 11 years old, as a hobbist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.
 
At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do they work easier, faster and with less errors.
 
Want more info or simply want to contact me?
Take a look at: www.paulozemek.com
Or e-mail me at: paulozemek@outlook.com

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
AnswerRe: Also disagreemvpPaulo Zemek16 Aug '12 - 11:36 
Well... the loading of a class, even if it is not instruction code, is a problem, specially if it becomes a need (that's why I consider the Serializable attribute a problem... it is not the default way of transforming a class into a serializable one... it is required, using ISerializable or not is another problem).
 
But to me Single Responsibility Principle applies to the class and to the developer of the class. If the class has a single responsibility, but someone is required to put code in the class (even if that code is only attribute declaration) to fulfill other needs, then the developer is changing the single responsibility of the class (and its own).
GeneralI disagree with this entire articlememberJohn Woodard16 Aug '12 - 7:30 
I would suggest that [Serializable] does NOT voilate SRP because you are not adding code to the class in order to serialize itself.
 
Essentially what you are doing is marking the class so another class (or method somewhere) will take your class as an argument and break it down or build it back up. It's no different than writing your own custom serializer for the class, except for the fact that you don't have to write it yourself.
 
Let me say it another way. You are not adding functionality to the class with the [Serializable] attribute, you are auto-generating code somewhere else that uses this class (or, depending on the attribute, just invoking some method somewhere that uses your class). Which is exactly what your custom serializer would have to have done.
 
So looking at it like that, it seems to me that attributes help you preserve SRP instead of threatening it.
 
Now, if you overrode ISerializable in your class, then yes that would potentially violate SRP.
GeneralRe: I disagree with this entire articlemvpPaulo Zemek16 Aug '12 - 7:45 
I agree and disagree.
 
Surely you are not writing the code that serializes your object (you are putting a "hint", so another code use it).
 
But, what happens if the object has a valid structure for serialization but the creator simple forgot to use the attribute? I can say that it is a real situation that happens very often.
Surely you can say: It was an error.
But what happens when you start to have more and more attributes that only serve as a hint?
Shouldn't even the "hint" be somewhere else? That is, the creator of the class does not has to worry at all about these other "traits" of the class?
 
So, I still consider that [Serializable] is another responsability that's being bound to this class. The ISerializable is even worse, but both share the same problem.
GeneralRe: I disagree with this entire articlememberninerats20 Aug '12 - 5:41 
Seems like there are two things going on here:
1. Do attributes (technically) violate SRP?
2. Should you use them?
 
1. I say no for the "hint" type - because they are not the classes *responsability* - they are additional info for a consuming class which has the Responsability. Now, that doesn't mean you don't have to worry about them when you edit the class. It means changing requirements won't break you code with attributes on it.
2. the article and comments bring up some cases where attributes incorrectly couple behaviors that shoul go together. That's obviously wrong, but a slightly different issue. However, it was a good insight and will change the way I look at attribute going forward.
GeneralMore examples...mvpPaulo Zemek16 Aug '12 - 8:04 
Maybe [Serializable] does not look so bad. I hope that at least the DisplayName/Description show the problem.
 
But let's imagine this situation:
We have "some instance" that:
A) Contains some data.
B) Does some actions.
 
But, after a revision, we see that such object may be serializable. So, we put the [Serializable] attribute. We did change the code to fullfill that need. And it seems to be ok.
 
Someone else's revises the code and says: OK... this code conforms to our Aspect generator system, it only needs one extra attribute to do loggings [Logable].
 
But that same Aspect generator also allows to add other aspects, like make the object read-only (or to Freeze it). But, to do so, it is important to put the attribute [Freezable] in that object.
 

In fact, any aspect may be added to such object and adding such aspects don't require new code. It is enough to simple put the mark on the type.
So, considering there is a list of 10 aspects that can be added to any instance, it is enough to put 10 attributes in the object.
 
Then I ask you... is this really not violating the single responsibility principle?
To me, having to change a type to "hint" other code that it conforms to the specification of something else is a violation. The writer of one code (that knows very well that it works for its intended purpose) may be committing an error simple by "forgotting" that someone else may have a "code generator" that needs a hint.
 
Giving the hint should not be part of the type specification. For example, if I remember well, Silverlight does not has the [Serializable] attribute. This does not means the same types (like the primitives and normal structs) can be serialized. This simple means that someone writing a new type in silverlight does not have to worry about that. The programmer developping a serialization framework is the one that should deal with that.
 
In fact, serialization can be very well developped after the object is deployed. It should not be part of such object.
GeneralRe: More examples...memberninerats16 Aug '12 - 21:06 
Attributes are metadata, not code. They don't add responsibilities to a class. The main idea bind SRP is dependency management - the class should only have "one reason to change". Attributes don't give the class a reason to change. Your concern about attribute pollution makes sense, but seems like a different kind of problem.
GeneralRe: More examples...memberjsc4220 Aug '12 - 0:25 
I agree that attributes are metadata, but they do add responsibilities to the code. What if you added an attribute that the code does not / cannot honour, e.g. if it was impossible to serialise it? Which is wrong? The code or the attribute?
GeneralRe: More examples...mvpPaulo Zemek20 Aug '12 - 3:14 
I can go on and also comment on validation attributes.
 
Doing:
[CheckRange(0, 100)]
public int SomeRange { get; set; }
 
Is effectively putting code (the CheckRange) to a property.
A call like CheckRange(() => SomeRange, 0, 100) will do exactly the same. But in one case it is a metadata, in another case it is real code.
 
Either way, as an attribute, it is putting the Validation responsability into the class declaration. The validation can very easily be put in a validation method or in a validation class.
GeneralRe: More examples...memberninerats20 Aug '12 - 5:46 
I agree this violates SRP, because it injects behavior. If the requirement to be between 1 and 100 changed, this class would have to change.
GeneralRe: More examples...memberninerats20 Aug '12 - 5:43 
The attribute would be wrong, because it is lying. But that's not an SRP violation, that's just wrong.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 9 Nov 2012
Article Copyright 2012 by Paulo Zemek
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid