Class Design Guidelines

Edit this page

A class or interface should have a single purpose (RG1000)

A class or interface should have a single purpose within the system it functions in. This rule is widely known as the Single Responsibility Principle, one of the S.O.L.I.D. principles.

Here’s an example list of the sorts of things that might count as a single responsibility:

  • Wrapping a primitive type to describe some value like an email or ISBN number
  • Holding data that describes a more general business concept, like a table schema or a set of customer details
    These may be value objects, entities or aggregates as defined by DDD.
  • Calculating some different data based on input data
  • Providing a service to interact with the outside world
  • Providing a place where data objects can be stored
    This might be some kind of repository interface, which defines CRUD operations for a particular data class, or it might be decomposed down further into individual command and query objects.
  • The same responsibility as another class, but at a different level of abstraction
    For example, one class might be responsible for handling generic data storage, while another class provides a JSON-file-based implementation of the same interface.
  • The same responsibility as another class, but within a different bounded context
    For example, there may be many different ‘Customer’ classes spread between forums, finance, research appointments or contact systems, since each of these systems has a different view on what is relevant about the ‘Customer’ concept.
  • Implementing a framework-specific job (for example, an ASP.NET MVC controller, a WPF viewmodel, or an NUnit test fixture)
    This implies, for instance, that a controller class should have very little code which isn’t directly tied to ASP.NET, and should delegate any other logic off to some other service.

Tip: Some smells that might imply a violation of this rule: a class with And in its name, or a class which appears to implement more than one Design Pattern.

Split code into a ‘functional core’ and ‘imperative shell’ (RG1000B)

As a whole, codebases should decompose into a ‘functional core’ and an ‘imperative shell’, with most of the above responsibilities falling on one side or the other. The first video from that link (‘Boundaries’ by Gary Bernhardt) explains the concept in more detail and is recommended viewing.

By keeping other responsibilities out of data classes, we prevent responsibilities from accumulating on them, and make it easier to divide up other parts of the application.

Orchestrator classes may be used to join the two parts together, with data classes/value types at the boundaries: for example, taking a list of actions to perform (as data) calculated by one class, and then passing them to another class which will actually run those actions as side-effects.

Objects should be immutable by default (RG1000C)

Objects should be immutable by default. Mutation should be an explicit decision.

We prefer immutability because:

  • Immutable objects are just simpler in a lot of cases. If the object’s state can’t change over its lifetime, that’s one less thing to worry about when reasoning about what the object does.
  • Immutable objects tend to get updated atomically, by some top-level reference being changed, rather than piece-by-piece. In non-immutable state, you can get quite a lot of emergent complexity coming out of trying to track what order things get mutated in. For example, Redux encourages you to keep all of your application state in one static location. This would probably be a terrible idea if it wasn’t also a) immutable and b) only updated through a well-defined process.
  • Immutable objects are shareable by default. You can pass a single immutable object out to multiple consumers, without having to worry about one consumer mutating the object and breaking the others. You can share an immutable object between different threads without having to worry about race conditions on the object’s state. In some cases, immutable “persistent data structures” can be more memory-efficient than mutable objects, since they can share some subset of the data structure when being updated where the entire object might need to be cloned instead.

We don’t get particularly strict on “true immutability” (after all, when you get down to it, system RAM is just a giant pile of mutable state). Objects that appear to the outside world to be immutable (such as Lazy, or a method which appears immutable but uses a for loop internally) are generally fine.

Immutable objects are inherently thread-safe. When making an object mutable, it’s a good idea to consider whether it could be mutated by more than one thread. If necessary, use thread-safe collections or synchronize access to the object.

Tip: Eric Lippert has a great series on immutability at 1, 2, 3, 4.

Tip: You might be tempted to think that struct is a good pattern for immutable objects (after all it provides value-type semantics). However, MSDN suggests you should only use a struct if the instance size is under 16 bytes which rules it out for most practical cases.

Tip: The Builder pattern can help you create immutable objects.

Only create a constructor that returns a useful object (RG1001)

There should be no need to set additional properties before the object can be used for whatever purpose it was designed. However, if your constructor needs more than three parameters (which violates RG1561), your class might have too much responsibility (and violates RG1000).

Avoid doing “work” in constructors (RG1002)

Where work means:

  • stuff that takes a long time or significant processing effort;
  • anything with side-effects;
  • creating dependencies (instead of injecting them from constructor parameters).

since:

  • constructors can’t be async;
  • it makes the class a lot harder to test if the class is hard to construct;
  • it makes any classes that depend on this class harder to test as well;
  • IOC containers often throw really complicated errors if they fail to construct an object.

If your constructor throws exceptions which aren’t immediately obvious ArgumentException/ArgumentNullExceptions, this may be a sign that it is doing work.

Consider pulling out a static factory method or factory class instead.

An interface should be small and focused (RG1003)

Interfaces should have a name that clearly explains their purpose or role in the system. Do not combine many vaguely related members on the same interface just because they were all on the same class. Separate the members based on the responsibility of those members, so that callers only need to call or implement the interface related to a particular task. This rule is more commonly known as the Interface Segregation Principle.

Use an interface rather than a base class to support multiple implementations (RG1004)

If you want to expose an extension point from your class, expose it as an interface rather than as a base class. You don’t want to force users of that extension point to derive their implementations from a base class that might have an undesired behavior. However, for their convenience you may implement a(n abstract) default implementation that can serve as a starting point.

