Click here to Skip to main content
15,868,419 members
Articles / Programming Languages / C#

Attributes vs. Single Responsibility Principle

Rate me:
Please Sign up or sign in to vote.
4.92/5 (13 votes)
9 Nov 2012CPOL6 min read 68.8K   213   24   54
This article explains how attributes can violate the single responsibility principle and presents some ideas to avoid the problem.

Background

Attributes are a great resource that allow 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:

C#
[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:

C#
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 simply 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:

C#
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.

History

  • 16th August, 2012: Initial version

License

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


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, 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 their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions

 
QuestionHere is the real problem.... Pin
Erik Funkenbusch7-Feb-15 21:45
Erik Funkenbusch7-Feb-15 21:45 
AnswerRe: Here is the real problem.... Pin
Paulo Zemek8-Feb-15 0:02
mvaPaulo Zemek8-Feb-15 0:02 
GeneralRe: Here is the real problem.... Pin
Erik Funkenbusch8-Feb-15 10:01
Erik Funkenbusch8-Feb-15 10:01 
GeneralRe: Here is the real problem.... Pin
Paulo Zemek8-Feb-15 10:51
mvaPaulo Zemek8-Feb-15 10:51 
GeneralRe: Here is the real problem.... Pin
Erik Funkenbusch8-Feb-15 11:47
Erik Funkenbusch8-Feb-15 11:47 
GeneralRe: Here is the real problem.... Pin
Paulo Zemek8-Feb-15 13:31
mvaPaulo Zemek8-Feb-15 13:31 
Yes... you "are right". "It doesn't matter that the end-result can be exactly the same, what matters is the time when things are executed."
Honestly, you aren't supposed to know when attributes are loaded and attached to types. When you request them, you get a copy. If registering them is actually executed as part of the library loading, together with the static constructor or etc is an implementation detail, and the MONO implementation can be different from the Microsoft one.

You are really right that we can always change closed sourced libraries by emitting new code and injecting things. And we can also always access a private member by using unsafe code and unsafe reflection. So, any principle is moot. We can always make unmaintainable code maintainable by adding more code, adapters, façades and layers.

So, this discussion is moot too. If you want, feel free to vote one. I know that I will not change your opinion, and you will not change mine. The most incredible is that I am able to use the Single Responsibility Principle and the Open-Closed Principle with my strict view of what's a change.


That is, maybe you are right by your interpretation of the principle. Maybe the original idea of the principle really accepts that SRP allows you to keep modifying the file as long as you don't modify the class, and attributes are "attached". So, I am really talking about my interpretation of SRP. And I will probably write an article about my view on SOLID. To me, SOLID doesn't state anything about attributes, but it states ideas. By my view and keeping the ideas in mind, changing a unit by any reason that's not its main purpose is a violation of the single responsibility principle... for that UNIT.
That is, a unit should contain a single reason to change, and this reason is a change to its single class. If you need to change to unit to attach something to the class, there's a violation. If you put 2 classes in the unit, there's a violation.
Partial classes solve the problem for the unit (not for the class), by allowing developers to stop changing a specific unit, even to add new methods. The new unit, may or may not be violating the principle according to its purpose. A unit created do add attributes to existing classes will not be violating anything. But by my view the class will by requiring to be recompiled. I don't care if the compiler recompiles the entire project when I do a build. What I care is that I should not need to explicitly ask to rebuild a unit if the SRP is working fine. And this must work in a single project that includes all units or in a "library per class" basis.
In any case, I see this discussion is not going to end. So, keep your idea of single responsibility principle and understand this as "Paulo Zemek's closed view on SOLID principles: Discussion about SRP". I am really thinking about posting an article about my view on SOLID principles. Really, as to me most principles seem to be interpreted in 10 different ways and the discussion is always about a small thing like "the principle doesn't state attributes, libraries, dynamic languages or anything different."

modified 8-Feb-15 20:21pm.

QuestionAgreed, but then not... Pin
Sander Rossel11-Aug-14 3:23
professionalSander Rossel11-Aug-14 3:23 
AnswerRe: Agreed, but then not... Pin
Paulo Zemek11-Aug-14 4:10
mvaPaulo Zemek11-Aug-14 4:10 
GeneralRe: Agreed, but then not... Pin
Sander Rossel11-Aug-14 4:22
professionalSander Rossel11-Aug-14 4:22 
GeneralRe: Agreed, but then not... Pin
Paulo Zemek11-Aug-14 5:30
mvaPaulo Zemek11-Aug-14 5:30 
QuestionI agree Pin
Jarmo Muukka18-Nov-13 23:14
Jarmo Muukka18-Nov-13 23:14 
AnswerRe: I agree Pin
Paulo Zemek19-Nov-13 1:58
mvaPaulo Zemek19-Nov-13 1:58 
GeneralMy vote of 1 Pin
KitKat3133728-Jun-13 6:52
professionalKitKat3133728-Jun-13 6:52 
GeneralRe: My vote of 1 Pin
Paulo Zemek28-Jun-13 6:56
mvaPaulo Zemek28-Jun-13 6:56 
GeneralRe: My vote of 1 Pin
KitKat3133728-Jun-13 7:34
professionalKitKat3133728-Jun-13 7:34 
GeneralRe: My vote of 1 Pin
Paulo Zemek28-Jun-13 7:45
mvaPaulo Zemek28-Jun-13 7:45 
GeneralRe: My vote of 1 Pin
KitKat3133728-Jun-13 8:10
professionalKitKat3133728-Jun-13 8:10 
GeneralRe: My vote of 1 Pin
Paulo Zemek28-Jun-13 8:37
mvaPaulo Zemek28-Jun-13 8:37 
GeneralRe: My vote of 1 Pin
KitKat3133728-Jun-13 9:37
professionalKitKat3133728-Jun-13 9:37 
GeneralRe: My vote of 1 Pin
Paulo Zemek28-Jun-13 10:26
mvaPaulo Zemek28-Jun-13 10:26 
GeneralRe: My vote of 1 Pin
Paulo Zemek28-Jun-13 10:51
mvaPaulo Zemek28-Jun-13 10:51 
GeneralRe: My vote of 1 Pin
Paulo Zemek28-Jun-13 11:09
mvaPaulo Zemek28-Jun-13 11:09 
QuestionHow much software have you written? Pin
Jasmine250112-Nov-12 12:54
Jasmine250112-Nov-12 12:54 
AnswerRe: How much software have you written? Pin
Paulo Zemek23-Feb-13 5:36
mvaPaulo Zemek23-Feb-13 5:36 
GeneralRe: How much software have you written? Pin
Jasmine250123-Feb-13 15:32
Jasmine250123-Feb-13 15:32 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.