Class Naming and Division in C#
Naming classes in code is hard.
Naming classes in code is hard. Microsoft published an MSDN article some time ago with some guidelines. They make some good points, and all these guidelines align with the practices they've used when building the .NET Framework. However, it doesn't really describe a process of naming classes, or determining boundaries within which classes should reside.
The opinions expressed in this article are my own, and I'm not claiming that any of this is the One True Way. It's up to you to determine if any of this information is beneficial to you. Experienced programmers won't get much out of this.
This is a living guide; updates will be made to this article.
Divide and Conquer
I like to split classes into a few kinds. These will be in the order that they need to be introduced, which is not necessarily simplest to most complex.
Model
Models hold data. I don't put logic into a model, and I also use a singular noun as the class name without any embellishment.
public class Person
{
public string Name { get; set; }
public string Email { get; set; }
}
Ideally, models would be immutable. Unfortunately, at the time of this writing, C# doesn't have a nice way of combining immutability and initializers. So, they are mutable properties, and a model can easily be created this way:
var foxy = new Person
{
Name = "Saxxon Fox",
Email = "saxxonpike@gmail.com"
}
We are able to specify only the properties we want to change. All other properties are left as their default values. When using models this way, use sensible defaults, and plan for the absence of data.
View
Views represent a model's data differently. Views don't change the data itself, nor do they actually contain any of the data. They are often used to make raw data "pretty".
Here's a model to demonstrate:
public class UnitedStatesAddress
{
public string Name { get; set; }
public List<string> AddressLines { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
}
Here's what the view looks like:
public class UnitedStatesAddressView
{
public string GetPrettyAddress(UnitedStatesAddress address)
{
var builder = new StringBuilder();
builder.AppendLine(address.Name);
foreach (var addressLine in address.AddressLines)
{
builder.AppendLine(addressLine);
}
builder.AppendLine($"{address.City}, {address.State} {address.Zip}");
return builder.ToString();
}
}
This view takes the data from the model and formats it in a way that would be appropriate to write as a mailing address on a package within the USA. (Correct, there is no validation; that is outside the scope of this example.)
Mapper
Mappers take data from one model and stuff it into another model. This is particularly useful for connecting two systems that are unrelated and possibly not directly compatible. Mappers may change how an individual value is represented during the mapping process, but not the value itself.
Let's say one class library has a model that looks like this:
public class User
{
public string Name { get; set; }
public string Email { get; set; }
public int UserId { get; set; }
}
And another class library has a model that looks like this:
public class Person
{
public string Name { get; set; }
public string Email { get; set; }
}
Even though there's a lot of commonality in the models, they are still different classes, and the compiler will treat them completely different. In order to get data from one model to another, here's our mapper:
public class PersonMapper
{
public Person MapUser(User user)
{
return new Person
{
Name = user.Name,
Email = user.Email
}
}
}
In this case, the name of the mapper describes the model that comes out, and the names of the methods describe which model goes in. While writing new code, it's easier for me to think in terms of models I need rather than models I have. Autocomplete will help me select the appropriate method.
There is a possibility of using overloads instead so that one could just drop the source model right into the parameter list. Name all the methods Map
and away we go. I'd leave this to the discretion of the reader.
Transformer
A transformer is similar to a mapper in that it accepts one model and spits out another. The difference is that transformers will actually form new values on the target model based on values from the source model.
We'll use the models from the mapper example above, with a subtle change. The name formats are different:
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public int UserId { get; set; }
}
public class Person
{
public string Name { get; set; }
public string Email { get; set; }
}
It looks like we'll have to format some of the data in order to make things work. Here's our transformer:
public class PersonTransformer
{
public Person TransformUser(User user)
{
return new Person
{
Name = $"{user.FirstName} {user.LastName}",
Email = user.Email
}
}
}
Some of our output data is now a compound version of input data. It should be treated as an entirely new value.
Often, the term 'mapper' might be used to cover both the mapper and transformer as described here. I don't believe that these two are the same. When I'm unsure, I look at other parts of the code to see how others have done it in the past, and follow that naming pattern.
Factory
Factories create objects of a particular class. It is useful in cases where the creation of objects relies on some sort of configuration that each created object shares. It is an especially helpful pattern for separating how objects are created from how these objects are used. For example, let's use this simple model class:
public class Fruit
{
public string Kind { get; set; }
}
Let's also use this factory that can create objects of our model class:
public class FruitFactory
{
public FruitFactory(string fruitKind)
{
FruitKind = fruitKind;
}
private string FruitKind { get; }
public Fruit Build()
{
return new Fruit { Kind = FruitKind };
}
}
The idea is every time that a factory is used to create an object, it's always a new object. Now, two factories can exist with a slightly different configuration:
var strawberryFactory = new FruitFactory("strawberry");
var bananaFactory = new FruitFactory("banana");
Each call to strawberryFactory.Build()
will give you a Fruit
object with its Kind
property set to "strawberry"
. Likewise, each call to bananaFactory.Build()
will give you a Fruit
object with its Kind
property set to "banana"
. The idea is that these factories can then be passed around to other parts of the program that don't care about what kind of Fruit
they receive so long as they are able to build Fruit
objects on demand.
I name factories after the models they create. So, a factory that creates a model called Song
would be called SongFactory
. If it needs to be qualified, I put the qualifiers in front of the name like so: ElectroSongFactory
.
In a more practical sense, factories are very often used with external configuration. It could be that database connections need to be created and disposed, and the host name for the server to connect to needs to be externally configured either via app.config
or some other place. That configuration can be fed into the factory when it's created.
Repository
A repository provides access to a data store. This access could be via a database, a file, an internet resource, anything that represents persistent data storage. The point of a repository is to protect the rest of the system from knowing whatever this storage mechanism might be. How it's stored should make no difference to the rest of the application.
Here's an example that accesses a file on disk. We'll start with the model:
public class Person
{
public Guid? Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Here is the repository, which handles loading and saving of Person
objects. (This does no validation or sanitation, so do not use this code outright. It was kept simple to demonstrate the real meat of the pattern.)
public class PersonRepository
{
public PersonRepository(string basePath)
{
BasePath = basePath;
}
private string BasePath { get; }
private string GetPathForId(Guid id)
{
return Path.Combine(BasePath, $"{id}.txt");
}
public Person Get(Guid id)
{
var data = File.ReadAllLines(GetPathForId(id));
return new Person
{
Id = id,
Name = data[0],
Email = data[1]
}
}
public Guid Save(Person person)
{
var id = person.Id;
if (!id.HasValue)
{
id = Guid.NewGuid();
}
var data = new[] {
person.Name,
person.Email
};
File.WriteAllLines(GetPathForId(id.Value), data);
return id.Value;
}
}
It's a good idea to return the unique ID of whatever model was saved so that it can be referenced elsewhere in the application. In this case, if an ID is provided, it will overwrite the existing model. But if an ID is not provided, a new ID is generated.
Tricky Disco
There are some class types and patterns that sometimes become tiny red flags.
Service
A service is a bucket of related functionality. Often, the word 'service' doesn't mean anything on its own. In large, uncoordinated codebases, something called a service can encapsulate any of the above behaviors. It's a mystery box.
It can be considered useful to combine classes and patterns listed in the above section in a more cohesive manner. But in this case, the service shouldn't have any special logic, and instead coordinate the above classes to achieve a more broad task.
Closing
The tools and patterns we have today are awesome because we really can construct just about anything. The terrifying part is the sheer number of possibilities to the same end. We will make a lot of mistakes and learn a lot of patterns that work for us individually.
This is a collection of patterns I found most useful myself. What is described here isn't a textbook definition of anything, they're simply ways I have, over the years, explored patterns that work for me personally. Perhaps you've found something insightful, too.