Use an interface to decouple classes from each other (RG1005)

Interfaces are a very effective mechanism for decoupling classes from each other:

  • They can prevent bidirectional associations;
  • They simplify the replacement of one implementation with another;
  • They allow the replacement of an expensive external service or resource with a temporary stub for use in a non-production environment.
  • They allow the replacement of the actual implementation with a dummy implementation or a fake object in a unit test;
  • Using a dependency injection framework you can centralize the choice of which class is used whenever a specific interface is requested.

Use Dependency Injection (RG1006)

One of the parts of SOLID is the Dependency inversion principle which aims at minimizing coupling. Abstractions should not depend on details (see RG1005).

Creating dependent objects in the constructor is a violation of this principle. Instead of having your objects create dependencies (or asking a factory to make them for them), pass the needed dependencies into the object externally. Simply put, avoid using new to build dependencies of your object.

If you apply dependency injection everywhere, then eventually somewhere someone has to create those dependencies (composition root).

For small applications with a single entry point, you can just new up the objects and pass them around (Pure DI). In this contrived simple case, it’s all simple.

static void Main(string args[]) {
    var logger = new Logger();
    var database = new Database(logger, args);
	var controller = new Controller(logger, database)

    new Application(logger, database, controller).Run();
}

As applications grow, the constructor tree gets more complicated. Creating objects in the right order becomes genuinely complicated because of dependency trees.

Some frameworks (such as ASP.NET) have a built-in solution for dependency resolution (example). If that’s present, then we prefer to use that over manually constructed object graphs.

If a built-in solution isn’t available then we recommend using AutoFac (our currently recommended DI engine)

Dependency Injection: Principles, Practices and Patterns is recommended reading around this area.

Avoid static classes (RG1008)

With the exception of extension method containers, static classes very often lead to badly designed code. They are also very difficult, if not impossible, to test in isolation, unless you’re willing to use some very hacky tools.

Note: If you really need that static class, mark it as static so that the compiler can prevent instance members and instantiating your class. This relieves you of creating an explicit private constructor.

Don’t suppress compiler warnings using the new keyword (RG1010)

Compiler warning CS0114 is issued when breaking Polymorphism, one of the most essential object-orientation principles. The warning goes away when you add the new keyword, but it keeps sub-classes difficult to understand. Consider the following two classes:

public class Book  
{
	public virtual void Print()  
	{
		Console.WriteLine("Printing Book");
	}  
}

public class PocketBook : Book  
{
	public new void Print()
	{
		Console.WriteLine("Printing PocketBook");
	}  
}

This will cause behavior that you would not normally expect from class hierarchies:

PocketBook pocketBook = new PocketBook();

pocketBook.Print(); // Outputs "Printing PocketBook "

((Book)pocketBook).Print(); // Outputs "Printing Book"

It should not make a difference whether you call Print() through a reference to the base class or through the derived class.

It should be possible to treat a derived object as if it were a base class object (RG1011)

In other words, you should be able to use a reference to an object of a derived class wherever a reference to its base class object is used without knowing the specific derived class. A very notorious example of a violation of this rule is throwing a NotImplementedException when overriding some of the base-class methods. A less subtle example is not honoring the behavior expected by the base class.

Note: This rule is also known as the Liskov Substitution Principle, one of the S.O.L.I.D. principles.

Don’t refer to derived classes from the base class (RG1013)

Having dependencies from a base class to its sub-classes goes against proper object-oriented design and might prevent other developers from adding new derived classes.

Avoid exposing the other objects an object depends on (RG1014)

If you find yourself writing code like this then you might be violating the Law of Demeter.

someObject.SomeProperty.GetChild().Foo()

An object should not expose any other classes it depends on because callers may misuse that exposed property or method to access the object behind it. By doing so, you allow calling code to become coupled to the class you are using, and thereby limiting the chance that you can easily replace it in a future stage.

Note: Using a class that is designed using the Fluent Interface pattern seems to violate this rule, but it is simply returning itself so that method chaining is allowed.

Exception: Inversion of Control or Dependency Injection frameworks often require you to expose a dependency as a public property. As long as this property is not used for anything other than dependency injection I would not consider it a violation.

Avoid bidirectional dependencies (RG1020)

This means that two classes know about each other’s public members or rely on each other’s internal behavior. Refactoring or replacing one of those classes requires changes on both parties and may involve a lot of unexpected work. The most obvious way of breaking that dependency is to introduce an interface for one of the classes and using Dependency Injection.

Exception: Domain models such as defined in Domain-Driven Design tend to occasionally involve bidirectional associations that model real-life associations. In those cases, make sure they are really necessary, and if they are, keep them in.

Classes should protect the consistency of their internal state (RG1026)

APIs should be valid for any way the caller chooses to use it. It’s not the callers responsibility to know the format of the arguments a class should take.

The preferred method for communicating what an API can accept is to use the type system and aim for the method signature to define what the function does. For example, don’t take an int that can be only one of five values, use an enum to define that range. Similarly, don’t use a string if you mean something in a specific format (see Primitive Obsession).

This isn’t always possible (for example, validating an object is disposed only once). When it’s not, assert the state is valid and throw meaningful exceptions when invalid state is detected.