Skip to content

Instantly share code, notes, and snippets.

@delbetu
Last active February 11, 2024 17:28
Show Gist options
  • Save delbetu/3fed29d1f56088938b019bc3b99bba72 to your computer and use it in GitHub Desktop.
Save delbetu/3fed29d1f56088938b019bc3b99bba72 to your computer and use it in GitHub Desktop.
Uncle Bob best practices function structure

Intro

  • 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 ?

Function arguments

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.

Three arguments max

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 ?

No boolean arguments ever

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.

Innies not outies

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.

The null defense

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?

Stepdown rule

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.

Switches and cases

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.

The Fan Out Problem

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.

Paradigms

Functional

  • 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

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 - Query separation

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.

Tell - Don't ask

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.

Structured

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 ?

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.

Special case

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)

Null is not an error

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

Try is one thing

A function should do only one thing and error handling is one thing.

OO

nothing here ??

@kennyray
Copy link

Thanks for taking the time to write this out. I was searching for one of his points and came across this excellent resource.

@delbetu
Copy link
Author

delbetu commented Dec 30, 2020

Thanks for taking the time to write this out. I was searching for one of his points and came across this excellent resource.

🤟

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment