S.O.L.I.D
Telerik Academy Alpha

 

HQC

 Table of contents

S.O.L.I.D?

 MInd map - SOLID

  • You should make a mind map
    • What you know about SOLID
      • Have you applied any of those IRL?
  • You have about 5 minutes to think and make it

 

 What?

  •  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.

 Why?

  • A software code should depict following qualities:

  • Maintainability

    • Maintenance requires more effort than any other software engineering activity

  • Extensibility

    • Extensibility is the capacity to extend or stretch the functionality of the development environment

  • Modularity

    • Modularity is designing a system that is divided into a set of functional units that can be composed into a larger application

 When?

  • They help a lot to create clean, extensible and maintainable code.
  • You should at least know them so you can decide when to apply them and when not.

"They are not laws. They are not perfect truths.”

 ​                                       Robert C. Martin (Uncle Bob)      

Single Responsibility

 Single Responsibility

  • It relates strongly to cohesion and coupling: strive for high cohesion, but for low coupling.
  • More responsibilities in a module makes it more likely to change.
  • The more modules a change affects, the more likely the change will introduce an error.
  • Therefore, a class should only have one reason to change and correspond to have one single responsibility.

 Cohesion

  • Cohesion: how strongly-related and focused are the various elements of a module (class, function, namespace...)

BAD

GOOD

 Coupling

  • Coupling: the degree to which each module relies on each on of the other modules.

BAD

GOOD

 SRP - Classic violations

  • Objects that can print/draw themselves 
  • Objects that export their specific data
public class Person {
    public string FirstName { get; set; }
    public string LastName { get; set; }    
    public object Export(string format, string? location = null) {
        switch(format) {
            case "Excel":
                // excel specific rendering and saving to location here
                excel.Save(location);
                break;
            case "JSON":
                // json specific rendering
                return jsonString;
                break;
            default:
                // default behaviour here, saving it in a text file
                File.WriteAllText(location.Value, $"{FirstName} {LastName}.");
        }
    }
}

 SRP - Solution

  • Person should hold its data only
  • Extract exporter class
public class Person {
    public string FirstName { get; set; }
    public string LastName { get; set; }    
}
public class PersonExporter {
    public object Export(string format, string? location = null) {
        switch(format) {
            case "Excel":
                excel.Save(location);
                break;
            case "JSON":
                return jsonString;
                break;
            default:
                File.WriteAllText(location.Value, 
                $"{person.FirstName} {person.LastName}.");
        }
    } 
}

Open/Closed

 Open/Closed

  • You should be able to extend/add functionaly by adding new classes and/or function while NOT changing the inners of your existing classes or functions.
     
  • Design so that
    • software is open to extension and new behaviour can be added in the future,
    • but closed to modification to changes to source or binary code are not required.

 OCP - Classic violations

  • Each change requires re-testing (possible bugs)
  • Cascading changes through modules
  • Logic depends on conditional statements
public class PersonExporter {
    public object Export(string format, string? location = null) {
        switch(format) {
            case "Excel":
                excel.Save(location);
                break;
            case "JSON":
                return jsonString;
                break;
            default:
                File.WriteAllText(location.Value, $"{FirstName} {LastName}.");
        }
    } 
}

 OCP - Solution

  •  Each class inheriting from IPersonFormatter will then implement it's own logic in the
interface IPersonFormatter<TReturnValue> {
    TReturnValue Format(Person person, string? location);
}
public class JSONPersonFormatter : IPersonFormatter<string> {
    public string Format(Person person, string? location) {
        // ignoring location and executing json formatting logic
        return jsonString;
    }
}
public class XMLPersonFormatter : IPersonFormatter<string> {
    public string Format(Person person, string? location) {
        // ignoring location and executing xml formatting logic
        return xmlString;
    }
}
public class PersonExporter {
    public object Export(Person person, IPersonFormatter formatter, string? location) {        
        return formatter.Format(person, location);
    }
}

Liskov Substution

 Liskov Substitution

  • If it looks like a duck, quacks like a duck and walks like a duck, it's probably a duck;
    • unless the other duck either needs batteries - then it's probably not the right abstractions
    • or unless the other duck sometimes relies on other ducks to fly, or flies on it'sown - then it's probably not the right abstraction as well

 Liskov Substitution

  • Child classes must not remove base class behaviour, nor violate base class invariants.
  • Code using the abstractions should not know they are different from their base types.

 LSP - Classic violations

  • Type checking for different methods
  • Not implemented overridden methods
  • Virtual methods in constructor
public class XMLPersonFormatter : IPersonFormatter<string> {
    public string Format(Person person, string? location) {
        // ignoring location and executing xml formatting logic
        return xmlString;
    }
}
public class ExcelPersonFormatter : IPersonFormatter<object> {
    public object Format(Person person, string? location) {
        // excel specific rendering and saving to location here        
        excel.Save(location);
        // we return null, as we don't have anything to return...
        return null;
    }
}

 LSP - Solution

  •  "Tell, Don't Ask"
    • Don’t ask for types
    • Tell the object what to do
