Skip to main content
deleted 2142 characters in body
Source Link
KeithS
  • 22.2k
  • 6
  • 53
  • 80

Now, that being said, religious adherence to SRP can lead to "ravioli code"; code in such small "bite-size chunks" that navigation through it becomes difficult as you jump from method to method, class to class, each with only two or three lines of very segregated, single-purpose code. But, when designing a class, the first question is always "what am I expecting this class to do?", followed by "is there any other class that does any of this?".

By following this principle, you increase the testability of your design by decreasing the number of tests you have to write that test the same functionality on different objects, and you also typically end up with smaller pieces of functionality that are easier to test in isolation.

O - Open/Closed Principle: A class should be open to extension, but closed to change. Once an object exists and works correctly, ideally there should be no need to go back into that object to make changes that add new functionality. Instead, the object should be extended, either by deriving it or by plugging new or different dependency implementations into it, to provide that new functionality. This avoids regression; you can introduce the new functionality when and where it is needed, without changing the behavior of the object as it is already used elsewhere.

Now, it is impossible to make a system that is completely closed to all types of change. There will always be a need, somewhere, sometime, to go in and change lines of code in a working class. However, you should generally be thinking, when designing classes, "how might the expected behavior of this object need to change, and how can I avoid having to open up the class to make those changes?".

By adhering to this principle, you generally increase the code's ability to tolerate "mocks" (which are often implemented as proxies which dynamically derive from an actual object), and you also avoid having to rewrite tests to anticipate new behavior; all existing tests for an object should still work on the un-extended implementation, while new tests for new functionality using the extended implementation should also work.

L - Liskov Substitution Principle: A class A, dependent upon class B, should be able to use any X:B without knowing the difference. This basically means that anything you use as a dependency should have similar behavior as seen by the dependent class. Let'sAs a short example, say you have an interface IWriter, which has a method interface that exposes Write(string) and, which is implemented by ConsoleWriter which outputs the string to the console. Now, a requirement is added that the output also be written you have to write to a file. So instead, so you create FileWriter. In doing so, you must take care to ensuremake sure that code dependent on IWriter does not need to knowFileWriter can be used the same way ConsoleWriter did (meaning that the only way the dependent can interact with it is working with a FileWriter and not a ConsoleWriter. Soby calling Write(string)), whileand so additional information that FileWriter mightmay need to know whatdo that job (like the path and file it must write to, it cannot expect its dependent class write to give it that information; the information) must comebe provided from somewhere else before it's given to a dependent. Why? Becausethan the dependent only knows about the Write(string) method, and cannot know about any properties or methods that FileWriter must expose in order to obtain its implementation-specific settings.

I - Interface Segregation Principle: An interface should define only thehave as few methods needed by its dependentsas is feasible to performprovide the functionality of the role defined by the interface. Simply put, more smaller interfaces are better than fewer larger interfaces. This is because a large interface has more reasons to change, and causes more other changes.

Take an interface IDoThreeThings, which exposes DoOneThing(), DoAnotherThing(), and DoAThirdThing(). All implementations of this interface are required to expose these three methods (and by LSP, all implementations should look the same to the dependent meaning any return values should be meaningful). However, say that not all of the dependent usages of this interface need to call all three methods; each usage may, elsewhere in fact, need only one. If a change is needed to DoAThirdThing() (say to add a parameter), the interface must change, all implementations must change, and all dependents must at least be recompiled, even those that never call DoAThirdThing(), to ensure that at the binary level they are pointing to the correct memory location of the methods they do call. To avoid this scenario, it's recommended that any methodscodebase that are required to be present but can be used independently shouldmay not be placed in a different interface: IDoOneThing, IDoAnotherThing, and IDoAThirdThing, each exposing one of the methods. One class can implement all three (but you should carefully consider whether that's prudent according to SRP), but each dependency can require only the method(s) it will call at any given timenecessary.

