Become SOLID in C#

Pavel Nasovich

Show me your code

public sealed class DelimitedSampleRecordLayout : DelimitedLayout<FixedSampleRecord>
{
    public DelimitedSampleRecordLayout()
    {
        this.WithDelimiter(";")
            .WithQuote("\"")
            .WithMember(x => x.Cuit)
            .WithMember(x => x.Nombre)
            .WithMember(x => x.Actividad, c => c.WithName("AnotherName"));
    }
}

Agenda

  1. What is SOLID?
  2. Single Responsibility Principle
  3. Open Closed Principle
  4. Liskov Substitution Principle
  5. Interface Segregation Principle
  6. Dependency Inversion Principle
  7. Questions and answers

Assumptions

  • Brains
  • Motivation
  • Basic C#/.net knowledge
  • Basic GIT knowledge
  • Bitbucket\Github

What will we do?

What will we do?

  • Discuss
  • Code
  • Code
  • Code
  • Homework

What will we do?

What should I do?

  • Clone git
  • Register in Telegram
  • Make changes
  • Give me rights for code review
  • ???
  • Profit!

What is SOLID?

What is SOLID?

S.O.L.I.D is an acronym for the first five object-oriented design(OOD) principles by Robert C. Martin, popularly known as Uncle Bob.

These principles, when combined together, make it easy for a programmer to develop software that are easy to maintain and extend. They also make it easy for developers to avoid code smells, easily refactor code, and are also a part of the agile or adaptive software development.

What is SOLID?

S.O.L.I.D stands for:

When expanded the acronyms might seem complicated, but they are pretty simple to grasp.

S Single-responsiblity principle
O Open-closed principle
L Liskov substitution principle
I Interface segregation principle
D Dependency Inversion Principle

What is SOLID?

Honestly, S.O.L.I.D might seem to be a handful at first, but with continuous usage and adherence to its guidelines, it becomes a part of you and your code which can easily be extended, modified, tested, and refactored without any problems.

Single Responsibility Principle

Single Responsibility Principle

Название: Принцип единственной ответственности.

Определение: У класса/модуля должна быть лишь одна причина для изменения.

Смысл принципа: Борьба со сложностью, важность которой резко возрастает при развитии логики приложения.

Краткое описание: Любой сложный класс должен быть разбит на несколько простых составляющих, отвечающих за определенный аспект поведения, что упрощает как понимание, так и будущее развитие.

Single Responsibility Principle

Two points of change for your application.

Single Responsibility Principle

Have a look at the code below, can you guess what the problem is ?

class Customer
{
    public void Add()
    {
        try
        {
            // Database code goes here
        }
        catch (Exception ex)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
        }
    }
}

The above customer class is doing things WHICH HE IS NOT SUPPOSED TO DO. Customer class should do customer data validations, call the customer data access layer etc, but if you see the catch block closely it also doing LOGGING activity. 

In simple words its over loaded with lot of responsibility.

Single Responsibility Principle

But if we can have each of those items separated its simple, easy to maintain and one change does not affect the other.

The same principle also applies to classes and objects in software architecture.

Single Responsibility Principle

public class UserService
{
     public void Register(string email, string password)
     {
          if (!email.Contains("@"))
              throw new ValidationException("Email is not an email!");
 
         var user = new User(email, password);
         _database.Save(user);
 
         _smtpClient.Send(new MailMessage("mysite@nowhere.com", email){Subject="HEllo fool!"});
     }
}

The name Register suggests that the method should register the user in the system.

Doing email validation doesn’t seem to belong in an register method.

Let’s break it out into a new method.

Single Responsibility Principle

public class UserService
{
     public void Register(string email, string password)
     {
          if (!ValidateEmail(email))
              throw new ValidationException("Email is not an email!");
 
         var user = new User(email, password);
         _database.Save(user);
 
         _smtpClient.Send(new MailMessage("mysite@nowhere.com", email){Subject="HEllo fool!"});
     }
 
     public bool ValidateEmail(string email)
     {
         return email.Contains("@");
     }
}

If we continue to look at the method we’ll see that it also sends an email and is therefore also responsible of delivering the email.

Lets move that to a new method too.

Single Responsibility Principle

public class UserService
{
     public void Register(string email, string password)
     {
          if (!ValidateEmail(email))
              throw new ValidationException("Email is not an email!");
 
         var user = new User(email, password);
         _database.Save(user);
 
         SendEmail(new MailMessage("mysite@nowhere.com", email){Subject="HEllo fool!"});
     }
 
     public virtual bool ValidateEmail(string email)
     {
         return email.Contains("@");
     }
 
     public bool SendEmail(MailMessage message)
     {
         _smtpClient.Send(message);
     }
}

We did it again. A single line method. Is this sh!t really necessary?

Yep, it is.

Single Responsibility Principle

public class UserService
{
     EmailService _emailService;
 
     public UserService(EmailService emailService)
     {
         _emailService = emailService;
     }
     public void Register(string email, string password)
     {
          if (!_emailService.ValidateEmail(email))
              throw new ValidationException("Email is not an email!");
 
         var user = new User(email, password);
         _database.Save(user);
 
         emailService.SendEmail(new MailMessage("mysite@nowhere.com", email){Subject="HEllo fool!"});
     }
}
 
public class EmailService
{
     public bool virtual ValidateEmail(string email)
     {
         return email.Contains("@");
     }
 
     public bool SendEmail(MailMessage message)
     {
         _smtpClient.Send(message);
     }
}

Summary

  1. Document your code. Using AND’s should make you check your code again.
  2. You should be able to associate all method names with the class/interface name.
  3. A method should only contain logic that can be associated with the method name.

Single Responsibility Principle

Let's code

Open Closed Principle

Open Closed Principle

Open Closed Principle

Название: Принцип Открыт-Закрыт

Определение: Программные сущности (классы, модули, функции и т.п.) должны быть открытыми для расширения, но закрытыми для модификации.

Смысл принципа: ограничить распространение изменений минимальным числом классов/модулей; позволить вести параллельную разработку путем фиксации интерфейсов классов и открытости реализаций.

Краткое описание: закрытость модулей означает стабильность интерфейса и возможность использования классов/модулей клиентами. Открытость модулей - это возможность внесения изменений в поведении, путем изменения реализации или же путем переопределения.

Open Closed Principle

public class Rectangle  
{
    public double Width { get; set; }
    public double Height { get; set; }
}

public class CombinedAreaCalculator  
{
    public double Area(object[] shapes)
    {
        double area = 0;
        foreach (var shape in shapes)
        {
            if (shape is Rectangle)
            {
                Rectangle rectangle = (Rectangle)shape;
                area += rectangle.Width * rectangle.Height;
            }
        }
        return area;
    }
}
0
00

Let's imagine a scenario in which we are given several Rectangles and need to calculate the total combined area of all of them. We then come along and create a solution that looks something like this:

Open Closed Principle

public class Circle  
{
    public double Radius { get; set; }
}
0
00

This code does exactly what we want it to do, and it works great for rectangles. But, what happens if some of our shapes are circles?

We have to change the CombinedAreaCalculator to accommodate this:

public class CombinedAreaCalculator {
    public double Area(object[] shapes) {
        double area = 0;
        foreach (var shape in shapes) {
            if (shape is Rectangle) {
                Rectangle rectangle = (Rectangle)shape;
                area += rectangle.Width * rectangle.Height;
            }
            if (shape is Circle) {
                Circle circle = (Circle)shape;
                area += (circle.Radius * circle.Radius) * Math.PI;
            }
        }
        return area;
    }
}

Open Closed Principle

0
00

By doing this we have violated the Open/Closed Principle; in order to extend the functionality of the CombinedAreaCalculator class, we had to modify the class's source.

What happens when some of our shapes are triangles, or octogons, or trapezoids? In each case, we have to add a new if clause to the CombinedAreaCalculator.

In essence, CombinedAreaCalculator is not closed for modification, and isn't really open for extension (what good would inheriting from CombinedAreaCalculator do for another class?).

Open Closed Principle

0
00

So, we need to refactor. There are many ways to refactor this to uphold Open/Closed; I'm going to show you just one of them. Let's create an abstract class that all the shapes can inherit from:

public abstract class Shape  
{
    public abstract double Area();
}

Notice that our abstract Shape class has a method for Area. We're moving the dependency for calculating the area from one centralized class to the individual shapes.

The CombinedAreaCalculator will just call each individual Shape class's Area method. 

Open Closed Principle

0
00

The individual shape classes can now be implemented like this:

public class Rectangle : Shape  
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() {
        return Width * Height;
    }
}
public class Circle : Shape  
{
    public double Radius { get; set; }
    public override double Area() {
        return Radius * Radius * Math.PI;
    }
}
public class Triangle : Shape  
{
    public double Height { get; set; }
    public double Width { get; set; }
    public override double Area() {
        return Height * Width * 0.5;
    }
}

Open Closed Principle

0
00

And finally, we can create the new CombinedAreaCalculator class:

public class CombinedAreaCalculator  
{
    public double Area(Shape[] shapes)
    {
        double area = 0;
        foreach (var shape in shapes)
        {
            area += shape.Area();
        }
        return area;
    }
}

We've made the Shape abstract class and the CombinedAreaCalculator class open for extension and closed for modification, thereby upholding the Open/Closed Principle.

If we need to add any other shapes, we just create a class for them that inherits from Shape and we're good to go.

Open Closed Principle

Open Closed Principle

Potential Hazards

You shouldn't interpret this rule as "don't change already implemented classes, ever." Of course scenarios will arise that will force or require you to change classes that are already implemented.

However, we should use discretion when attempting to make these modifications, and keeping OCP in mind allows us to do that in a more efficient manner.

We only need to keep in mind that modules, ideally, should be open for extension and closed for modification. But if we have to change the code to support new rules and requirements, and the best way to support those requirements is to change existing class functionality, we shouldn't be afraid to just do it.

Open Closed Principle

class Product
{
    public Product(ProductColor color)
    {
        this.Color = color;
    }

    public Product(ProductColor color, ProductSize size)
    {
        this.Color = color;
        this.Size = size;
    }

    public ProductColor Color { get; set; }
    public ProductSize Size { get; set; } 
}
public enum ProductColor
{
    Blue,
    Yellow,
    Red,
    Gold,
    Brown
}
public enum ProductSize
{
    Small,
    Medium,
    Large,
    ReallyBig
}
1
11

Open Closed Principle

