Actually, this is reply to comment made by Ed, but since its big, I decided to add it as new post. Just changed it a bit.
Ed asked if I can give any meaningfull definition of simplicity.
No, I cannot give any clear and concise definition and I agree that it’s not easy thing to do, especially if we look from perspective of software development.
But, we can think about it for a moment.
We have three aspects to consider at the same time: simplicity of solution / complexity of requirements / maintability over time.
So, for simple requirements, simplicity can (and should mean) mean: to implement solution in a straightforward way,
by using only basic language constructs with minimum amount of code and encapsulated in such a way, that it is easy
to understand it and maintain it.
But, as complexity of requirements rises, we should use appropriate higher-level abstractions.
We will probably have many classes and interfaces with different specialties and they all are going to be orchestrated in some deliberate way.
And, this is where ’simplicity’ becomes relative meaning.
Not to stay only on words, I came up with small example.
This is it:
We need to implement few checks before we can allow bus driver to hit the throttle and start driving. It’s very nice and smart bus.
Checks should be:
- Are all doors closed properly?
- Is engine in good shape (oil and water temperature)?
- Do Tires sensors return ‘all ok’?
- If we are in mod ‘Passengers are children’, check if all passengers’ seat belts are used in occupied seats.
All check results are obtained by external components which we use in our app in a form of interfaces and we obtain it through some manager/controller class.
Solution No. 1:
<code>
public bool RunChecksBeforeBusStarts()
{
IDoors doors = DevicesController.GetDoors();
foreach(IDoor door indexer doors)
{
if (door.Closed == false)
{
return false;
}
}
IEngine engine = DevicesController.GetEngine();
if (engine.OilTemperatureStatus != NOMINAL)
{
return false;
}
if (engine.WaterTemperatureStatus != NOMINAL)
{
return false;
}
ITiresController tiresController = DeviceController.GetTiresController();
if (tiresController.Status != OK)
{
return false;
}
if (application.PassengerType = CHILDREN)
{
ISeatsController seatsController = DeviceController.GetSeatsController();
ISeats occupiedSeats = seatsController.GetOccupiedSeats();
foreach(ISeat seat indexer occupiedSeats)
{
if (seat.SeatBeltStatus != FASTENED)
{
return false;
}
}
}
}
</code>
I hope we can agree that it is simple enough and only refactoring step we can do here is to do something like this:
Solution No. 2:
<code>
public bool RunChecksBeforeBusStarts()
{
if (AreDoorsClosed() == false)
return false;
if (AreEngineParametarsNominal() == false)
return false;
if (AreTyresStatusOk() == false)
{
return false;
}
if (application.PassengerType = CHILDREN)
{
if (AllOccupiedSeatsAreFastened() == false)
return false;
}
}
</code>
We just clearly stated what we are doing here and delegated specific checks to atomic methods.
So, above two examples are simple and we only used basic constructs (no big abstractions). This solution is fine by me (hopefully you are satisfied also).
Now it becomes interesting…we are required to add 10-15 more checks. On top of that, we can even be required to
handle things in different way, depending on category to which specific check belongs (also new requirement) etc.
My experience is enough for me to recognize immediately I simply cannot continue expanding No. 1 or No. 2 solutions, since:
- Cyclomatic complexity is going to hit a roof
- Main check method will become huge, even if we use Compose /Atomic methods
- Maintainability is going to be nightmare for anybody, even me (the implementer)
That is why I am going to do something like this:
<code>
/// Each specific check implementation will have to inherit from this interface
interface ICheck
{
bool Check();
}
/// Base manager for any sort of checks
internal class Checks
{
internal BaseChecks()
{
_checkStorage = new List();
}
private List _checkStorage;
protected void AddCheck(ICheck newCheck)
{
_checkStorage.Add(newCheck);
}
protected bool RunChecks()
{
return _checkStorage.TrueForAll(RunSingleCheck);
}
}
/// Our specific check situation
class BusPreRunChecker : Checks
{
public bool RunAllChecks()
{
InitializeAllChecks();
RunChecks();
}
public bool InitializeAllChecks()
{
AddCheck(new EngineCheck());
….
….
}
}
</code>
And, I am going to use it in following way:
<code>
BusPreRunChecker checker = new BusPreRunChecker();
If (checker.RunAllChecks() == false) –> disable throttle and signal to driver..
</code>
From current perspective, it is elegant enough.
Complexity of the whole solution is dispersed in small and specialized ICheck inherited classes.
Now, it is easy enough to:
- Maintain – we can now rearrange test execution order, throw out tests, add new ones etc.
- Trace possible bugs – if bug is in one test, that’s in exactly one check class etc.
- Cover it with unit tests – each test should depend only on input parameters and that’s it.
But, we cannot call No. 3 solution ‘the simplest solution’. Especially if we add everything we need to quality it as ‘production code’ etc…
The simplest solutions are No 1. and No 2. from one perspective, because they are understandable to even novice developers.
(Of course, if we use No1. And No2. for second set of requirements, they are going to understand it only in parts,
as anybody else, because original intent of the whole solution is going to be lost for good.
)
From other perspectives like, maintainability of solution, flexibility etc. No.3 is a sure winner.
What principles can we recognize in solution No. 3:
- Single Responsibility Principle
BusPreRunChecker – manages checks in every conceivable way
Inherited ICheck classes – encapsulate knowledge of single check - Open/Closed principle
Next developer in line can only continue expanding solution in a very narrow way, the way original implementer
envisioned it. Original design and idea behind it should survive for long time. - Liskov Substitution Principle
All operations done on instances of ICheck in BusPreRunChecker can be done on all inherited classes of ICheck.
- If we rearrange solution a bit and use CheckFactory from Checker class to obtain all necessary check we need to
run, our checker class would only have dependency on IChecks and will not have any knowledge of concrete check classes.
That would mean we conform to Dependency Inversion Principle.
Now, the only question is, did we come to No. 3 solution by watching closely all the principles etc. from some, more formal perspective, or we designed it by
‘Coherent thinking and clear expression of thoughts through design and code’ phrase I used, which stands for iterative work, pounding on how to get things clean(er) etc.
I came to No. 3 solution by second option. And, since it took few mental steps to come from solution No. 1 to solution No. 3, that is what I meant by this sentence.
So, in reality, I designed something which in the end, conformed to some principles, but actual design process was done in ‘loose way’.
Object Oriented Principles I mentioned from above are principles explained by Robert C. Martin.
He explained all of them in one place and in really nice way.
All recommendations I wrote in my first post are, let say, basic and everyday recommendations for easier and safer way of developing software.
They are by no means coherent approach to software design.
You can visit Edmund Kirwan’s site, where you can find papers on encapsulation, coupling etc. You will get the picture when his first website’s page opens.
Happy coding…
Since I decided to write Blog about software development, at least sporadically, it is appropriate to introduce myself.
For last 9 years I am developing business software on Microsoft platform using C++ and in last few years C# programming languages.
In fact, I started programming by using Basic on Commodore 64 long time ago, but probably you don’t want to hear that part… J
Lately, I am trying to keep up with all this frantic Microsoft development of different Frameworks and API’s, by looking at ASP.NET MVC (great stuff), WPF and Silverlight.
It is all very nice and exciting, but something that never ages, and what should be most important if you are developer is software design and writing quality code.
That’s why I decided to write small story about software development from my perspective.
But, back to start of the story. After I started working as a developer and as soon as I passed the phase „am I up to it“, I went to next one, which is „how to write good or even better, a great code, so that I can claim I am professional developer“.
After lot of small TestXXX projects, books on subjects of software design and quality coding, few held presentation on smaller/ bigger local conferences on same subjects, and well, coding itself, I came to following conclusion:
If you can design and implement simplest solution in a given context, you are great developer.
Simplicity, especially if you are working in a team, is No. 1 thing in my book.
And you can’t achieve this without coherent thinking and clear expression of thoughts through design and code. Discipline is required.
For me, that means:
-
No class/methods/functions/variables with non-descriptive names.
It’s not easy sometimes to do this, but it’s of vital importance. Even creator of some code is going to forget soon what exactly it was about. Not to mention others.
If we need a method that is going to fetch all workers who worked on Project1 and not on Project2, then it should be called ‘GetAllWorkersThatWorkedOnProject1AndNotOnProject2′.
Behind the scene, if you have some smart functionality, it can delegate to method called GetWorkers(12). J
-
Methods should never try to ’sell’ us something which is not true.
Method named ‘Get…’ must only do get/read operation of some sort and never, ever change some app state. I saw it more than once…probably you also.
-
No methods with 100+ lines of code. Not to mention beasts with 1000+.
In reality, I rarely or never go over 50 lines per method and 95% of them are between 1 and 25 lines of code, especially now in C#.
This enables me to encapsulate some atomic action, and reuse it next time. So, it’s like building a pyramid.
Atomic methods on the bottom, wanted functionality on top. Between that, semi-smart methods which do some higher functionality, that in turn use those at lower levels.
Person, who so elegantly explain how to do it and why, is Joshua Kerievsky in his Refactoring to Patterns book. In case you haven’t read it, and you are interested in this subject, I highly recommend it.
-
No for->if->for->while->switch monsters in one method.
If actual implementation needs all that, so be it, but I am pretty sure it doesn’t need to be in one method, or even in one class. Break it up…
-
No odd experiments in design, which is going to serve for production.
I saw some strange designs and hand in hand, accompanied implementations (to say it in a polite way).
What helps me if to draw solution by using simple form of UML. My preferred and currently only way is on paper.
In case I see too many entity associations (dependencies) I change the drawing. In case some associations look strange, I change it again.
When I get simplest picture with fewest connections between objects, then I can go to coding.
If nothing else, this will help me escape ’strange’ classification of my design. J
-
I try to avoid ‘Primitive obsession’, whenever possible.
Meaning, you cannot implement something complex by only using bunch of primitive types and perform even more operations on them, can you? Maybe it’s lack of imagination in some folks, I don’t know…
-
No ‘I am going to refactor it next time’. Or, ‘I didn’t have time, don’t you get it?!’
For me it is close to impossible to understand it, since my basic and only tool during coding phase is constant refactoring.
Isn’t it a fact, that the only way you can finalize some implementation, which is even a bit more complex, is to refactor it until it gets clean and human readable.
So, by avoiding all of the things from above, I can reduce complexity, improve clearness of solution, and avoid lot of unnecessary bugs from beginning.
If we climb little bit higher, and escape all this low-level coding stuff, we come to designing the whole solution. Then it is preferable to use all of this world’s accumulated knowledge presented to us in a form of GoF Design Patterns or even higher, Enterprise Patterns. If and when we recognize one during analysisJ.
In case I don’t recognize anything, I am going to try to build solution at hand, just by using encapsulation, separation of concerns and by keeping number of dependencies between entities to a bare minimum. At least or starters…
To understand it in a concuss level, and not only instinctively, I recommend you read Robert C. Martin’s book Agile Principles, Patterns, and Practices in C#, part ‘Object Oriented principles’.
Last by not least, I am trying to communicate my intent to next developer who will use my solution, as much as possible. What I mean is:
-
By using only constructors with exact number of initialization parameters sufficient for a class at hand to function properly.
-
If I have more than few constructors, with different parameters, I am going to declare them non accessible, and implement factory methods which are going to be more explainable, what is different usage of these.
-
If I have some sort of ’smart solution’, which requires that you do something before calling some action, I will usually have check in place for that. I’ll thow exception with text: „You forget to do…“.
-
By using abstract classes, I am telling next developer, what needs to be done, if you want to write new kind of something, in a whole family of classes.
-
By using interface, as an input type in constructor or setter, for some worker or controller class, I am leaving place for different type of implementation of something we are going to use as helper functionality.
-
By implementing as narrow as possible public interface of some class, I try to reduce next developer’s effort in ‘what should I call now’ game.
-
If something can be put behind my implementation facade, it’s going to behind.
What I said here, is the way I work or at least, try to work on day to day basis.
I am always trying to get to the simplest and most maintainable solution, and yes, of course, I don’t always succeed. Who does?
I hope what I wrote here is by your taste…until next time…