ThisAdherence to ISP improves testability by reducing the complexity of systems under test and of dependencies of those SUTs. If the object you are testing depends on an interface IDoThreeThings which exposes DoOne(), DoTwo() and DoThree(), you must mock an object that implements all three methods even if the object only uses the DoTwo method. But, if the object depends only on IDoAnotherThingIDoTwo (which exposes only DoTwo), you can more easily mock an object that has that one method.

D - Dependency Inversion Principle: Concretions and abstractions should never depend on other concretions, but on abstractions. This principle directly enforces the tenet of loose coupling. An object should never have to know what an object IS; it should instead care what an object DOES. So, the use of interfaces and/or abstract base classes is always to be preferred over the use of concrete implementations when defining properties and parameters of an object or method. That allows you to swap one implementation for another without having to change the usage (if you also follow LSP, which goes hand in hand with DIP).

Now, that being said, religious adherence to SRP can lead to "ravioli code"; code in such small "bite-size chunks" that navigation through it becomes difficult as you jump from method to method, class to class, each with only two or three lines of very segregated, single-purpose code. But, when designing a class, the first question is always "what am I expecting this class to do?", followed by "is there any other class that does any of this?".

By following this principle, you increase the testability of your design by decreasing the number of tests you have to write that test the same functionality on different objects, and you also typically end up with smaller pieces of functionality that are easier to test in isolation.

O - Open/Closed Principle: A class should be open to extension, but closed to change. Once an object exists and works correctly, ideally there should be no need to go back into that object to make changes that add new functionality. Instead, the object should be extended, either by deriving it or by plugging new or different dependency implementations into it, to provide that new functionality. This avoids regression; you can introduce the new functionality when and where it is needed, without changing the behavior of the object as it is already used elsewhere.

Now, it is impossible to make a system that is completely closed to all types of change. There will always be a need, somewhere, sometime, to go in and change lines of code in a working class. However, you should generally be thinking, when designing classes, "how might the expected behavior of this object need to change, and how can I avoid having to open up the class to make those changes?".

By adhering to this principle, you increase the code's ability to tolerate "mocks" (which are often implemented as proxies which dynamically derive from an actual object), and you also avoid having to rewrite tests to anticipate new behavior; all existing tests for an object should work, while new tests for new functionality should also work.

L - Liskov Substitution Principle: A class A, dependent upon class B, should be able to use any X:B without knowing the difference. This basically means that anything you use as a dependency should have similar behavior as seen by the dependent class. Let's say you have an interface IWriter, which has a method Write(string) and is implemented by ConsoleWriter which outputs the string to the console. Now, a requirement is added that the output also be written to a file. So, you create FileWriter. In doing so, you must take care to ensure that code dependent on IWriter does not need to know that it is working with a FileWriter and not a ConsoleWriter. So, while FileWriter might need to know what file it must write to, it cannot expect its dependent class to give it that information; the information must come from somewhere else before it's given to a dependent. Why? Because the dependent only knows about the Write(string) method, and cannot know about any properties or methods that FileWriter must expose in order to obtain its implementation-specific settings.

I - Interface Segregation Principle: An interface should define only the methods needed by its dependents to perform the role defined by the interface. Simply put, more smaller interfaces are better than fewer larger interfaces. This is because a large interface has more reasons to change, and causes more other changes.

Take an interface IDoThreeThings, which exposes DoOneThing(), DoAnotherThing(), and DoAThirdThing(). All implementations of this interface are required to expose these three methods (and by LSP, all implementations should look the same to the dependent meaning any return values should be meaningful). However, say that not all of the dependent usages of this interface need to call all three methods; each usage may, in fact, need only one. If a change is needed to DoAThirdThing() (say to add a parameter), the interface must change, all implementations must change, and all dependents must at least be recompiled, even those that never call DoAThirdThing(), to ensure that at the binary level they are pointing to the correct memory location of the methods they do call. To avoid this scenario, it's recommended that any methods that are required to be present but can be used independently should be placed in a different interface: IDoOneThing, IDoAnotherThing, and IDoAThirdThing, each exposing one of the methods. One class can implement all three (but you should carefully consider whether that's prudent according to SRP), but each dependency can require only the method(s) it will call at any given time.