class ProductFilter
{
	public IEnumerable<Product> ByColor(IList<Product> products, ProductColor productColor)
	{
		foreach (var product in products)
		{
			if (product.Color == productColor) yield return product;
		}
	}

	public IEnumerable<Product> ByColorAndSize(
            IList<Product> products, 
            ProductColor productColor,
            ProductSize productSize)
	{
		foreach (var product in products)
		{
			if ((product.Color == productColor) && (product.Size == productSize)) {
                            yield return product;
                        }
		}
	}

	public IEnumerable<Product> BySize(IList<Product> products, ProductSize productSize)
	{
		foreach (var product in products)
		{
			if ((product.Size == productSize)) yield return product;
		}
	}
}
1
11

Open Closed Principle

private IList<Product> BuildProducts()
{
	return new List<Product> {
		new Product(ProductColor.Blue, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Medium),
		new Product(ProductColor.Red, ProductSize.Large),
		new Product(ProductColor.Blue, ProductSize.ReallyBig)
	};
}
[Test]
public void filterByBlue_return_2()
{
	// arrange
	ProductFilter filter = new ProductFilter();
	IList<Product> products = BuildProducts();

	// act
	var result = filter.ByColor(products, ProductColor.Blue);

	// assert
	Assert.That(result.Count(), Is.EqualTo(2));
	Assert.That(result, Has.All.Matches<Product>(x => x.Color == ProductColor.Blue));
}
1
11

Open Closed Principle

private IList<Product> BuildProducts()
{
	return new List<Product> {
		new Product(ProductColor.Blue, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Medium),
		new Product(ProductColor.Red, ProductSize.Large),
		new Product(ProductColor.Blue, ProductSize.ReallyBig)
	};
}
[Test]
public void filterBySmall_return_2()
{
	// arrange
	ProductFilter filter = new ProductFilter();
	IList<Product> products = BuildProducts();

	// act
	var result = filter.BySize(products,  ProductSize.Small);

	// assert
	Assert.That(result.Count(), Is.EqualTo(2));
	Assert.That(result, Has.All.Matches<Product>(x => x.Size == ProductSize.Small));
}
1
11

Open Closed Principle

private IList<Product> BuildProducts()
{
	return new List<Product> {
		new Product(ProductColor.Blue, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Medium),
		new Product(ProductColor.Red, ProductSize.Large),
		new Product(ProductColor.Blue, ProductSize.ReallyBig)
	};
}
[Test]
public void filterByBlueAndSmall_return_1()
{
	// arrange
	ProductFilter filter = new ProductFilter();
	IList<Product> products = BuildProducts();

	// act
	var result = filter.ByColorAndSize(products, ProductColor.Blue, ProductSize.Small);

	// assert
	Assert.That(result.Count(), Is.EqualTo(1));
	Assert.That(result, Has.All.Matches<Product>(x => x.Size == ProductSize.Small));
}
1
11

Open Closed Principle

1
11

Open Closed Principle

class Product
{
    public Product(ProductColor color)
    {
        this.Color = color;
    }

    public Product(ProductColor color, ProductSize size)
    {
        this.Color = color;
        this.Size = size;
    }

    public ProductColor Color { get; set; }
    public ProductSize Size { get; set; } 
}
public enum ProductColor
{
    Blue,
    Yellow,
    Red,
    Gold,
    Brown
}
public enum ProductSize
{
    Small,
    Medium,
    Large,
    ReallyBig
}

After refactoring. Base structure is the same.

1
11

Open Closed Principle

class ProductFilter
{
	public IEnumerable<Product> By(
            IList<Product> products, 
            IFilterSpecification filterSpecification)
	{
		return filterSpecification.Filter(products);
	}
}
interface IFilterSpecification
{
	IEnumerable<Product> Filter(IList<Product> products);
}
1
11

Open Closed Principle

class FilterSpecificationColor : IFilterSpecification
{
	private readonly ProductColor productColor;

	public FilterSpecificationColor(ProductColor productColor)
	{
		this.productColor = productColor;
	}

	public IEnumerable<Product> Filter(IList<Product> products)
	{
		foreach (var product in products)
		{
			if (product.Color == productColor) yield return product;
		}
	}
}
1
11

Open Closed Principle

class FilterSpecificationSize : IFilterSpecification
{
	private readonly ProductSize productSize;

	public FilterSpecificationSize(ProductSize productSize)
	{
		this.productSize = productSize;
	}

	public IEnumerable<Product> Filter(IList<Product> products)
	{
		foreach (var product in products)
		{
			if (product.Size == productSize) yield return product;
		}
	}
}
1
11

Open Closed Principle

class FilterSpecificationColorAndSize : IFilterSpecification
{
	private readonly ProductColor productColor;
	private readonly ProductSize productSize;

	public FilterSpecificationColorAndSize(
            ProductColor productColor, 
            ProductSize productSize)
	{
		this.productColor = productColor;
		this.productSize = productSize;
	}

	public IEnumerable<Product> Filter(IList<Product> products)
	{
		foreach (var product in products)
		{
			if (product.Color == productColor 
                            && product.Size == productSize) 
                        {
                            yield return product;
                        }
		}
	}
}
1
11

Open Closed Principle

