- function signature should be small- the fewer the arguments the better
- what types should be passed into those arguments
- why switch and if cost such harm in the software structure ? -> how to get rid of them ?
- why assign operator is consider harmful ?
- Many software problems can be avoided by constraining state changing operators, and side effects.
- How to structure our functions making error handling clean and maintainable fashion ?
Arguments are hard to read and hard to understand, each one can break your flow if you are reading down the code each one can cost you to do a doble take they generate confusion The Rule is use as less arguments as possible.
If you have three is hard to remember the order. If three variables are so cohesive than need to be passed together into a function why are not an object ?
It implies that the function does two things! Instead you should write two functions, one for the true case and one for the false case Passing in two booleans is even worse, two booleans will cost you a double take indeed.
Don't use output arguments When the reader see an argument he expect that the argument is passing data into the function. If reader see an output param he will need to double take.
Passing null into a function, or writing a function that expects null to be passed in is almost the same than passing a boolean. Even worse it is not obious that there are two possible states. Separate the behaviour for the null case and the behaviour for the not null case into two functions. Deffensive programming is null checks and error checks. Deffensive programming is a smell Deffensive programming means that you don't trust your team or unit tests. If you are constantly checking if your arguments are null that means that your unit tests are not preventing you from passing those null.
In public api, deffensive programming makes sense because who knows what people will pass in?
Every public function call private children functions, which again call their children functions. They are ordered in the order they are called and in the order of the herachy. There are no backward reference. So you ever read scrolling down.
Switch statements are a missed opportunity to use polymorphism
Swtich statements are not oo.But what is so great about oo anyway ?
Big benfit from OO is the ability to manage dependencies.
If module A
calls a function in module B
, that means that there is a dependency between them.
Also we need to declare that dependency using an import
within module A
ModuleA -----> ModuleB
runtime dependency
In runtime in order to get the ModuleA running we need to have ModuleB loaded in memory.
In compile time, This source code dependency
implies that they cannot be deployed separately.
Every change made on module B will force a re-compile and re-deployment of module A.
OO Give us a technique to invert the source code dependency
and leave the runtime dependency
alone.
Introducing a polimorphic interface between them.
Module A ---> I <--implements-- Module B
This change the source code dependency
This allow modules A and B to be deployed separately
Module B can be plugged in into Module A
There is no need to add import Module B
within Module A
Independent deployability is just one good thing about OO
Switch statement is the antithesis of independent deployability.
Each case within the switch is likely to have a dependency on an external module.
In a switch statement the source code dependency points in the same direction as the flow of control.
That means that if any of the switch statements depends on all the downstream modules.
If any of the downstream modules change there is an impact on the switch and anything that use it.
In other words it creates a knot of dependencies that makes the independent deployability
virtually impossible.
To solve this we have two options
1- Apply polymorphism to invert the dependency. Take the argument of the switch and replace it with an abstract base class that has a methods that corresonds to the methods being performed by the switch. After replacing a swtich with several classes which inherits from base_class we need to figure out where and how create those instances. Tipically we create those instances within factories. In every application we should be able to draw a line which separates the core app functionality from the low leve details. Main and App partition Main partition should be small Main contains factories, configuration data, the main program. The Application partition is subdivided into different submodules The dependencies between this two partitions should point in one direction and one direction only They should cross the line pointing towards the application The main partition should depend on the application The application have no dependencies backwards to its main Main is a plugin to the application This is a technique called dependency injection. The trick of dependency injection is carefully define and maintain your partiotioning
2- Move this switch to a place where it cannot hurt. Switch statement that live in the main partition usually do not harm. Nothing that happens in the app partition can affect the main partition. Any change on the cases wont affect the switch. Main and App partition remains independently deployable and the switch statements in the main do not harm.
The goal of this is to build a system composed of several independent deployable modules. But we usually deploy all of them toghether so,, what is the point of this ? A system which is independently deployable is also independently developable.
Switch statements breaks the structure that we desired for our application.
- No assignment statement
- Instead of setting a bunch of values into variables you pass them as arguments into function
- Instead of looping a set of variables you recurse
- the returned value of a function depends only on its arguments and not in other state of the system
- No side effects
When a function change a variable that outlives without it then that function has a side effect.
And that side effect may change the behaviour of the function(or other function) on future calls.
This is a persistence source of errors.
Often those side effects functions comes in pairs (open-close) (set-get) (new-delete)
One of them needs to be called before the other.
Having a temporal coupling
How do we eliminate temporal coupling ?
By passing a block.
function myOpen(file, fileCommand) {
file.open();
fileCommand.process(file);
file.close();
}
Our goal is have discipline about where and how those side effects happen.
Command -> changes the state of the system, it has side effects. It returns nothing.
Query -> does not change the state of the system. It returns the value of a computation or the state of the system.
Example (getters and setters).
This rule of returns
for commands and queries have some advantanges.
It is easy to recognize wether or not a function has side effects.
If returns void then has side effects. If returns something then do not have side effects.
We should tell objects what to do but not ask them what the state is. Decisions based on the state should be within the object. The object knows its own state and can make their own decisions.
This rule avoids that query functions get out of control. An object with to many query functions is not encapsulating anything. It is a bad idea to make a single function to know the entire navigation of the system like this:
o.getX().getY().getZ().doSomething();
It couples the function to too much of the hole system. The law of demeter formalizes tell don't ask with these set of rules You may call methods of objects that are: 1 - Passed as arguments 2 - Created locally 3 - Instance variables 4 - Globals You may not call method on objects that are:
- Returned from a previous method call
Any function that tells instead of ask is decupled from it surrounding.
Every algorithm should be composed out of three basic operations
- Sequence The exit of the first block fits the entry of the second
- Selection Its a boolean expression that breaks the flow of control into two path ways
- Iteration Repeated execution of a block until some boolean expression is satified
Dijstra shows that you can construct proof of correctness under this contraints(without using go-to). A probable system is also an understandable system. Easy to reason about.
return in the middle of a loop violates the single input-single output rule. break in the middle of a loop also creates an indirective unexpressed exit condition. Also makes our code harder to understand.
Error handling is important but if it obscures the logic it is wrong. So, how to manage our errors without obscuring the logic ?
Buildig the Stack class we are worried about two possible errors Underflow
, Overflow
We write one test for each.
Avoid returning error codes on the implementation.
Throw your own exceptions, declared under the scope of your class.
And name it whit as much specific information as possible.
Checked exceptions verify that are being handled at compile time, so if you throw checked exceptions your are forcing the client code to manage exceptions.
Is prefered to use unchecked exceptions.
Use precise names, context and scope for exceptions so no messages are needed.
What happen with zero size stack ? should it throw an error when poping, pushing ? It turns out that a zero size stack does have a defined behaviour. Instead of adding ifs everywhere in the code lets create an empty stack class which inherits from base stack and implements its special behaviour.(Null object pattern)
what should return top method if the stack is empty ?
nobody who calls the method expects a null as return
this null slides into the system until eventually it causes a null pointer exception
So we could throw a new StackEmptyException
What happen when calling find
it does not find anything. what should return ?
Java conventions says you can return -1 but it is an intenger not Nothing.
In this case we could return null
A function should do only one thing and error handling is one thing.
nothing here ??
Thanks for taking the time to write this out. I was searching for one of his points and came across this excellent resource.