This improves testability by reducing the complexity of systems under test and of dependencies of those SUTs. If the object you are testing depends on IDoThreeThings, you must mock an object that implements all three methods. But, if the object depends only on IDoAnotherThing, you can more easily mock an object that has that one method.

D - Dependency Inversion Principle: Concretions and abstractions should never depend on other concretions, but on abstractions. This principle directly enforces the tenet of loose coupling. An object should never have to know what an object IS; it should instead care what an object DOES. So, the use of interfaces and/or abstract base classes is always to be preferred over the use of concrete implementations when defining properties and parameters of an object or method. That allows you to swap one implementation for another without having to change the usage (if you also follow LSP).

By following this principle, you increase the testability of your design by decreasing the number of tests you have to write that test the same functionality on different objects, and you also typically end up with smaller pieces of functionality that are easier to test in isolation.

O - Open/Closed Principle: A class should be open to extension, but closed to change. Once an object exists and works correctly, ideally there should be no need to go back into that object to make changes that add new functionality. Instead, the object should be extended, either by deriving it or by plugging new or different dependency implementations into it, to provide that new functionality. This avoids regression; you can introduce the new functionality when and where it is needed, without changing the behavior of the object as it is already used elsewhere.

By adhering to this principle, you generally increase the code's ability to tolerate "mocks", and you also avoid having to rewrite tests to anticipate new behavior; all existing tests for an object should still work on the un-extended implementation, while new tests for new functionality using the extended implementation should also work.

L - Liskov Substitution Principle: A class A, dependent upon class B, should be able to use any X:B without knowing the difference. This basically means that anything you use as a dependency should have similar behavior as seen by the dependent class. As a short example, say you have an IWriter interface that exposes Write(string), which is implemented by ConsoleWriter. Now you have to write to a file instead, so you create FileWriter. In doing so, you must make sure that FileWriter can be used the same way ConsoleWriter did (meaning that the only way the dependent can interact with it is by calling Write(string)), and so additional information that FileWriter may need to do that job (like the path and file to write to) must be provided from somewhere else than the dependent.

I - Interface Segregation Principle: An interface should have as few methods as is feasible to provide the functionality of the role defined by the interface. Simply put, more smaller interfaces are better than fewer larger interfaces. This is because a large interface has more reasons to change, and causes more changes elsewhere in the codebase that may not be necessary.

Adherence to ISP improves testability by reducing the complexity of systems under test and of dependencies of those SUTs. If the object you are testing depends on an interface IDoThreeThings which exposes DoOne(), DoTwo() and DoThree(), you must mock an object that implements all three methods even if the object only uses the DoTwo method. But, if the object depends only on IDoTwo (which exposes only DoTwo), you can more easily mock an object that has that one method.

D - Dependency Inversion Principle: Concretions and abstractions should never depend on other concretions, but on abstractions. This principle directly enforces the tenet of loose coupling. An object should never have to know what an object IS; it should instead care what an object DOES. So, the use of interfaces and/or abstract base classes is always to be preferred over the use of concrete implementations when defining properties and parameters of an object or method. That allows you to swap one implementation for another without having to change the usage (if you also follow LSP, which goes hand in hand with DIP).

Source Link
KeithS
  • 22.2k
  • 6
  • 53
  • 80

Yes, SOLID is a very good way to design code that can be easily tested. As a short primer:

S - Single Responsibility Principle: An object should do exactly one thing, and should be the only object in the codebase that does that one thing. For instance, take a domain class, say an Invoice. The Invoice class should represent the data structure and business rules of an invoice as used in the system. It should be the only class that represents an invoice in the codebase. This can be further broken down to say that a method should have one purpose and should be the only method in the codebase that meets this need.

Now, that being said, religious adherence to SRP can lead to "ravioli code"; code in such small "bite-size chunks" that navigation through it becomes difficult as you jump from method to method, class to class, each with only two or three lines of very segregated, single-purpose code. But, when designing a class, the first question is always "what am I expecting this class to do?", followed by "is there any other class that does any of this?".