private IList<Product> BuildProducts()
{
	return new List<Product> {
		new Product(ProductColor.Blue, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Medium),
		new Product(ProductColor.Red, ProductSize.Large),
		new Product(ProductColor.Blue, ProductSize.ReallyBig)
	};
}
[Test]
public void filterByBlue_return_2()
{
	// arrange
	ProductFilter filter = new ProductFilter();
	IList<Product> products = BuildProducts();

	// act
	var result = filter.By(products, new FilterSpecificationColor(ProductColor.Blue));

	// assert
	Assert.That(result.Count(), Is.EqualTo(2));
	Assert.That(result, Has.All.Matches<Product>(x => x.Color == ProductColor.Blue));
}
1
11

Open Closed Principle

private IList<Product> BuildProducts()
{
	return new List<Product> {
		new Product(ProductColor.Blue, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Medium),
		new Product(ProductColor.Red, ProductSize.Large),
		new Product(ProductColor.Blue, ProductSize.ReallyBig)
	};
}
[Test]
public void filterBySmall_return_2()
{
	// arrange
	ProductFilter filter = new ProductFilter();
	IList<Product> products = BuildProducts();

	// act
	var result = filter.By(products, new FilterSpecificationSize(ProductSize.Small));

	// assert
	Assert.That(result.Count(), Is.EqualTo(2));
	Assert.That(result, Has.All.Matches<Product>(x => x.Size == ProductSize.Small));
}
1
11

Open Closed Principle

private IList<Product> BuildProducts()
{
	return new List<Product> {
		new Product(ProductColor.Blue, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Small),
		new Product(ProductColor.Yellow, ProductSize.Medium),
		new Product(ProductColor.Red, ProductSize.Large),
		new Product(ProductColor.Blue, ProductSize.ReallyBig)
	};
}
[Test]
public void filterByBlueAndSmall_return_1()
{
	// arrange
	ProductFilter filter = new ProductFilter();
	IList<Product> products = BuildProducts();

	// act
	var result = filter.By(products, 
            new FilterSpecificationColorAndSize(ProductColor.Blue, ProductSize.Small));

	// assert
	Assert.That(result.Count(), Is.EqualTo(1));
	Assert.That(result, Has.All.Matches<Product>(x => x.Size == ProductSize.Small));
}
1
11

Open Closed Principle

Open Closed Principle

public abstract class Beverage
{
	public string Description { get; set; }
	public abstract Decimal GetCost();
}

public class ExpressoWithMocha : Beverage
{
	public override decimal GetCost()
	{
		return 5.5m;
	}
}

public class ExpressoWithSteamedMilkAndMocha : Beverage
{
	public override decimal GetCost()
	{
		return 6.5m;
	}
}
2
22

Open Closed Principle

public abstract class Beverage
{
	public string Description { get; set; }
	public abstract Decimal GetCost();
}
public class Expresso : Beverage
{
	public override Decimal GetCost()
	{
		return 5m;
	}
}
public class WithMocha : Beverage
{
	private readonly Beverage beverage;

	public WithMocha(Beverage beverage)
	{
		this.beverage = beverage;
	}

	public override decimal GetCost()
	{
		return beverage.GetCost() + 0.5m;
	}
}
public class WithSteamedMilk : Beverage
{
	private readonly Beverage beverage;

	public WithSteamedMilk(Beverage beverage)
	{
		this.beverage = beverage;
	}

	public override decimal GetCost()
	{
		return beverage.GetCost() + 1;
	}
}
2
22

Open Closed Principle

Open Closed Principle

public class Beverage
{
	private Decimal cost;
	public string Description { get; set; }

	public Beverage(Decimal cost)
	{
		this.cost = cost;
	}

	public Decimal GetCost()
	{
		return cost;
	}
}
public class PriceList
{
	public decimal GetExpresso()
	{
		return 5m;
	}
	public decimal GetMocha()
	{
		return 0.5m;
	}
	public decimal GetSteamedMilk()
	{
		return 1m;
	}
}
public abstract class Beverage
{
	public string Description { get; set; }
	public abstract Decimal GetCost();
}
3
33

Open Closed Principle

public class BeverageBuilder
{
	private readonly PriceList priceList;
	private Beverage beverage;

	public BeverageBuilder(PriceList priceList)
	{
		this.priceList = priceList;
	}

	public BeverageBuilder CreateExpresso()
	{
		beverage = new Beverage(priceList.GetExpresso());
		return this;
	}
	public BeverageBuilder WithMocha()
	{
		beverage = new Beverage(beverage.GetCost() + priceList.GetMocha());
		return this;
	}
	public BeverageBuilder WithSteamedMilk()
	{
		beverage = new Beverage(beverage.GetCost() + priceList.GetSteamedMilk());
		return this;
	}
	public Beverage Build()
	{
		return beverage;
	}
}
3
33

Open Closed Principle

Open Closed Principle

