Software Design and Architecture Roadmap
Topics
- Clean Code
- Programming Paradigms
Resources:
- How to Learn Software Design and Architecture | The Full-stack Software Design & Architecture Map
- Software Design and Architecture Roadmap
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Refactoring, by Martin Fowler (2nd edition)
- The Pragmatic Programmer, by Andy Hunt and Dave Thomas
- The Design of Everyday Things, by Don Norman
(1) CSharp Fundamentals
Topics
- Variables and data types
- Control structures:
if
,switch
,for
,while
- Methods and functions
- Classes and objects
- Abstract classes
- Records
- Interfaces
- Generic
- Anonymous Types
Resources:
(2) Object-Oriented Programming (OOP) in C#
Topics
- Encapsulation, inheritance, polymorphism and abstraction
- SOLID principles
- Design patterns (Factory, Singleton, Repository)
- Delegates and events
- Custom exceptions
- Nullable reference types
- Collections and LINQ
Resources
- Paper “Design Principles and Design Patterns” by Robert C. Martin
- Book: “Design Patterns in C#” by Vaskaran Sarcar
- Design Patterns in C# – Refactoring.guru
- C# Clean Code: SOLID Principles – Dev.to
- Object-oriented programming
- Apress/design-patterns-csharp-2e – Github.com
(3) Dotnet and ASP Dotnet Core
Topics
- Lifecycle of a .NET application
- Project structure (
Program.cs
,Startup.cs
, orbuilder
) - Routing and controllers (
Controller
,Route
,HttpGet/Post/etc.
) - Dependency Injection
- Middleware
Resources
(4) Data Access with Entity Framework Core
Topics
- Defining models and relationships
- DbContext and migrations
- CRUD with LINQ and EF
- Fluent API vs Data Annotations
Resources
(5) REST APIs with ASP.NET Core
Topics
- Creating endpoints using
ApiController
- Model validation (
DataAnnotations
) - Filters (
ActionFilter
,ExceptionFilter
) - Swagger (API documentation)
Resources
(6) Security and Authentication
Topics
- JWT Authentication and Authorization
- Identity Framework
- Policies and roles
Resources
- Secure ASP.NET Core with JWT
- Course (YouTube): JWT Authentication in ASP.NET Core
(7) Testing
Topics
- xUnit, NUnit
- Unit testing and integration testing
- Mocking with Moq
Resources
Other Resources
Resources
Getting Started
What’s CSharp?
C# is a high-level, general-purpose programming language developed by Microsoft as part of the .NET framework. It’s an object-oriented language, meaning it uses objects to structure code and data, and is used to build a variety of applications. C# is known for its ease of learning, strong community support, and ability to produce highly performant code.
.NET SDK Instalation
The easiest way to have the .NET SDK installed in your personal computer is to download Visual Studio. You can also use other IDE, but you will need to install the SDK manually.
Hello World
A sample C# program is show here.
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
Run the program as below:
$ dotnet run Program.cs
Variables
Normal Declaration:
string firstName = "Someone";
char userOption = 'A';
int gameScore = 123;
float percentage = 12.10;
double portion = 4.556
decimal particlesPerMillion = 123.4567;
bool processedCustomer = true;
Implicitly Typed:
var message = "Hello world!";
Constant
const int ConstNum = 5;
Content
- Use Intention-Revealing Names
- Avoid Disinformation
- Make Meaningful Distinctions
- Use Pronounceable Names
- Use Searchable Names
- Avoid Encodings
- Avoid Mental Mapping
- Class Names
- Method Names
- Don’t Be Cute
- Pick One Word per Concept
- Don’t Pun
- Use Solution Domain Names
- Use Problem Domain Names
- Don’t Add Gratuitous Context
- Resources
Use Intention-Revealing Names
The name of a variable, function, or class, should answer all the big questions. It should tell you why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent.
Example 1
Let’s say we want to define a variable to store elapsed time in days.
// No question is answered.
int d; // elapsed time in days
// The name has now more sense.
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
Example 2
Let’s try to analyze what’s the purpose of this code:
List<int[]> theList = new();
public List<int[]> GetThem() {
List<int[]> list1 = new();
foreach (int[] x in theList)
{
if (x[0] == 4)
{
list1.Add(x);
}
}
return list1;
}
There are some questions we need to answer, so we can understand the code:
- What kinds of things are in
theList
? - What is the significance of the zeroth subscript of an item in
theList
? - What is the significance of the value
4
? - How would I use the list being returned?
Let’s say we have the following context:
- We’re working in a mine sweeper game.
- We find that the board is a list of cells called
theList
. Let’s rename that togameBoard
. - Each cell on the board is represented by a simple array.
- We further find that the zeroth subscript is the location of a
status
value and that a status value of4
meansflagged
.
Just by giving these concepts names we can improve the code considerably:
List<int[]> gameBoard = new();
const int StatusValue = 0;
const int Flagged = 4;
public List<int[]> GetFlaggedCells() {
List<int[]> flaggedCells = new();
foreach (int[] cell in gameBoard)
{
if (cell[StatusValue] == Flagged)
{
flaggedCells.Add(cell);
}
}
return flaggedCells;
}
The simplicity of the code has not changed, but the code has become much more explicit.
There’s a chance to make it even clearer by creating a class for cells instead of using array of int
:
List<int[]> gameBoard = new();
public class Cell
{
private int Status { get; set; }
public bool IsFlagged()
{
... // The logic to handle each status goes here.
}
}
public List<Cell> GetFlaggedCells() {
List<Cell> flaggedCells = new();
foreach (Cell cell in gameBoard)
{
if (cell.IsFlagged())
{
flaggedCells.Add(cell);
}
}
return flaggedCells;
}
Avoid Disinformation
Programmers must avoid leaving false clues that obscure the meaning of code.
Example 1
Do not use abbreviation.
int hp;
string aix;
bool sco;
Example 2
Do not refer to a grouping of accounts as an accountList
unless it’s actually a List.
Dictionary<Account> accountList = new(); // If the container holding the accounts is not actually a List, it may lead to false conclusions.
// A better way to name the variable would be:
Dictionary<Account> accountGroup = new();
Dictionary<Account> bunchOfAccounts = new();
Dictionary<Account> accounts = new();
Example 3
How long does it take to spot the subtle difference between the following variables:
string XYZControllerForEfficientHandlingOfStrings;
string XYZControllerForEfficientStorageOfStrings;
Spelling similar concepts similarly is information. Using inconsistent spellings is disinformation. It is very helpful if names for very similar things sort together alphabetically and if the differences are very obvious
Example 4
A truly awful example of disinformative names would be the use of lower-case L or uppercase O as variable names, especially in combination.
int a = l;
if ( O == l )
{
a = O1;
}
else
{
l = 01;
}
The problem, of course, is that they look almost entirely like the constants one and zero, respectively.
Make Meaningful Distinctions
If names must be different, then they should also mean something different.
Example 1
Number-series naming (a1, a2, .. aN) is the opposite of intentional naming.
// a1 and a2 variables doesn't have any meaningful distinction.
public static void CopyChars(char[] a1, char[] a2) {
for (int i = 0; i < a1.Length; i++) {
a2[i] = a1[i];
}
}
// If we call them source and destination, they mean something different.
public static void CopyChars(char[] source, char[] destination) {
for (int i = 0; i < source.Length; i++) {
destination[i] = source[i];
}
}
Example 2
Noise words are another meaningless distinction.
public class Product {
...
}
public class ProductInfo { // This class name doesnt have a different meaning than Product class
...
}
public class ProductData { // This class name doesnt have a different meaning than Product class
...
}
Example 3
Some other recommendations.
// Wrong use of some words.
string NameVariable;
string NameString;
string[] UsersTable;
Customer customerObject = new();
CustomerObject customer = new();
// Avoid using type words.
string Name;
string[] Users;
Customer customer = new();
Use Pronounceable Names
Make your names pronounceable. If you can’t pronounce it, you can’t discuss it without sounding like an idiot.
Example
// Wrong use pronounceable names.
class DtaRcrd102 {
private DateTime Genymdhms;
private DateTime Modymdhms;
private String Pszqint = "102";
...
};
// Meaninful names and easy to pronounce.
class Customer {
private DateTime GenerationTimestamp;
private DateTime ModificationTimestamp;
private String RecordId = "102";
};
Use Searchable Names
If a variable or constant might be seen or used in multiple places in a body of code, it is imperative to give it a search-friendly name.
Example
// Short variable names which are not useful for searching.
int s = 0;
int t[] = [1, 2, 3, 4, 5, 6, ...];
for (int j=0; j<34; j++) {
s += (t[j]*4)/5;
}
// Meaninful names and are useful for searching.
int realDaysPerIdealDay = 4;
int realdays = 3;
const int WORK_DAYS_PER_WEEK = 5;
const int NUMBER_OF_TASKS = 34;
int sum = 0;
int taskEstimate[] = [1, 2, 3, 4, 5, 6, ...];
for (int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
Avoid Encodings
Encoding type or scope information into names simply adds an extra burden of deciphering.
Hungarian Notation
Hungarian Notation is a naming convention in programming where variable and function names include information about their type, purpose, or intended use.
// Given we have a phone number variable in string type, we declare the following:
string phoneString;
// We then change the type to Phone Number:
PhoneNumber phoneString;
// The name will not changed when type changed!
Member Prefixes
You also don’t need to prefix member variables with m_ anymore.
// Using a short name for description. You need to have a comment explaining the context of the variable.
public class Part {
private string m_dsc; // The textual description
void SetName(String name) {
m_dsc = name;
}
}
// Using a meaninful name for description. It's not required any extra comment to explain what does the variable is used to.
public class Part {
string Description;
void SetDescription(String description) {
this.description = description;
}
}
Interfaces and Implementations
Interfaces and implementations will have the same name, so we need to somehow difference them.
public interface IAnimal { ... }
public class Animal : IAnimal { ... }
Avoid Mental Mapping
Readers shouldn’t have to mentally translate your names into other names they already know. This problem generally arises from a choice to use neither problem domain terms nor solution domain terms.
string[] groups = [ .... ];
string[] people = [ .... ];
// Using traditional i and j variables, so we need to have a mental mapping that i is iterating groups and j is iterating people.
for (int i = 0; i < groups.Length; i++) {
for (int j = 0; j < people.Length; j++) {
...
}
}
// If we use meaninful names for groupId and personId, then we don't need to have a mental mapping.
for (int groupId = 0; groupId < groups.Length; groupId++) {
for (int personId = 0; personId < people.Length; personId++) {
...
}
}
Class Names
// Classes and objects should have noun or noun phrase names.
public class Customer { ... }
public class WikiPage { ... }
public class Account { ... }
public class AddressParser { ... }
// Avoid words like Manager, Processor, Data, or Info in the name of a class.
public class CustomerData { ... }
public class CustomerInfo { ... }
// A class name should not be a verb.
public class Eating { ... }
Method Names
Methods should have verb or verb phrase names.
public void PostPayment() { ... }
public void DeletePage() { ... }
public void Save() { ... }
public void IsPosted() { ... }
Don't Be Cute
If names are too clever, they will be memorable only to people who share the author’s sense of humor, and only as long as these people remember the joke.
// Cute but not meaninful.
public void HolyHandGrenade() { ... }
// Meaninful named method.
public void DeleteItems() { ... }
Pick One Word per Concept
Pick one word for one abstract concept and stick with it.
// Using different names for the same purpose.
public class UserService {
public List<User> Fetch() { ... }
}
public class GroupService {
public List<Group> Retrieve() { ... }
}
public class TaskService {
public List<Task> Get() { ... }
}
// Using one same name for the same purpose.
public class UserService {
public List<User> Get() { ... }
}
public class GroupService {
public List<Group> Get() { ... }
}
public class TaskService {
public List<Task> Get() { ... }
}
Don't Pun
Avoid using the same word for two purposes. Using the same term for two different ideas is essentially a pun.
// Using the same name for diffent purposes.
public class UserService {
public void Add(User user) { ... } // This method let us create a new user.
}
public class GroupService {
public void Add(Group group, User user) { ... } // This method let us insert a user into a group.
}
// Using the appropriate name for each purpose.
public class UserService {
public void Create(User user) { ... } // This method let us create a new user.
}
public class GroupService {
public void Insert(Group group, User user) { ... } // This method let us insert a user into a group.
}
Use Solution Domain Names
Remember that the people who read your code will be programmers. So go ahead and use computer science (CS) terms, algorithm names, pattern names, math terms, and so forth.
- The name
AccountVisitor
means a great deal to a programmer who is familiar with the VISITOR pattern. - What programmer would not know what a
JobQueue
was?
There are lots of very technical things that programmers have to do. Choosing technical names for those things is usually the most appropriate course.
Use Problem Domain Names
If there are no programming terms, we can use terms related to the feature or business.
Example
public class Account {
private string AccountBalance; // This variable name has no programming terms, so it's related to the bussiness.
}
Separating solution and problem domain concepts is part of the job of a good programmer and designer. The code that has more to do with problem domain concepts should have names drawn from the problem domain.
Add Meaningful Context
There are a few names which are meaningful in and of themselves—most are not. Instead, you need to place names in context for your reader by enclosing them in well-named classes, methods, functions, etc.
// We have several fields that doesn't have a clear context
public class User {
public string FirstName { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
public int HouseNumber { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zipcode { get; set; }
}
User user = new();
// In this context, we only know the State property is part of the user.
Console.WriteLine(user.State);
// In this context, we only know the State propertyis part of the user's address
Console.WriteLine(user.Street);
Console.WriteLine(user.HouseNumber);
Console.WriteLine(user.City);
Console.WriteLine(user.State);
//
public class Address {
public string Street { get; set; }
public int HouseNumber { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zipcode { get; set; }
}
public class User {
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
User user = new();
// In this context, we do know the State property is part of the user's address.
Console.WriteLine(user.Address.State);
Don't Add Gratuitous Context
In an imaginary application called “Gas Station Deluxe,” it is a bad idea to prefix every class with GSD. Frankly, you are working against your tools. You type G and press the completion key and are rewarded with a mile-long list of every class in the system. Is that wise? Why make it hard for the IDE to help you?
// Likewise, say you invented a "MailingAddress" class in GSD's accounting module, and you named it "GSDAccountAddress"
public class GSDAccountAddress { ... }
// Later, you need a mailing address for your customer contact application. Do you use "GSDAccountCustomer"? Does it sound like the right name? Some characters are redundant or irrelevant.
public class GSDAccountCustomer { ... }
// The names "accountAddress" and "customerAddress" are fine names for instances of the class "Address" but could be poor names for classes.
public class Address { ... }
Address accountAddress = new();
Address customerAddress = new();
Shorter names are generally better than longer ones, so long as they are clear. Add no more context to a name than is necessary.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Identifier name - rules and conventions - C# | Microsoft Learn
- .NET Coding Conventions - C# | Microsoft Learn
- Clean Code in C# Part 1 Meaningful Names - DEV Community
Data Types
There are two different data types in C#:
- Value Types: Directly store the data. Once you assign a value, it holds that data
int
,char
,float
are just a few examples.
- Reference Types: Store a memory address. They point to the address of the value.
string
,class
,array
are commonly used.
Integer
Math operations:
int sum = 7 + 5;
int difference = 7 - 5;
int product = 7 * 5;
int quotient = 7 / 5;
int modulus = 7 % 5;
Console.WriteLine("Sum: " + sum); // Sum: 12
Console.WriteLine("Difference: " + difference); // Difference: 2
Console.WriteLine("Product: " + product); // Product: 35
Console.WriteLine("Quotient: " + quotient); // Quotient: 1
Console.WriteLine($"Modulus: {7 % 5}"); // Modulus: 2
Order of operations
In math, PEMDAS is an acronym that helps students remember the order of operations. The order is:
- Parentheses (whatever is inside the parenthesis is performed first)
- Exponents
- Multiplication and Division (from left to right)
- Addition and Subtraction (from left to right)
Increment and decrement
int value = 1;
value = value + 1;
Console.WriteLine("First increment: " + value); // First increment: 2
value += 1;
Console.WriteLine("Second increment: " + value); // Second increment: 3
value++;
Console.WriteLine("Third increment: " + value); // Third increment: 4
value = value - 1;
Console.WriteLine("First decrement: " + value); // First decrement: 3
value -= 1;
Console.WriteLine("Second decrement: " + value); // Second decrement: 2
value--;
Console.WriteLine("Third decrement: " + value); // Third decrement: 1
String
Combine String using character escape sequences:
// Character escape sequences
Console.WriteLine("Hello\nWorld!");
Console.WriteLine("Hello\tWorld!");
Console.WriteLine("Hello \"World\"!"); // Hello "World"!
Console.WriteLine("c:\\source\\repos"); // c:\source\repos
// Verbatim string literal
Console.WriteLine(@" c:\source\repos
(this is where your code goes)");
// c:\source\repos
// (this is where your code goes)
// Unicode escape character
Console.WriteLine("\u3053\u3093\u306B\u3061\u306F World!"); // こんにちは World!
Combine String using string concatenation:
string firstName = "Bob";
string greeting = "Hello";
string message = greeting + " " + firstName + "!";
Console.WriteLine(message); // Hello Bob!
Combine String using string interpolation:
string firstName = "Bob";
string greeting = "Hello";
Console.WriteLine($"{greeting} {firstName}!"); // Hello Bob!
// Combine verbatim literals and string interpolation
string projectName = "First-Project";
Console.WriteLine($@"C:\Output\{projectName}\Data"); // C:\Output\First-Project\Data
Array
Declaration:
string[] customerIds = new string[3];
string[] customerIds = [ "A123", "B456", "C789" ]; // Introduced in C#12
string[] customerIds = { "A123", "B456", "C789" }; // Older version
Assigning values:
string[] customerIds = new string[3];
customerIds[0] = "C123";
customerIds[1] = "C456";
customerIds[2] = "C789";
Size of the array:
string[] customerIds = [ "A123", "B456", "C789" ];
Console.WriteLine($"There are {customerIds.Length} customers.");
Tuples
Declaration:
var pt = (X: 1, Y: 2);
var slope = (double)pt.Y / (double)pt.X;
Console.WriteLine($"A line from the origin to the point {pt} has a slope of {slope}."); // A line from the origin to the point (1, 2) has a slope of 2.
Content
- Small
- Do One Thing
- One Level of Abstraction per Function
- Switch Statements
- Use Descriptive Names
- Function Arguments
- Have No Side Effects
- Command Query Separation
- Prefer Exceptions to Returning Error Codes
- Don’t Repeat Yourself
- Structured Programming
- Resources
Rules of functions
It’s importance to create small, focused, and single-responsibility functions that are easy to understand, test, and maintain.
Small
- The first rule of functions is that they should be small. They should hardly ever be 20 lines long.
- This implies that the blocks within
if
statements,else
statements,while
statements, and so on should be one line long. Probably that line should be a function call. - This also implies that functions should not be large enough to hold nested structures. Therefore, the indent level of a function should not be greater than one or two.
Do One Thing
- Functions should do one thing, should do it well, and should do it only.
- Functions should not be able to be divided into more sections/functions.
One Level of Abstraction per Function
- To make sure the functions are doing “one thing”, we need to make sure that the statements within the function are all at the same level of abstraction.
- We can follow the “The Stepdown Rule”. It’s all about reading code from top to bottom. This way each function introduces the next and make the code readable from top to bottom.
Switch Statements
- It’s hard to make a small
switch
statement. - It violates the Single Responsibility Principle (SRP) because there is more than one reason for it to change.
- It violates the Open Closed Principle8 (OCP) because it must change whenever new cases are added.
- They can be tolerated if they appear only once, are used to create polymorphic objects, and are hidden behind an inheritance
Use Descriptive Names
- Function name should describes what the function does.
- The smaller and more focused a function is, the easier it is to choose a descriptive name.
- Don’t be afraid to make a name long. A long descriptive name is better than a short enigmatic name.
Function Arguments
- The ideal number of arguments for a function is zero.
- Three arguments should be avoided where possible.
- Arguments should make sense with the function name. There are two very common reasons to pass a single argument into a function.
- You may be asking a question about that argument.
- Or you may be operating on that argument, transforming it into something else and returning it.
- Avoid using Flag Arguments.
- Use object arguments.
// In this case, the first two arguments seem to be related.
Circle MakeCircle(double x, double y, double radius);
// We can abstract x and y into Point class, and have one instance instead.
Circle MakeCircle(Point center, double radius);
- Function and arguments should form a very nice verb/noun pair.
Have No Side Effects
- Your function promises to do one thing, so it should not do other hidden things.
Command Query Separation
- Functions should either do something or answer something, but not both.
Prefer Exceptions to Returning Error Codes
- Use try/catch to handle error instead of returning error codes as exceptions can be added without forcing any recompilation or redeployment.
// Returning error codes from specific actions.
if (DeletePage(page) == StatusOk)
{
if (registry.DeleteReference(page.name) == StatusOk)
{
if (configKeys.DeleteKey(page.name.makeKey()) == StatusOk)
{
Console.WriteLine("page deleted");
}
else
{
Console.WriteLine("configKey not deleted");
}
}
else
{
Console.WriteLine("deleteReference from registry failed");
}
}
else
{
Console.WriteLine("delete failed");
return E_ERROR;
}
// We can replace the nested if statements with using try/catch.
try {
DeletePage(page);
registry.DeleteReference(page.name);
configKeys.DeleteKey(page.name.makeKey());
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
- Functions should do one thing. Error handing is one thing.
Don’t Repeat Yourself
- In case you find some code that is being used, try to separate it into a reusable function.
Structured Programming
- Edsger Dijkstra’s rule says “every function and every block within a function should have one entry and one exit”.
- Following these rules means that there should only be one return statement in a function, no
break
orcontinue
statements in a loop, and never, ever, anygoto
statements. - This rule provides significant benefit in large functions only.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Clean Code in C# Part 2 Methods - DEV Community
Content
- Rules of comments
- Good comments
- Bad comments
- Resources
Rules of comments
Truth can only be found in one place: the code. Only the code can truly tell you what it does. It is the only source of truly accurate information. Therefore, though comments are sometimes necessary, we will expend significant energy to minimize them.
One of the more common motivations for writing comments is bad code.
Clear and expressive code with few comments is far superior to cluttered and complex code with lots of comments.
// Using comments to explain the code:
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) { ... }
// Use the code to explain the behavior:
if (employee.isEligibleForFullBenefits()) { ... }
Good comments
Some comments are necessary or beneficial.
Legal Comments
- Copyright and authorship statements are necessary and reasonable things to put into a comment at the start of each source file.
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.
Informative Comments
- It is sometimes useful to provide basic information with a comment. For example, adding a comment to explain return value of an abstract method.
// Returns an instance of the Responder being tested.
protected abstract Responder responderInstance();
Explanation of Intent
- Sometimes a comment goes beyond just useful information about the implementation and provides the intent behind a decision.
Example
- When comparing two objects, the author decided that he wanted to sort objects of his class higher than objects of any other.
public int compareTo(Object o)
{
...
return 1; // we are greater because we are the right type.
}
Clarification
- Sometimes it is just helpful to translate the meaning of some obscure argument or return value into something that’s readable.
- Comments can be useful when the code is part of the standard library, or in code that you cannot.
public string CompareStrings( string str1, string str2 )
{
// Compare the values, using the CompareTo method on the first string.
int cmpVal = str1.CompareTo(str2);
if (cmpVal == 0)
{
// str1 == str2
return 0;
}
else if (cmpVal < 0)
{
// str1 < str2
return -1;
}
else
{
// str1 > str2
return 1;
}
}
Warning of Consequences
- Sometimes it is useful to warn other programmers about certain consequences.
// WARNING: This method directly modifies the database.
// Any changes here will be permanent and irreversible.
public void UpdateDatabase(Data data)
{
// Database update logic here
}
TODO Comments
- It is sometimes reasonable to leave “To do” notes in the form of //TODO comments.
- TODO comments let the team know that something needs to be done.
- TODOs are jobs that the programmer thinks should be done, but for some reason can’t do at the moment.
// TODO: Implement error handling for database connection
public void ConnectToDatabase(string connectionString)
{
// ... code to connect to the database ...
}
Amplification
- A comment may be used to amplify the importance of something that may otherwise seem inconsequential.
string personName = " FirstName Lastname ";
string[] fullName = fullname.Trim().Split(' ');
// the trim is real important. It removes the starting and ending spaces that could cause the user instance to have unnecessary spaces in its parts.
User user = new();
user.FirstName = fullName[0];
user.LastName = fullName[1];
Public APIs
- If you are writing a public API, then you should certainly write good comments for it.
- You can use javadocs for Java code or jsdocs for JavaScript.
Bad comments
Most comments fall into this category. Usually they are crutches or excuses for poor code or justifications for insufficient decisions, amounting to little more than the programmer talking to himself.
Some examples
- Mumbling: Plopping in a comment just because you feel you should or because the process requires it, is a hack.
- Redundant Comments: The comment explains the same as you can deduce from function, variables, etc. It probably takes longer to read than the code itself.
- Misleading Comments: You find misinformation from the comment.
- Mandated Comments: It is just plain silly to have a rule that says that every variable must have a comment.
- Journal Comments: Sometimes people add a comment to the start of a module every time they edit it. Something like a changelog.
- Noise Comments: Sometimes you see comments that are nothing but noise. They restate the obvious and provide no new information.
- Position Markers: Sometimes programmers like to mark a particular position in a source file.
- Closing Brace Comments: Sometimes programmers will put special comments on closing braces.
- Attributions and Bylines: Source code control systems are very good at remembering who added what, when. There is no need to pollute the code with little bylines.
- Commented-Out Code: Few practices are as odious as commenting-out code. Don’t do this!
- Too Much Information: Don’t put interesting historical discussions or irrelevant descriptions of details into your comments.
- Inobvious Connection: The connection between a comment and the code it describes should be obvious.
- Function Headers: Short functions don’t need much description. A well-chosen name for a small function that does one thing is usually better than a comment header.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Clean Code in C# Part 3 Comments - DEV Community
Methods
No params:
Console.WriteLine("Generating random numbers:");
DisplayRandomNumbers(); // 17 29 46 36 3
void DisplayRandomNumbers()
{
Random random = new Random();
for (int i = 0; i < 5; i++)
{
Console.Write($"{random.Next(1, 100)} ");
}
Console.WriteLine();
}
Using parameters:
CountTo(5);
void CountTo(int max)
{
for (int i = 0; i < max; i++)
{
Console.Write($"{i}, "); // 0, 1, 2, 3, 4
}
}
Optional parameters:
CountTo();
void CountTo(int max = 10)
{
for (int i = 0; i < max; i++)
{
Console.Write($"{i}, "); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
}
}
Returning values:
int sum = SumTo(5);
Console.Write($"sum: {sum}"); // sum: 15
int CountTo(int max)
{
int result = 0;
for (int i = 1; i <= max; i++)
{
result += i;
}
return result;
}
Stateless
The following code is stateless because it doesn’t require to store any state to work, you just call the static method WriteLine from Console class.
Console.WriteLine("Hello World!");
Stateful
The following code is stateful because it is required to store previous information of the state to calculate next random value.
Random dice = new Random();
int roll = dice.Next(1, 7);
Condition
if-else operator
string color = "black";
if (color == "black")
{
Console.WriteLine("It's black.");
}
else if (color == "white")
{
Console.WriteLine("It's white.");
}
else {
Console.WriteLine("It's other color.");
}
Conditional Operator
int saleAmount = 1001;
int discount = saleAmount > 1000 ? 100 : 50;
Console.WriteLine($"Discount: {discount}");
Scope
bool flag = true;
if (flag)
{
int value = 10;
Console.WriteLine($"Inside the code block: {value}"); // Prints value.
}
Console.WriteLine($"Outside the code block: {value}"); // Gives error because value is declared inside the if code block.
Switch
string fruit = "apple";
switch (fruit)
{
case "apple":
Console.WriteLine($"App will display information for apple.");
break;
case "banana":
Console.WriteLine($"App will display information for banana.");
break;
case "cherry":
Console.WriteLine($"App will display information for cherry.");
break;
default:
Console.WriteLine($"App will not display information about any fruit.");
break;
}
Loop
Foreach
string[] names = { "Rowena", "Robin", "Bao" };
foreach (string name in names)
{
Console.WriteLine(name); // "Rowena", "Robin", "Bao"
}
For
for (int i = 0; i < 10; i++)
{
if (i > 5) {
break;
}
Console.WriteLine(i); // 1, 2, 3, 4, 5
}
Do-While
Random random = new Random();
int current = 0;
do
{
current = random.Next(1, 11);
if (current >= 8) {
continue;
}
Console.WriteLine(current);
} while (current != 7);
While
Random random = new Random();
int current = random.Next(1, 11);
while (current >= 3)
{
Console.WriteLine(current);
current = random.Next(1, 11);
}
Console.WriteLine($"Last number: {current}");
Content
- Rules of formatting
- Vertical Formatting
- Horizontal Formatting
- Resources
Rules of formatting
The Purpose of Formatting
Code formatting is about communication, and communication is the professional developer’s first order of business.
Vertical Formatting
According to Robert C. Martin example, files long preferred to be 200 lines with an upper limit of 500 lines long.
Although this should not be a hard and fast rule, it should be considered very desirable. Small files are usually easier to understand than large files are.
The Newspaper Metaphor
Think of a well-written newspaper article. You read it vertically. At the top you expect a headline that will tell you what the story is about and allows you to decide whether it is something you want to read. The first paragraph gives you a synopsis of the whole story, hiding all the details while giving you the broad-brush concepts. As you continue downward, the details increase until you have all the dates, names, quotes, claims, and other minutia.
Having this example put on code formatting, we can think the following:
- File name should be simple but explanatory. The name, by itself, should be sufficient to tell us whether we are in the right module or not.
- The topmost parts of the source file should provide the high-level concepts and algorithms.
- Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file.
Vertical Openness Between Concepts
Nearly all code is read left to right and top to bottom. Each line represents an expression or a clause, and each group of lines represents a complete thought. Those thoughts should be separated from each other with blank lines.
Vertical Density
If openness separates concepts, then vertical density implies close association. So lines of code that are tightly related should appear vertically dense. For example, in a class, each property will follow the previous one, then we place a blank line to separate properties from methods.
Vertical Distance
Concepts that are closely related should be kept vertically close to each other. Clearly this rule doesn’t work for concepts that belong in separate files. But then closely related concepts should not be separated into different files unless you have a very good reason. Indeed, this is one of the reasons that protected variables should be avoided.
- Variable Declarations: Variables should be declared as close to their usage as possible.
- Instance variables: They should be declared at the top of the class. This should not increase the vertical distance of these variables.
- Dependent Functions: If one function calls another, they should be vertically close, and the caller should be above the callee, if at all possible. Constants should be kept at an appropriate level; therefore, we pass them from the place where it makes sense to know it to the low-level function where it is actually used.
- Conceptual Affinity: The stronger the affinity the less vertical distance there should be between them. This affinity could be direct dependence “One function calling another, or A group of functions perform similar operation”, or it could be conceptual affinity “Share a common naming scheme or Perform variation of the same basic task”.
Vertical Ordering
In general we want function call dependencies to point in the downward direction. That is, a function that is called should be below a function that does the calling. This creates a nice flow down the source code module from high level to low level.
Horizontal Formatting
Programmers clearly prefer short lines. The old Hollerith limit of 80 is a bit arbitrary, and there should not be a problem to lines edging out to 100 or even 120. But beyond that is probably just careless.
Horizontal Openness and Density
Use horizontal white space to associate things that are strongly related and disassociate things that are weakly related.
- Assignment operators surrounded with white space.
string name = "Fernando";
- No space between function name and the opening parenthesis while placing space between the closing parenthesis and the curly brackets:
GetFullName() { ... }
- Accentuate the precedence of operators.
CalculateTotal()
{
// The factors have no white space between them because they are high precedence.
// The terms are separated by white space because addition and subtraction are lower precedence.
return 4*4 + 3*a*c;
}
Indentation
A source file is a hierarchy rather like an outline. Each level of this hierarchy is a scope into which names can be declared and in which declarations and executable statements are interpreted.
To make this hierarchy of scopes visible, we indent the lines of source code in proportion to their position in the hiearchy.
- Statements at the level of the file, such as most class declarations, are not indented at all.
- Methods within a class are indented one level to the right of the class.
- Implementations of those methods are implemented one level to the right of the method declaration.
- Block implementations are implemented one level to the right of their containing block
- And so on … but we never ever break indentation.
Resources
- Clean Code: A Handbook of Agile Software Craftsmanship, by Robert C. Martin
- Clean Code in C# Part 4 Formatting - DEV Community