Declaration
var v = new { Amount = 108, Message = "Hello" };
// Rest the mouse pointer over v.Amount and v.Message in the following
// statement to verify that their inferred types are int and string.
Console.WriteLine(v.Amount + v.Message);
Principles
C# is an object-oriented programming language. The four basic principles of object-oriented programming are:
- Encapsulation: Hiding the internal state and functionality of an object and only allowing access through a public set of functions.
- Inheritance: Ability to create new abstractions based on existing abstractions.
- Polymorphism: Ability to implement inherited properties or methods in different ways across multiple abstractions.
- Abstraction: Modeling the relevant attributes and interactions of entities as classes to define an abstract representation of a system.
Encapsulation
Definition
Encapsulation is sometimes referred to as the first pillar or principle of object-oriented programming. A class or struct can specify how accessible each of its members is to code outside of the class or struct. Members not intended for consumers outside of the class or assembly are hidden to limit the potential for coding errors or malicious exploits.
Members
The following list includes all the various kinds of members that can be declared in a class, struct, or record.
- Fields
- Constants
- Properties
- Methods
- Constructors
- Events
- Finalizers
- Indexers
- Operators
- Nested Types
Accessibility
Some methods and properties are meant to be called or accessed from code outside a class or struct, known as client code. Other methods and properties might be only for use in the class or struct itself. It’s important to limit the accessibility of your code so that only the intended client code can reach it. You specify how accessible your types and their members are to client code by using the following access modifiers:
public
: Access is not restricted.protected
: Access is limited to the containing class or types derived from the containing class.internal
: Access is limited to the current assembly.protected internal
: Access is limited to the current assembly or types derived from the containing class.private
: Access is limited to the containing type.private protected
: Access is limited to the containing class or types derived from the containing class within the current assembly.
// Assembly1.cs
public class OtherClass
{
internal static int intM = 0;
}
// Assembly2.cs
using System;
class ParentClass
{
public int a = 10; // No access restrictions.
protected int b = 20; // Access from the ParentClass and any other class that inherits this one.
internal int c = 30; // Access only on the same assembly.
private int d = 40; // Access only on the same class.
public int GetB()
{
return b; // OK
}
public int GetD()
{
return d; // OK
}
}
class ChildClass : ParentClass
{
public void ChangeB(int newB)
{
b = newB; // OK
}
}
class Program {
static void Main()
{
var parent = new ParentClass();
var child = new ChildClass();
// var other = new OtherClass(); OK
// public
parent.a = 20; // OK
// protected
// parent.b ERROR
// child.b ERROR
// internal
parent.c = 20; // OK
child.c = 20; // OK
// other.intM = 20; ERROR
// private
// parent.d ERROR
// child.d ERROR
}
}
Inheritance
Classes (but not structs) support the concept of inheritance. A class that derives from another class, called the base class, automatically contains all the public, protected, and internal members of the base class except its constructors and finalizers.
// Base class
public class Animal
{
public virtual string Name { get; set; }
public virtual void Eat() // virtual keyword allows to be overridden in a derived class.
{
Console.WriteLine("Animal is eating.");
}
}
// Derived class
public class Dog : Animal
{
private string _name;
public string Breed { get; set; }
public override string Name
{
get
{
return _name;
}
set
{
if (!string.IsNullOrEmpty(value))
{
_name = value;
}
else
{
_name = "Unknown";
}
}
}
public void Bark()
{
Console.WriteLine("Dog is barking.");
}
public override void Eat() // Because the Eat method is virtual, in a derived class it's possible to use override method to change the body.
{
Console.WriteLine("Dog is eating."); // Overriding the Eat method
}
}
Inheritance from abstract class
abstract class Shape
{
public abstract int GetArea();
}
class Square : Shape
{
private int _side;
public Square(int n) => _side = n;
// GetArea method is required to avoid a compile-time error.
public override int GetArea() => _side * _side;
static void Main()
{
var sq = new Square(12);
Console.WriteLine($"Area of the square = {sq.GetArea()}");
}
}
// Output: Area of the square = 144
Polymorphism
Polymorphism (meaning “many forms”) allows objects of different classes to be treated as objects of a common type. This is achieved through inheritance and method overriding.
public class Shape
{
// A few example members
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }
// Virtual method
public virtual void Draw()
{
Console.WriteLine("Performing base class drawing tasks");
}
}
public class Circle : Shape
{
public override void Draw()
{
// Code to draw a circle...
Console.WriteLine("Drawing a circle");
base.Draw();
}
}
public class Rectangle : Shape
{
public override void Draw()
{
// Code to draw a rectangle...
Console.WriteLine("Drawing a rectangle");
base.Draw();
}
}
public class Triangle : Shape
{
public override void Draw()
{
// Code to draw a triangle...
Console.WriteLine("Drawing a triangle");
base.Draw();
}
}
class Program {
static void Main()
{
var shapes = new List<Shape>
{
new Rectangle(),
new Triangle(),
new Circle()
};
foreach (var shape in shapes)
{
shape.Draw();
}
/*
Output:
Drawing a rectangle
Performing base class drawing tasks
Drawing a triangle
Performing base class drawing tasks
Drawing a circle
Performing base class drawing tasks
*/
}
}
Hide base class members with new members
public class BaseClass
{
public void DoWork() { WorkField++; }
public int WorkField;
public int WorkProperty
{
get { return 0; }
}
}
public class DerivedClass : BaseClass
{
public new void DoWork() { WorkField++; }
public new int WorkField;
public new int WorkProperty
{
get { return 0; }
}
}
class Program {
static void Main()
{
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.
BaseClass A = (BaseClass)B;
A.DoWork(); // Calls the old method.
}
}
Prevent derived classes from overriding virtual members
public class A
{
public virtual void DoWork() { }
}
public class B : A
{
public override void DoWork() { }
}
public class C : B
{
public sealed override void DoWork() { } // sealed keyword is used to prevent to override it on derived class.
}
public class D : C
{
public new void DoWork() { } // it's possible to use the new keyword to declare a new method called DoWork for D class.
}
Access base class virtual members from derived classes
public class Base
{
public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
public override void DoWork()
{
//Perform Derived's work here
//...
// Call DoWork on base class
base.DoWork();
}
}
Abstraction
Abstraction focuses on presenting only essential details to the user while hiding the complex implementation details. It involves defining abstract classes or interfaces that specify a contract (methods that must be implemented by derived classes) without providing concrete implementations.
public abstract class Shape
{
public abstract int GetArea();
}
public class Square : Shape
{
private int _side;
public Square(int n) => _side = n;
// GetArea method is required to avoid a compile-time error.
public override int GetArea() => _side * _side;
static void Main()
{
var sq = new Square(12);
Console.WriteLine($"Area of the square = {sq.GetArea()}");
}
}
// Output: Area of the square = 144
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
Overview
The first paradigm to be adopted (but not the first to be invented) was structured programming, which was discovered by Edsger Wybe Dijkstra in 1968. Dijkstra showed that the use of unrestrained jumps (goto statements) is harmful to program structure. As we’ll see in the chapters that follow, he replaced those jumps with the more familiar if/then/else and do/while/until constructs.
We can summarize the structured programming paradigm as follows:
Structured programming imposes discipline on direct transfer of control.
Proof
The problem that Dijkstra recognized, early on, was that programming is hard, and that programmers don’t do it very well.
Dijkstra’s solution was to apply the mathematical discipline of proof. His vision was the construction of a Euclidian hierarchy of postulates, theorems, corollaries, and lemmas.
Same as mathematicians do, programmers should use proven structures, and tie them together with code that they would then prove correct themselves.
During his investigation, Dijkstra discovered that certain uses of goto statements prevent modules from being decomposed recursively into smaller and smaller units, thereby preventing use of the divide-and-conquer approach necessary for reasonable proofs.
Other uses of goto, however, did not have this problem. Dijkstra realized that these “good” uses of goto corresponded to simple selection and iteration control structures such as if/then/else
and do/while
. Modules that used only those kinds of control structures could be recursively subdivided into provable units.
This discovery was remarkable: The very control structures that made a module provable were the same minimum set of control structures from which all programs can be built. Thus structured programming was born.
As computer languages evolved, the goto statement moved ever rearward, until it all but disappeared.
At the end, there was no formal proof, and the Euclidean hierarchy of theorems was never built.
Functional Decomposition
Structured programming allows modules to be recursively decomposed into provable units, which in turn means that modules can be functionally decomposed.
By following these disciplines, programmers could break down large proposed systems into modules and components that could be further broken down into tiny provable functions.
Scientific Method
Given there was no formal proof, scientific method was then considered.
Science is fundamentally different from mathematics, in that scientific theories and laws cannot be proven correct, but can demonstrated in several ways.
Science does not work by proving statements true, but rather by proving statements false.
Resources
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design, by Robert C. Martin
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
Overview
The second paradigm to be adopted was actually discovered two years earlier, in 1966, by Ole Johan Dahl and Kristen Nygaard. These two programmers noticed that the function call stack frame in the ALGOL language could be moved to a heap, thereby allowing local variables declared by a function to exist long after the function returned. The function became a constructor for a class, the local variables became instance variables, and the nested functions became methods. This led inevitably to the discovery of polymorphism through the disciplined use of function pointers.
We can summarize the object-oriented programming paradigm as follows:
Object-oriented programming imposes discipline on indirect transfer of control.
Resources
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design, by Robert C. Martin
SOLID
Definition
SOLID is a set of five design principles that aim to make object-oriented software easier to understand, maintain, and extend. These principles were popularized by Robert C. Martin (Uncle Bob) in his 2000 paper “Design Principles and Design Patterns” and the acronym SOLID was coined a few years later.
The five SOLID principles are:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
I’ll be using the same example from C# Clean Code: SOLID Principles – Dev.to, so as the initial code we have the following:
public class OrderItem
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class Order
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; }
public string CustomerType { get; set; } // 'Regular', 'Premium'
public Order()
{
Items = new List<OrderItem>();
}
// Calculate order total
public decimal GetTotal()
{
decimal total = 0;
foreach (var item in Items)
{
total += item.Price * item.Quantity;
}
// Apply discount based on customer type
if (CustomerType == "Premium")
{
total *= 0.9m; // 10% discount for premium customers
}
return total;
}
// Print order receipt
public void PrintReceipt()
{
Console.WriteLine($"Order ID: {Id}");
foreach (var item in Items)
{
Console.WriteLine($"{item.Name} - {item.Quantity} x {item.Price} = {item.Quantity * item.Price}");
}
Console.WriteLine($"Total: {GetTotal()}");
}
}
Single Responsibility Principle (SRP)
Definition
The Single Responsibility Principle states that a class should have only one reason to change.
Use Case
Order
class violates the SRP because it has two responsibilities, Order calculation and receipt printing. To fix it, we can separate the responsibilities in two different classes.
Solution
public class OrderItem { ... }
public class Order // This class only handles order calculation now.
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; }
public string CustomerType { get; set; } // 'Regular', 'Premium'
public Order()
{
Items = new List<OrderItem>();
}
// Calculate order total
public decimal GetTotal()
{
decimal total = 0;
foreach (var item in Items)
{
total += item.Price * item.Quantity;
}
// Apply discount based on customer type
if (CustomerType == "Premium")
{
total *= 0.9m; // 10% discount for premium customers
}
return total;
}
}
public class ReceiptPrinter // This class only handles receipt printing now.
{
// Print order receipt
public void PrintReceipt()
{
Console.WriteLine($"Order ID: {Id}");
foreach (var item in Items)
{
Console.WriteLine($"{item.Name} - {item.Quantity} x {item.Price} = {item.Quantity * item.Price}");
}
Console.WriteLine($"Total: {GetTotal()}");
}
}
Open-Closed Principle (OCP)
Definition
The Open-Closed Principle suggests that software entities should be open for extension but closed for modification.
Use Case
The GetTotal
method in our Order class has hardcoded discount logic for premium customers, which violates OCP. To fix it, we can move the discount logic outside the Order
class, and pass it as parameter. This way, the discount functionality will be extendable without chaging the Order
class code.
Solution
public class OrderItem { ... }
public interface IDiscount // We declare IDiscount interface, and include a ApplyDiscount method, so we can have multiple classes implementing it for various kinds of discounts.
{
decimal ApplyDiscount(decimal total);
}
public class NoDiscount : IDiscount // This first class will handle no discount scenario.
{
public decimal ApplyDiscount(decimal total)
{
return total;
}
}
public class PremiumDiscount : IDiscount // This second class will handle the discount for premium customers.
{
public decimal ApplyDiscount(decimal total)
{
return total * 0.9m; // 10% discount for premium customers
}
}
public class Order // This class only handles order calculation now.
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; }
public IDiscount Discount { get; set; } // Customer Type logic was removed, and instead IDiscount was added, so the discount can be handled from outside.
public Order(IDiscount discount) // IDiscount interface is included in the constructor, so any kind of discount can passed from outside and we don't need to worry about how it's applied.
{
Items = new List<OrderItem>();
Discount = discount;
}
// Calculate order total
public decimal GetTotal()
{
decimal total = 0;
foreach (var item in Items)
{
total += item.Price * item.Quantity;
}
return Discount.ApplyDiscount(total); // Condition to apply discount for premium customer was removed and instead the ApplyDiscount method is called from the Discount instance.
}
}
public class ReceiptPrinter { ... }
Liskov Substitution Principle (LSP)
Definition
The Liskov Substitution Principle states that subclasses should be substitutable for their base classes without altering the correctness of the program.
Use Case
By introducing the IDiscount
interface, we’ve already ensured that any class implementing IDiscount
(like PremiumDiscount
, NoDiscount
, or BirthdayDiscount
) can replace each other without breaking the functionality.
Solution
public class OrderItem { ... }
public interface IDiscount
{
decimal ApplyDiscount(decimal total);
}
public class NoDiscount : IDiscount { ... }
public class PremiumDiscount : IDiscount { ... }
public class BirthdayDiscount : IDiscount
{
public decimal ApplyDiscount(decimal total)
{
return total * 0.95m; // 5% birthday discount
}
}
public class ReceiptPrinter { ... }
var birthdayOrder = new Order(new BirthdayDiscount()); // Any discount class can now be used in place of another, following LSP
Console.WriteLine(birthdayOrder.GetTotal());
Interface Segregation Principle (ISP)
Definition
The Interface Segregation Principle suggests that clients should not be forced to depend on interfaces they don’t use.
Use Case
Instead of creating one large interface (e.g., IOrderManager
), it’s better to break it into smaller, more focused interfaces. Let’s apply ISP by splitting responsibilities into smaller interfaces for orders and receipt printing.
Solution
public class OrderItem { ... }
public interface IDiscount { ... }
public class NoDiscount : IDiscount { ... }
public class PremiumDiscount : IDiscount { ... }
public class BirthdayDiscount : IDiscount { ... }
public interface IOrder // Adding this new interface, with "GetTotal" as the only required method to be implemented .
{
decimal GetTotal();
}
public interface IReceiptPrinter // Adding this new interface, with "PrintReceipt" as the only required method to be implemented .
{
void PrintReceipt(Order order);
}
public class Order : IOrder { ... } // Order class is only implementing required methods from IOrder interface.
public class ReceiptPrinter : IReceiptPrinter { ... } // ReceiptPrinter class is only implementing required methods from IReceiptPrinter interface.
Dependency Inversion Principle (DIP)
Definition
The Dependency Inversion Principle states that high-level modules should depend on abstractions, not on concrete implementations.
Use Case
Instead of creating one large interface (e.g., IOrderManager
), it’s better to break it into smaller, more focused interfaces. Let’s apply ISP by splitting responsibilities into smaller interfaces for orders and receipt printing.
Solution
public class OrderItem { ... }
public interface IDiscount { ... }
public class NoDiscount : IDiscount { ... }
public class PremiumDiscount : IDiscount { ... }
public class BirthdayDiscount : IDiscount { ... }
public interface IOrder { ... }
public interface IReceiptPrinter { ... }
public class Order : IOrder // Order class depends on IOrder interface, this way it ensures this class is flexible and it can easily use different implementations without tightly coupled to any one class.
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; }
private readonly IDiscount _discount; // We can change the access modifier to private and set it to readonly, so this parameter is only passed in the constructor.
public Order(IDiscount discount) // The discount will depend on an abstraction for discounts, IDiscount, instead of concrete implementations like PremiumDiscount or NoDiscount.
{
Items = new List<OrderItem>();
_discount = discount;
}
public decimal GetTotal()
{
decimal total = 0;
foreach (var item in Items)
{
total += item.Price * item.Quantity;
}
return _discount.ApplyDiscount(total);
}
}
public class ReceiptPrinter : IReceiptPrinter { ... } // ReceiptPrinter class depends on IReceiptPrinter interface, this way it ensures this class is flexible and it can easily use different implementations without tightly coupled to any one class.
Conclusion
By applying the SOLID principles to a simple Customer Order System example, we refactored the code to become more maintainable, scalable, and flexible. Each principle brings a unique benefit:
- Single Responsibility Principle: Makes each class focused on a single task, improving clarity and maintainability.
- Open-Closed Principle: Allows extending functionality without modifying existing code, reducing the risk of introducing bugs.
- Liskov Substitution Principle: Ensures that subclasses can be used in place of their base classes, preserving correctness.
- Interface Segregation Principle: Promotes the use of smaller, more focused interfaces, reducing unnecessary dependencies.
- Dependency Inversion Principle: Encourages classes to depend on abstractions rather than concrete implementations, improving flexibility.
SOLID
Definition
SOLID is a set of five design principles that aim to make object-oriented software easier to understand, maintain, and extend. These principles were popularized by Robert C. Martin (Uncle Bob) in his 2000 paper “Design Principles and Design Patterns” and the acronym SOLID was coined a few years later.
The five SOLID principles are:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
I’ll be using the same example from C# Clean Code: SOLID Principles – Dev.to, so as the initial code we have the following:
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
Overview
The third paradigm, which has only recently begun to be adopted, was the first to be invented. Indeed, its invention predates computer programming itself. Functional programming is the direct result of the work of Alonzo Church, who in 1936 invented l-calculus while pursuing the same mathematical problem that was motivating Alan Turing at the same time. His l-calculus is the foundation of the LISP language, invented in 1958 by John McCarthy. A foundational notion of l-calculus is immutability—that is, the notion that the values of symbols do not change. This effectively means that a functional language has no assignment statement. Most functional languages do, in fact, have some means to alter the value of a variable, but only under very strict discipline.
We can summarize the functional programming paradigm as follows:
Functional programming imposes discipline upon assignment.
Resources
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design, by Robert C. Martin
Error Handling
try
{
// try code block - code that may generate an exception
}
catch (Exception ex)
{
// catch code block - code to handle an exception
}
finally
{
// finally code block - code to clean up resources
}
Throwing Exceptions
throw new FormatException("FormatException: Calculations in process XYZ have been cancelled due to invalid data format.");