public void Parse()
{
    StringReader reader = new StringReader(scriptTextToProcess);
    StringBuilder scope = new StringBuilder();
    string line = reader.ReadLine();
    while (line != null)
    {
        switch (line[0])
        {
            case '$':
                // Process the entire "line" as a variable, 
                // i.e. add it to a collection of KeyValuePair.
                AddToVariables(line);
                break;
            case '!':
                // Depending of what comes after the '!' character, 
                // process the entire "scope" and/or the command in "line".
                if (line == "!execute")
                    ExecuteScope(scope);
                else if (line.StartsWith("!custom_command"))
                    RunCustomCommand(line, scope);
                else if (line == "!single_line_directive")
                    ProcessDirective(line);
 
                scope = new StringBuilder();
                break;
 
            default:
                // No processing directive, i.e. add the "line" 
                // to the current scope.
                scope.Append(line);
                break;
        }
 
        line = reader.ReadLine();
    }
}
4
44

Lets create an interface which is used for each handler (for '$' and '!')

Open Closed Principle

public interface IMyHandler
{
    void Process(IProcessContext context, string line);
}

Notice that we include a context object. This is quite important. If we create a new parser called SuperCoolParser in the future we can let it create and pass a SuperAwesomeContext to all handlers.

New handlers which supports that context can use it while others stick with the basic implementation.

We comply with Liskovs Substitution Principle and doesn’t have to change the IMyHandler.Process
signature (and therefore keeping it closed for modification) when we add new features later on.

4
44

Open Closed Principle

public class Parser
{
    private Dictionary<char, IMyHandler> _handlers = new Dictionary<char, IMyHandler>();
    private IMyHandler _defaultHandler;
 
    public void Add(char controlCharacter, IMyHandler handler)
    {
        _handlers.Add(controlCharacter, handler);
    }
 
    private void Parse(TextReader reader)
    {
        StringBuilder scope = new StringBuilder();
        IProcessContext context = null; // create your context here.
 
        string line = reader.ReadLine();
        while (line != null)
        {
            IMyHandler handler = null;
            if (!_handlers.TryGetValue(line[0], out handler))
                handler = _defaultHandler;
 
            handler.Process(context, line);
 
 
            line = reader.ReadLine();
        }
    }
}

The parser itself is implemented as:

4
44

Open Closed Principle

case '!':
    // Depending of what comes after the '!' character,
    // process the entire "scope" and/or the command in "line".
    if (line == "!execute")
	ExecuteScope(scope);
    else if (line.StartsWith("!custom_command"))
	RunCustomCommand(line, scope);
    else if (line == "!single_line_directive")
	ProcessDirective(line);

    scope = new StringBuilder();
    break;

Go back to

A lot of if statements. That method likely have to be changed to add support for more features. Hence it do also violate the principle. Let’s refactor again.

public interface ICommandHandler
{
    void Handle(ICommandContext context, string commandName, string[] arguments);
}
4
44

Open Closed Principle

public class CommandService : IMyHandler
{
    public void Add(string commandName, ICommandHandler handler) 
    {
    }
 
    public void Handle(IProcessContext context, string line)
    {
       // first word on the line is the command, all other words are arguments.
       // split the string properly
 
       // then find the corrext command handler and invoke it.
       // take the result and add it to the `IProcessContext`
    }
}
4
44

That gives more flexibility for both handling the actual protocol and add more commands. you do not have to change anything to add more functionality.

The solution is therefore OK regarding Open/Closed and some other SOLID principles.

Liskov Substitution Principle

Liskov Substitution Principle

Название: Принцип замещения Барбары Лисков

Определение: Должна быть возможность вместо базового типа подставить любой его подтип.

Смысл: Реализуйте наследование подтипов правильно.

Краткое описание: для корректной реализации отношения «ЯВЛЯЕТСЯ», наследник может ослаблять предусловие и усиливать постусловие (требовать меньше и гарантировать больше), при этом инварианты базового класса должны выполняться наследником. При нарушении этих правил подстановка экземпляров наследника в метод, принимающий базовый класс будет приводить к непредсказуемым последствиям.

Liskov Substitution Principle

Liskov Substitution Principle

What Is This Principle?

The Liskov Substitution Principle(LSP), named for and originally defined by Barbara Liskov, states that we should be able to treat a child class as though it were the parent class.

Essentially this means that all derived classes should retain the functionality of their parent class and cannot replace any functionality the parent provides.

The LSP is very similar in principle to the Open/Closed Principle.

Liskov Substitution Principle

Benefits?

This principle aims to keep functionality intact. It's main purpose is to guarantee that objects lower in a relational hierarchy can be treated as though they are objects higher in the hierarchy.

Basically, any child class should be able to do anything the parent can do.

Liskov Substitution Principle

A Simple Example

We'll use the classic Circle-Ellipse problem to demonstrate this principle. Let's imagine that we need to find the area of any ellipse.

So, we create a class that represents an ellipse:

public class Ellipse  
{
    public double MajorAxis { get; set; }
    public double MinorAxis { get; set; }

    public virtual void SetMajorAxis(double majorAxis)
    {
        MajorAxis = majorAxis;
    }

    public virtual void SetMinorAxis(double minorAxis)
    {
        MinorAxis = minorAxis;
    }

    public virtual double Area()
    {
        return MajorAxis * MinorAxis * Math.PI;
    }
}

Liskov Substitution Principle

A Simple Example

We know from high school geometry that a circle is just a special case for an ellipse, so we create a Circle class that inherits from Ellipse, but SetMajorAxis sets both axes (because in a circle, the major and minor axes must always be the same, which is just the radius):

public class Circle : Ellipse  
{
    public override void SetMajorAxis(double majorAxis)
    {
        base.SetMajorAxis(majorAxis);
        this.MinorAxis = majorAxis; //In a cirle, each axis is identical
    }
}