public interface IPersonFormatter {
    Stream Format(Person person);
}
  • One possible change, there could be other solutions, would be to make a more reliable return value for the Format method.
  • So that IPersonFormatter's Format method always returns a stream (or another in memory form) of the data

 LSP - Solution

public class XMLPersonFormatter : IPersonFormatter {
    public Stream Format(Person person) {
        // ignoring location and executing xml formatting logic
        // saving it to a MemoryStream when working in .NET for example
        return xmlStringMemoryStream;
    }
}
public class ExcelPersonFormatter : IPersonFormatter {
    public Stream Format(Person person) {
        // excel specific rendering and saving to location here        
        excel.Save(location);
        // we now don't return null, but a MemoryStream 
        // we don't have anything to return...
        return excelWorkBookMemoryStream;
    }
}

 LSP - Solution

public class PersonExporter {
    public Stream Export(
            Person person,
            IPersonFormatter formatter,
            string? location) {        
        var stream = formatter.Format(person);
        if(location.HasValue) {
            // Handle saving the stream to given location/file            
        }
        // we return the stream - even if we already saved it
        return stream;
    }
}
  • As now all implementations of IPersonFormatter return a predicatable Stream object, we can change the design of the PersonExporter class so that it handles the streams based on a given location.

Interface Segregation

By Mecho Puh

 Interface Segregation

  • No client should be forced to depend on methods it does not use, implicitely implying that a client should never be forced to implement an interface it doesn't use.
  • Prefer small cohesive interfaces, to large bulky interfaces.

 ISP - Classic violations

  • Unimplemented methods (also in LSP)
  • Use of only small portion of a class
public interface IPersonRepository {
    void Save(Person person);
    void Save(IEnumerable<Person> people);
    IEnumerable<Person> Get();
}
public class PersonReadOnlyRepository : IPersonRepository {
    public void Save(Person person) {
        throw NotImplementedException();
    }
    public void Save(IEnumerable<Person> people) {
        throw NotImplementedException();
    }
    public IEnumerable<Person> Get() {
        return database.People.ToEnumerable();
    }
}

 ISP - Solution

  • Seperate a larger bulky interface into smaller interfaces.
public interface IWriteablePersonRepository {
    void Save(Person person);
    void Save(IEnumerable<Person> people);
}

public interface IReadablePersonRepository {
    IEnumerable<Person> Get();
}

public class PersonReadOnlyRepository : IReadablePersonRepository {
    public IEnumerable<Person> Get() {
        return database.People.ToEnumerable();
    }
}

Dependency Inversion

 Dependency Inversion

  • High-level modules should not depend on low-level modules, both should depend on abstractions. Abstractions should not depend on details, details should depend on abstractions.
  • Class constructors, or (public/proteced) methods, should require any dependencies. And let you know what they need to do it's work.

 DIP - Classic violations

  • Using of the new keyword
  • Using static methods/properties
public class PersonRepository : IReadablePersonRepository {

    public IEnumerable<Person> Get() {
        var db = new DatabaseContext()
        return db.People.ToEnumerable();
    }
}

 DIP - Solution

  • Default constructor
  • Main method/starting point
  • Inversion of Control container (will be discussed later)
public class PersonRepository : IReadablePersonRepository, IWriteablePersonRepository {
    private readonly db;

    public PersonRepository(IDatabaseContext db) {
        this.db = db;
    }
    public IEnumerable<Person> Get() {
        this.db.People.ToEnumerable();        
    }
}

Other Principles

Don't Repeat Yourself

(DRY)

 DRY

  • This principle is so important to understand, that I won't write it twice!
  • When you are building a large software project, you will usually be overwhelmed by the overall complexity.
  • Humans are not good at managing complexity; they're good at finding creative solutions for problems of a specific scope.

 DRY - Classic violations

  • Magic Strings/Values
  • Duplicate logic in multiple locations
  • Repeated if-then logic
  • Conditionals instead of polymorphism
  • Repeated Execution Patterns
  • Lots of duplicate, probably copy-pasted, code
  • Only manual tests
  • Static methods everywhere

You Ain't Gonna Need It

(YAGNI)

 YAGNI

  • 80% of the time spent on a software project is invested in 20% of the functionality.
  • It basically translates to: If it's not in the concept, it's not in the code.

 YAGNI - Disadvantages

  • Time for adding, testing, improving
  • Debugging, documented, supported
  • Difficult for requirements
  • Larger and complicate software
  • May lead to adding even more features
  • May be not know to clients

Keep It Simple Stupid

(KISS)

 KISS

  • The simplest explanation tends to be the right one.
  • Double-check the requirements whether they are really stripped down to the essence that the client needs.
  • Take the time to discuss critical points and explain why other solutions might be more suitable.

Questions?

[C# HQC] S.O.L.I.D. Principles

By telerikacademy

[C# HQC] S.O.L.I.D. Principles

  • 1,123