By following this principle, you increase the testability of your design by decreasing the number of tests you have to write that test the same functionality on different objects, and you also typically end up with smaller pieces of functionality that are easier to test in isolation.

O - Open/Closed Principle: A class should be open to extension, but closed to change. Once an object exists and works correctly, ideally there should be no need to go back into that object to make changes that add new functionality. Instead, the object should be extended, either by deriving it or by plugging new or different dependency implementations into it, to provide that new functionality. This avoids regression; you can introduce the new functionality when and where it is needed, without changing the behavior of the object as it is already used elsewhere.

Now, it is impossible to make a system that is completely closed to all types of change. There will always be a need, somewhere, sometime, to go in and change lines of code in a working class. However, you should generally be thinking, when designing classes, "how might the expected behavior of this object need to change, and how can I avoid having to open up the class to make those changes?".

By adhering to this principle, you increase the code's ability to tolerate "mocks" (which are often implemented as proxies which dynamically derive from an actual object), and you also avoid having to rewrite tests to anticipate new behavior; all existing tests for an object should work, while new tests for new functionality should also work.

L - Liskov Substitution Principle: A class A, dependent upon class B, should be able to use any X:B without knowing the difference. This basically means that anything you use as a dependency should have similar behavior as seen by the dependent class. Let's say you have an interface IWriter, which has a method Write(string) and is implemented by ConsoleWriter which outputs the string to the console. Now, a requirement is added that the output also be written to a file. So, you create FileWriter. In doing so, you must take care to ensure that code dependent on IWriter does not need to know that it is working with a FileWriter and not a ConsoleWriter. So, while FileWriter might need to know what file it must write to, it cannot expect its dependent class to give it that information; the information must come from somewhere else before it's given to a dependent. Why? Because the dependent only knows about the Write(string) method, and cannot know about any properties or methods that FileWriter must expose in order to obtain its implementation-specific settings.

This is huge for writing testable code, because a design that conforms to the LSP can have a "mocked" object substituted for the real thing at any point without changing expected behavior, allowing for small pieces of code to be tested in isolation with the confidence that the system will then work with the real objects plugged in.

I - Interface Segregation Principle: An interface should define only the methods needed by its dependents to perform the role defined by the interface. Simply put, more smaller interfaces are better than fewer larger interfaces. This is because a large interface has more reasons to change, and causes more other changes.

Take an interface IDoThreeThings, which exposes DoOneThing(), DoAnotherThing(), and DoAThirdThing(). All implementations of this interface are required to expose these three methods (and by LSP, all implementations should look the same to the dependent meaning any return values should be meaningful). However, say that not all of the dependent usages of this interface need to call all three methods; each usage may, in fact, need only one. If a change is needed to DoAThirdThing() (say to add a parameter), the interface must change, all implementations must change, and all dependents must at least be recompiled, even those that never call DoAThirdThing(), to ensure that at the binary level they are pointing to the correct memory location of the methods they do call. To avoid this scenario, it's recommended that any methods that are required to be present but can be used independently should be placed in a different interface: IDoOneThing, IDoAnotherThing, and IDoAThirdThing, each exposing one of the methods. One class can implement all three (but you should carefully consider whether that's prudent according to SRP), but each dependency can require only the method(s) it will call at any given time.

This improves testability by reducing the complexity of systems under test and of dependencies of those SUTs. If the object you are testing depends on IDoThreeThings, you must mock an object that implements all three methods. But, if the object depends only on IDoAnotherThing, you can more easily mock an object that has that one method.

D - Dependency Inversion Principle: Concretions and abstractions should never depend on other concretions, but on abstractions. This principle directly enforces the tenet of loose coupling. An object should never have to know what an object IS; it should instead care what an object DOES. So, the use of interfaces and/or abstract base classes is always to be preferred over the use of concrete implementations when defining properties and parameters of an object or method. That allows you to swap one implementation for another without having to change the usage (if you also follow LSP).

Again, this is huge for testability, as it allows you, once again, to inject a mock implementation of a dependency instead of a "production" implementation into your object being tested, while still testing the object in the exact form it will have while in production. This is key to unit testing "in isolation".