See the problem now?

Liskov Substitution Principle

A Simple Example

If we set both axes, attempting to calculate the area gives the wrong result.

Circle circle = new Circle();  
circle.SetMajorAxis(5);  
circle.SetMinorAxis(4);  
var area = circle.Area(); //5*4 = 20, but we expected 5*5 = 25 

This is a violation of the Liskov Substitution Principle. However, the best way to refactor this code is not obvious, as there are quite a few possibilities.

Liskov Substitution Principle

A Simple Example

One solution might be to have Circle implement SetMinorAxis as well:

public class Circle : Ellipse  
{
    public override void SetMajorAxis(double majorAxis)
    {
        base.SetMajorAxis(majorAxis);
        this.MinorAxis = majorAxis; //In a cirle, each axis is identical
    }

    public override void SetMinorAxis(double minorAxis)
    {
        base.SetMinorAxis(minorAxis);
        this.MajorAxis = minorAxis;
    }

    public override double Area()
    {
        return base.Area();
    }
}

Liskov Substitution Principle

A Simple Example

Another solution, one with less code overall, might be to treat Circle as an entirely separate class:

public class Circle  
{
    public double Radius { get; set; }
    public void SetRadius(double radius)
    {
        this.Radius = radius;
    }

    public double Area()
    {
        return this.Radius * this.Radius * Math.PI;
    }
}

Liskov Substitution Principle

A Simple Example

Both solutions have their own drawbacks.

  • The first can be considered a hack, since we have two different methods on the Circle class that essentially do the same thing.
  • The second could be considered improper modeling, as we are treating Circle like a separate class even though it really is a special case of Ellipse.

Given the choice, though, I personally am more likely to choose the second solution rather than the first one, as I feel it provides a better overall model (and doesn't use any redundant code).

Liskov Substitution Principle

ElectricDuck Example

Let’s use the motivator image as inspiration and define the following classes:

public interface IDuck
{
   void Swim();
   // contract says that IsSwimming should be true if Swim has been called.
   bool IsSwimming { get; }
}
public class ElectricDuck : IDuck
{
   bool _isSwimming;

   public void Swim()
   {
      if (!IsTurnedOn)
        return;
      _isSwimming = true;
      //swim logic  

   }

   bool IsSwimming { get { return _isSwimming; } }
}
public class OrganicDuck : IDuck
{
   public void Swim()
   {
      //do something to swim
   }

   bool IsSwimming 
   { 
       get 
       { 
        /* return if the duck is swimming */ 
       }
   }
}

Liskov Substitution Principle

ElectricDuck Example

And the calling code:

void MakeDuckSwim(IDuck duck)
{
    duck.Swim();
}

As you can see, there are two examples of ducks. One organic duck and one electric duck. The electric duck can only swim if it's turned on.

This breaks the LSP principle since it must be turned on to be able to swim as the IsSwimming (which also is part of the contract) won't be set as in the base class.

Liskov Substitution Principle

ElectricDuck Example

You can of course solve it by doing something like this:

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();
    duck.Swim();
}

But that would break Open/Closed principle and has to be implemented everywhere (and thefore still generates unstable code).

Liskov Substitution Principle

ElectricDuck Example

The proper solution would be to automatically turn on the duck in the Swim method and by doing so make the electric duck behave exactly as defined by the IDuck interface

public class ElectricDuck : IDuck
{
   public void Swim()
   {
      if (!IsTurnedOn)
        TurnOnDuck();
 
      //swim logic  
   }
}

Liskov Substitution Principle

Whenever you're feeling the urge to check the subtype of an object, you're most likely doing it WRONG.

Liskov Substitution Principle

Whenever you're feeling the urge to check the subtype of an object, you're most likely doing it WRONG.

Liskov Substitution Principle

Whenever you're feeling the urge to check the subtype of an object, you're most likely doing it WRONG.

Liskov Substitution Principle

Whenever you're feeling the urge to check the subtype of an object, you're most likely doing it WRONG.

Liskov Substitution Principle

Whenever you're feeling the urge to check the subtype of an object, you're most likely doing it WRONG.

Liskov Substitution Principle

Visitor pattern Example

I actually like the Visitor pattern a lot as it can take care of large if-statement spaghetti and it is simpler to implement than what you'd think on existing code. Say we have the following context:

public class Context {

    public void DoStuff(string query) {

        // outcome no. 1
        if (query.Equals("Hello")) {
            Console.WriteLine("Hello world!");
        } 

        // outcome no. 2
        else if (query.Equals("Bye")) {
            Console.WriteLine("Good bye cruel world!");
        }

        // a change request may require another outcome...
    }
}

Liskov Substitution Principle

Visitor pattern Example

And usage will be look like:

// usage:
Context c = new Context();

c.DoStuff("Hello");
// prints "Hello world"

c.DoStuff("Bye");
// prints "Bye"

Liskov Substitution Principle

Visitor pattern Example

The outcomes of the if-statement can be translated into their own visitors as each is depending on some decision and some code to run. We can extract these like this:

public interface IVisitor {
    public bool CanDo(string query);
    public void DoStuff();
}

