- Clean Code in C#
- Jason Alls
- 1766字
- 2025-04-04 12:56:15
Avoiding multiple parameters
Niladic methods are the ideal type of methods in C#. Such methods have no parameters (also known as arguments). Monadic methods only have one parameter. Dyadic methods have two parameters. Triadic methods have three parameters. Methods that have more than three parameters are known as polyadic methods. You should aim to keep the number of parameters to a minimum (preferably less than three).
In the ideal world of C# programming, you should do your best to avoid triadic and polyadic methods. The reason for this is not because it is bad programming, but because it makes your code easier to read and understand. Methods with lots of parameters can cause visual stress to programmers, and can also be a source of irritation. IntelliSense can also be difficult to read and understand as you add more parameters.
Let's look at a bad example of a polyadic method that updates a user's account information:
public void UpdateUserInfo(int id, string username, string firstName, string lastName, string addressLine1, string addressLine2, string addressLine3, string addressLine3, string addressLine4, string city, string postcode, string region, string country, string homePhone, string workPhone, string mobilePhone, string personalEmail, string workEmail, string notes)
{
// ### implementation omitted ###
}
As shown by the UpdateUserInfo method, the code is horrible to read. How can we modify the method so that it transforms from a polyadic method into a monadic method? The answer is simple – we pass in a UserInfo object. First of all, before we modify the method, let's take a look at our UserInfo class:
public class UserInfo
{
public int Id { get;set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string AddressLine3 { get; set; }
public string AddressLine4 { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string Country { get; set; }
public string HomePhone { get; set; }
public string WorkPhone { get; set; }
public string MobilePhone { get; set; }
public string PersonalEmail { get; set; }
public string WorkEmail { get; set; }
public string Notes { get; set; }
}
We now have a class that contains all the information we need to pass into the UpdateUserInfo method. The UpdateUserInfo method can now be transformed from a polyadic method into a monadic method, as follows:
public void UpdateUserInfo(UserInfo userInfo)
{
// ### implementation omitted ###
}
How much better does the preceding code look? It is smaller and much more readable. The rule of thumb should be to have less than three parameters, and ideally none. If your class is obeying the SRP, then consider implementing the parameter object pattern, as we have done here.
Implementing SRP
All objects and methods that you write should, at most, have one responsibility and no more. Objects can have multiple methods, but those methods, when combined, should all work toward the single purpose of the object they belong to. Methods can call multiple methods, where each does different things. But the method itself should only do one thing.
A method that knows and does far too much is known as a God method. And likewise, an object that knows and does too much is known as a God object. God objects and methods are hard to read, maintain, and debug. Such objects and methods can often have the same bug repeated many times. People who are good at the programming craft will avoid God objects and God methods. Let's look at a method that is doing more than one thing:
public void SrpBrokenMethod(string folder, string filename, string text, emailFrom, password, emailTo, subject, message, mediaType)
{
var file = $"{folder}{filename}";
File.WriteAllText(file, text);
MailMessage message = new MailMessage();
SmtpClient smtp = new SmtpClient();
message.From = new MailAddress(emailFrom);
message.To.Add(new MailAddress(emailTo));
message.Subject = subject;
message.IsBodyHtml = true;
message.Body = message;
Attachment emailAttachment = new Attachment(file);
emailAttachment.ContentDisposition.Inline = false;
emailAttachment.ContentDisposition.DispositionType =
DispositionTypeNames.Attachment;
emailAttachment.ContentType.MediaType = mediaType;
emailAttachment.ContentType.Name = Path.GetFileName(filename);
message.Attachments.Add(emailAttachment);
smtp.Port = 587;
smtp.Host = "smtp.gmail.com";
smtp.EnableSsl = true;
smtp.UseDefaultCredentials = false;
smtp.Credentials = new NetworkCredential(emailFrom, password);
smtp.DeliveryMethod = SmtpDeliveryMethod.Network;
smtp.Send(message);
}
SrpBrokenMethod is clearly doing more than one thing, so it breaks the SRP. We will now break this method down into a number of smaller methods that only do one thing. We will also address the issue of the polyadic nature of the method in that it has more than two parameters.
Before we begin to break down the method into smaller methods that do only one thing, we need to look at all the actions that the method is performing. The method starts by writing text to a file. It then creates an email message, assigns an attachment, and finally sends the email. So, for this, we need methods for the following:
- Write text to file
- Create an email message
- Add an email attachment
- Send email
Looking at the current method, we have four parameters that are passed into it for writing text to a file: one for the folder, one for the filename, one for the text, and one for the media type. The folder and filename can be combined into a single parameter called filename. If filename and folder are two separate variables that are used inside the calling code, then they can be passed into the method as a single interpolated string, such as $"{folder}{filename}".
As for the media type, this could be privately set inside a struct during construction time. We could use that struct to set the properties we need so that we can pass the struct in with the three properties as a single parameter. Let's look at the code that accomplishes this:
public struct TextFileData
{
public string FileName { get; private set; }
public string Text { get; private set; }
public MimeType MimeType { get; }
public TextFileData(string filename, string text)
{
Text = text;
MimeType = MimeType.TextPlain;
FileName = $"{filename}-{GetFileTimestamp()}";
}
public void SaveTextFile()
{
File.WriteAllText(FileName, Text);
}
private static string GetFileTimestamp()
{
var year = DateTime.Now.Year;
var month = DateTime.Now.Month;
var day = DateTime.Now.Day;
var hour = DateTime.Now.Hour;
var minutes = DateTime.Now.Minute;
var seconds = DateTime.Now.Second;
var milliseconds = DateTime.Now.Millisecond;
return $"{year}{month}{day}@{hour}{minutes}{seconds}{milliseconds}";
}
}
The TextFileData constructor ensures that the FileName value is unique by calling the GetFileTimestamp() method and appending it to the end of FileName. To save the text file, we call the SaveTextFile()method. Notice that MimeType is set internally and is set to MimeType.TextPlain. We could have simply hardcoded MimeType as MimeType = "text/plain";, but the advantage of using an enum is that the code is reusable, with the added benefit of you not having to remember the text for a specific MimeType or look it up on the internet. Now, we'll code enum and add a description to the enum value:
[Flags]
public enum MimeType
{
[Description("text/plain")]
TextPlain
}
Well, we've got our enum, but now we need a way to extract the description so that it can be easily assigned to a variable. Therefore, we will create an extension class that will enable us to get the description of an enum. This enables us to set MimeType, as follows:
MimeType = MimeType.TextPlain;
Without the extension method, the value of MimeType would be 0. But with the extension method, the value of MimeType is "text/plain". You can now reuse this extension in other projects and build it up as you require.
The next class we will write is the Smtp class, whose responsibility is to send an email via the Smtp protocol:
public class Smtp
{
private readonly SmtpClient _smtp;
public Smtp(Credential credential)
{
_smtp = new SmtpClient
{
Port = 587,
Host = "smtp.gmail.com",
EnableSsl = true,
UseDefaultCredentials = false,
Credentials = new NetworkCredential(
credential.EmailAddress, credential.Password),
DeliveryMethod = SmtpDeliveryMethod.Network
};
}
public void SendMessage(MailMessage mailMessage)
{
_smtp.Send(mailMessage);
}
}
The Smtp class has a constructor that takes a single parameter of the Credential type. This credential is used to log into the email server. The server is configured in the constructor. When the SendMessage(MailMessage mailMessage) method is called, the message is sent.
Let's write a DemoWorker class that splits the work into different methods:
public class DemoWorker
{
TextFileData _textFileData;
public void DoWork()
{
SaveTextFile();
SendEmail();
}
public void SendEmail()
{
Smtp smtp = new Smtp(new Credential("fakegmail@gmail.com",
"fakeP@55w0rd"));
smtp.SendMessage(GetMailMessage());
}
private MailMessage GetMailMessage()
{
var msg = new MailMessage();
msg.From = new MailAddress("fakegmail@gmail.com");
msg.To.Add(new MailAddress("fakehotmail@hotmail.com"));
msg.Subject = "Some subject";
msg.IsBodyHtml = true;
msg.Body = "Hello World!";
msg.Attachments.Add(GetAttachment());
return msg;
}
private Attachment GetAttachment()
{
var attachment = new Attachment(_textFileData.FileName);
attachment.ContentDisposition.Inline = false;
attachment.ContentDisposition.DispositionType =
DispositionTypeNames.Attachment;
attachment.ContentType.MediaType =
MimeType.TextPlain.Description();
attachment.ContentType.Name =
Path.GetFileName(_textFileData.FileName);
return attachment;
}
private void SaveTextFile()
{
_textFileData = new TextFileData(
$"{Environment.SpecialFolder.MyDocuments}attachment",
"Here is some demo text!"
);
_textFileData.SaveTextFile();
}
}
The DemoWorker class shows a much cleaner version of sending an email message. The main method responsible for saving an attachment and sending it as an attachment via email is called DoWork(). This method only contains two lines of code. The first line calls the SaveTextFile() method, while the second line calls the SendEmail() method.
The SaveTextFile() method creates a new TextFileData struct and passes in the filename and some text. It then calls the SaveTextFile() method in the TextFileData struct, which is responsible for saving the text to the file specified.
The SendEmail() method creates a new Smtp class. The Smtp class has a Credential parameter, while the Credential class has two string parameters for email address and password. The email and password are used to log into the SMTP server. Once the SMTP server has been created, the SendMessage(MailMessage mailMessage) method is called.
This method requires a MailMessage object to be passed in. So, we have a method called GetMailMethod() that builds a MailMessage object that is then passed into the SendMessage(MailMessage mailMessage) method. GetMailMethod() adds an attachment to MailMessage by calling the GetAttachment() method.
As you can see from these modifications, our code is now more compact and readable. That is the key to good quality code that is easy to modify and maintain: it must be easy to read and understand. That is why it is important for your methods to be small and clean with as few parameters as possible.
Does your method break the SRP? If it does, you should consider breaking the method up into as many methods as there are responsibilities. And that concludes this chapter on writing clean functions. It is now time to summarize what you have learned and test your knowledge.