Take 2: Why we use SOLID in static languages and how we get the same functionality for cheap in dynamic languages
One of the things we do pretty well at Los Techies is explaining SOLID principles and why they make our code more maintainable and if you’re not familiar with our work on SOLID, read Chad Meyer’s post to get an understanding on the Los Techies perspective. Now that you’ve taken a bit of time to read that the following chart takes SOLID’s benefits and then shows how they apply to dynamic languages:
Principle | Primary Benefit | Secondary Benefit | What dynamic langs do to replace said principle |
Single Responsibility Principle (SRP) | Classes are in understandable units | Testability | Nothing |
Open-Closed Principle (OCP) | Flexibility | Guidance | can override anything globally (monkey patch) |
Liskov Substitution Principle (LSP) | Not bleeding subclass specific knowledge everywhere | Least surprise | Nothing |
Interface Segregation Principle (ISP) | Interfaces are in understandable units | Less work for interface implementation | everything’s an interface (duck typing) |
Dependency Inversion Principle(DIP) | Flexibility | Testability | can override anything globally (monkey patch) |
The above is my opinion and I’m sure people will beat me to death because I’m just some dude not many people know about, but I think I’ve got a grasp of what SOLID means in C# and I’ve grappled with it from a practical perspective.
You’ll note when it comes to LSP and SRP dynamic languages really give you nothing at all, but OCP, ISP, and DI benefits wise are supplanted by language features in python, ruby, etc. How does this happen? In the case of ISP and “duck typing” this should be pretty obvious, but in the cases of OCP, DIP
DIP/OCP testability achieved through C#:
private IFooService _service;
public FooBar(IFooService service){
_service = service;
}
public void DoFoo(){
_service.run();
}
}
//testcode
} </div> </div>
DIP/OCP equivalent testability achieved through Python:
def DoFoo(self):
#test code
class TestFooBar(unittest.TestCase):
def runmock(self):
def test_do_fooservice_is_called(self):
Now rightfully you may say the Python version has a few more lines of code, and it certainly can be considered ugly…but don’t you see that I replaced an entire mocking library in a few lines of code. There are also mocking libraries and monkey patch libraries for Python that would shorten my line of code count further. Testability and flexibility dovetail, but the global accessibility of class definitions is understandably hard to grasp for the static mindset (it took me months), so we’ll continue on to flexibility:
}
public class XmlOutput:IOutput {
public class HtmlOutput: IOutput {
public class TestRunner {
//later on we use IoC containers to make this chain easy
// imagine contextual resolution is happening intelligently
In Python with “monkey patching” everything is “Open For Extension” and dependencies can “always” be injected. A code sample based on my last blog post but extended to include some more complete client code follows:
class XmlOutput(object):
class HtmlOutput(object):
class TestRunner(object):
def run_tests_from_dir(testdirectory, outfile):
#rest of the script
import sys
testdirectory = sys.argv[1]
Again this is longer code than the c# example.. but we’ve replaced an entire IoC container and whatever custom code we would have had to add to create custom dependency resolver for selecting HTML or XML output options has now been replaced by a simple if else if conditional at the beginning of the code base execution. Now all calls in this runtime, from any module, that reference the Output class will use XmlOutput or HtmlOutput instead, all with “monkey patching”.
Hopefully, from here the rest of the dominos will fall into place once you get the concepts above, and the things we rely on SOLID for are in some cases already there in dynamic languages. Consequentially, my dynamic code looks (now) nothing like my static code because I no longer have to use DI/IoC to achieve my desired flexibility and access to use proper unit tests.
[TestFixture]
public class FooBarSpec{
[Test]
public void should_call_fooservice(){
var mockfoo = new Mock
var foobar = new FooBar(mockfoo.object);
foobar.DoFoo();
mockfoo.Verify(foo=>foo.run())
}
</p>
service = FooService()
service.run()
import unittest
self.fooservice_run_was_called = True
originalrun = FooService.run #storing the original method in a variable
FooService.run = self.runmock # I’m globally overriding the method call FooService.run AKA Monkey Patch
FooBar().DoFoo()
FooService.run = originalrun #globally put it back to where it was AKA Monkey Patch
assert self.fooservice_run_was_called
</div> </div>
void save(string fileloc, ResultObjects[] results);</p>
public void save(string fileloc, ResultObjects[] results){
// place xml specific code here
}
public void save(string fileloc, ResultObjects[] results){
// place html specific code here
}
private IOutput _output;
private ITests _tests;
public StoreTestResults(IOutput output, ITests tests){
_output = output;
_tests = tests;
}
public void RunTestsFromDir(string testdirectory, string outfile){
var results = _tests.GetResults(testdirectory);
_output.save(outputfile, results)
}
pubilc static void main(string[] args){
// on the resolver because someone did some cool stuff in windsor
var runner = testcontainer.Resolve
runner.RunTestsFromDir(args[], args[1]);
} </div> </div>
class Output(object):
def save(fileloc, results):
pass</p>
def save(fileloc,results):
“””xml specific logic here”””
pass
def save(fileloc,results):
“””xml specific logic here”””
pass
tests = Tests()
output = Output()
output.save( outfile , tests.get_results(testdirectory)
import outputlib as o
outfile = sys.argv[2]
if outfile.endswith(“xml”):
o.Output = o.XmlOutput #globally override calls to Output with XmlOutput. AKA Monkey Patch
elif outfile.endswith(“html”):
o.Output = o.HtmlOutput #globally override calls to Output with HtmlOutput AKA Monkey Patch
runner = o.TestRunner()
runner.run_tests_from_dir(testdirectory, outfile) </div> </div>