// outcome 1
public class HelloVisitor : IVisitor {
    public bool CanDo(string query) {
        return query.Equals("Hello");
    }
    public void doStuff() {
         Console.WriteLine("Hello World");
    }
}
// outcome 2
public class ByeVisitor : IVisitor {
    public bool CanDo(string query) {
        return query.Equals("Bye");
    }
    public void DoStuff() {
        Console.WriteLine("Good bye cruel world");
    }
}

Liskov Substitution Principle

Visitor pattern Example

At this point, if the programmer did not know about the Visitor pattern, he'd instead implement the Context class to check if it is of some certain type. Because the Visitor classes have a boolean canDo method, the implementor can use that method call to determine if it is the right object to do the job.

The context class can use all visitors (and add new ones) like this:

Liskov Substitution Principle

Visitor pattern Example

public class Context {
    private List<IVisitor> visitors = new List<IVisitor>();

    public Context() {
        visitors.Add(new HelloVisitor());
        visitors.Add(new ByeVisitor());
    }

    // instead of if-statements, go through all visitors
    // and use the canDo method to determine if the 
    // visitor object is the right one to "visit"
    public void DoStuff(string query) {
        foreach(var visitor in visitors) {
            if (visitor.CanDo(query)) {
                visitor.DoStuff();
                break;
                // or return... it depends if you have logic 
                // after this foreach loop
            }
        }
    }

    // dynamically adds new visitors
    public void AddVisitor(IVisitor visitor) {
        if (visitor != null)
            visitors.add(visitor);
    }
}

Liskov Substitution Principle

Ostrich Example

Let's imagine that we want to create an object model of the birds. All looks rather good from the first view:

class Bird
{
   public virtual void Fly()
   {
      Console.WriteLine("Flying ....");
   }
}

class Pigeon: Bird
{
   public override void Fly()
  {
      //implementing some flying abilities
      base.Fly();
      Console.WriteLine("...and shitting on your head wherever you go ;)");
   }
}

Liskov Substitution Principle

Ostrich Example

And we use them as:

class Program
{
   static void Main()
   {
       var birds = new List<Bird>();        
       birds.Add(new Pigeon());
       birds.Add(new Ostrich());
        
       foreach (Bird bird in birds)        
       {
           bird.Fly();
       }
   }
}

Liskov Substitution Principle

Ostrich Example

New specification.

And we add...

Ostrich:

class Ostrich : Bird
{
   public override void Fly()
   {
      //Oops....
     Console.WriteLine("I wish i could but ....");
   }
}

Liskov Substitution Principle

Using Contracts to discover Liskov Substitution Principle Violations

  1. Install the Code Contracts for .NET extension into Visual Studio.
  2. Open Visual Studio and load the solution containing the projects you want to apply contracts to.
  3. Open the properties for the project and you’ll see a new tab in the project properties window called “Code Contracts”
  4. Make sure that the “Perform Runtime Contract Checking” and “Perform Static Contract Checking” boxes are checked. For the moment the other options can be left at their default values. Only apply these to the debug build. It will slow down the application while it is running as each time a method with contract conditions is called it will be performing runtime checks.

Liskov Substitution Principle

Using Contracts to discover Liskov Substitution Principle Violations

Setting up the Contract

Using the Rectangle/Square example from Bob Martin’s book Agile Principles, Patterns and Practices in C# here is the code with contracts added:

Liskov Substitution Principle

Using Contracts to discover Liskov Substitution Principle Violations

public class Rectangle {
    private int _width;
    private int _height;
    public virtual int Width {
        get { return _width; }
        set {
            Contract.Requires(value >= 0);
            Contract.Ensures(Width == value);
            Contract.Ensures(Height == Contract.OldValue(Height));
            _width = value;
        }
    }

    public virtual int Height {
        get { return _height; }
        set {
            Contract.Requires(value >= 0);
            Contract.Ensures(Height == value);
            Contract.Ensures(Width == Contract.OldValue(Width));
            _height = value;
        }
    }
    public int Area { get { return Width * Height; } }
}

Liskov Substitution Principle

Using Contracts to discover Liskov Substitution Principle Violations

The Square class is in violation of the LSP because it changes the behaviour of the Width and Height setters. To any user of Rectangle that doesn’t know about squares it is quite understandable that they’d assume that setting the Height left the Width alone and vice versa.
So if they were given a square and they attempted to set the width and height to different values then they’d get a result from Area that was inconsistent with their expectation and if they set Height then queried Width they may be somewhat surprised at the result.

[Test]
public void TestSquare()
{
    var r = new Square();
    r.Width = 10;

    Assert.AreEqual(10, r.Height);
}

Liskov Substitution Principle

Using Contracts to discover Liskov Substitution Principle Violations

When the test runner gets to this test it will fail. Not because the underlying code is wrong (it will set Height to be equal to Width), but because the method violates the constraints of the base class.

Liskov Substitution Principle

Using Contracts to discover Liskov Substitution Principle Violations

The contract says that when the base class changes the Width the Height remains the same and vice versa.

So, despite the fact that the unit tests were not explicitly testing for an LSP violation in Square, the contract system sprung up and highlighted the issue causing the test to fail.

Liskov Substitution Principle

Violating one principle but following other

There are ways to break one of the principles but still have the other be followed. The examples below seem contrived, for good reason, but I've actually seen these popping up in production code (and even worser).

