S.O.L.I.D
Telerik Academy Alpha
HQC
Table of contents
-
S.O.L.I.D Principles
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?
- What you know about SOLID
- 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,109