Follows OCP but not LSP

Lets say we have the given code:

public interface IPerson {}

public class Boss : IPerson {
    public void DoBossStuff() { ... }
}

public class Peon : IPerson {
    public void DoPeonStuff() { ... }
}

public class Context {
    public IEnumerable<IPerson> GetPersons { ... }
}

Liskov Substitution Principle

Violating one principle but following other

Follows OCP but not LSP

This piece of code follows the open-closed principle. If we're calling the context's GetPersons method, we'll get a bunch of persons all with their own implementations. That means that IPerson is closed for modification, but open for extension. However things take a dark turn when we have to use it:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Innumerable<IPerson> persons = context.GetPersons();
foreach (IPerson person in persons) {
    // now we have to check the type... :-P
    if (person.GetType() == typeof(Boss)) {
        ((Boss) person).DoBossStuff();
    }
    else if (person.GetType() == typeof(Peon)) {
        ((Peon) person).DoPeonStuff();
    }
}

Liskov Substitution Principle

Violating one principle but following other

Follows OCP but not LSP

You have to do type checking and type conversion! Remember how I mentioned above how type checking is a bad thing? Oh no! But fear not, as also mentioned above either do some pull-up refactoring or implement a Visitor pattern. In this case we can simply do a pull up refactoring after adding a general method:

public class Boss : IPerson {
    // we're adding this general method
    public void DoStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void DoBossStuff() { ... }
}

public interface IPerson {
    // pulled up method from Boss
    public void DoStuff();
}

Liskov Substitution Principle

Violating one principle but following other

Follows OCP but not LSP

The benefit now is that you don't need to know the exact type anymore, following LSP:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Innumerable<IPerson> persons = context.GetPersons();
foreach (IPerson person in persons) {
    // yay, no type checking!
    person.DoStuff();
}

Liskov Substitution Principle

Violating one principle but following other

Follows LSP but not OCP

Lets look at some code that follows LSP but not OCP, it is kind of contrived but bear with me on this one it's very subtle mistake:

public class LiskovBase {
    public void doStuff() {
        Console.WriteLine("My name is Liskov");
    }
}
public class LiskovSub : LiskovBase {
    public void doStuff() {
        Console.WriteLine("I'm a sub Liskov!");
    }
}
public class Context {
    private LiskovBase base;

    // the good stuff
    public void DoLiskovyStuff() {
        base.DoStuff();
    }

    public void SetBase(LiskovBase base) { this.base = base }
}

Liskov Substitution Principle

Violating one principle but following other

Follows LSP but not OCP

The code does LSP because the context can use LiskovBase without knowing the actual type. You'd think this code follows OCP as well but look closely, is the class really closed? What if the DoStuff method did more than just print out a line?

The answer if it follows OCP is simply: NO, it isn't because in this object design we're required to override the code completely with something else. This opens up the cut-and-paste can of worms as you have to copy code over from the base class to get things working. The DoStuff method sure is open for extension, but it wasn't completely closed for modification.

Liskov Substitution Principle

Violating one principle but following other

Follows LSP but not OCP

We can apply the Template method pattern on this. The template method pattern is so common in frameworks that you might have been using it without knowing it (e.g. java swing components, c# forms and components, etc.).

Liskov Substitution Principle

Violating one principle but following other

Follows LSP but not OCP

public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public void DoStuff() {
        Console.WriteLine(GetStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public virtual string GetStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub : LiskovBase {
    // the extension overridden
    // the actual code that varied
    public override string GetStuffString() {
        return "I'm sub Liskov!";
    }
}

Liskov Substitution Principle

Violating one principle but following other

Follows LSP but not OCP

This example follows OCP and seems silly, which it is, but imagine this scaled up with more code to handle.

I keep seeing code deployed in production where subclasses completely override everything and the overridden code is mostly cut-n-pasted between implementations.

It works, but as with all code duplication is also a set-up for maintenance nightmares.

Liskov Substitution Principle

Conclusion

I hope this all clears out some questions regarding OCP and LSP and the differences/similarities between them. It is easy to dismiss them as the same but the examples above should show that they aren't.

Do note that, gathering from above sample code:

OCP is about locking the working code down but still keep it open somehow with some kind of extension points. This is to avoid code duplication by encapsulating the code that changes as with the example of Template Method pattern. It also allows for failing fast as breaking changes are painful (i.e. change one place, break it everywhere else). For the sake of maintenance the concept of encapsulating change is a good thing, because changes always happen.

Liskov Substitution Principle

Conclusion

I hope this all clears out some questions regarding OCP and LSP and the differences/similarities between them. It is easy to dismiss them as the same but the examples above should show that they aren't.

Do note that, gathering from above sample code:

LSP is about letting the user handle different objects that implement a supertype without checking what the actual type they are. This is inherently what polymorphism is about. This principle provides an alternative to do type-checking and type-conversion, that can get out of hand as the number of types grow, and can be achieved through pull-up refactoring or applying patterns such as Visitor.

Interface Segregation Principle

Interface Segregation Principle

Interface Segregation Principle

Interface Segregation Principle

Dependency Inversion Principle

Dependency Inversion Principle

Dependency Inversion Principle

Dependency Inversion Principle

Questions and answers

Questions and answers

Questions and answers

Questions and answers