In general, it gives bits of advice around professionalism and indicates that we, as programmers, can shape our paths.
We have agency! The industry gives us opportunity to choose our work environment, field and technologies.
You can change your organization or change your organization.
— Martin Fowler
Take responsibility, be accountable, don't be afraid to admit your errors.
Teams should have an environment based on trust where the members can safely speak their mind, present ideas and rely on each other.
Provide options, don't make lame excuses.
Before you tell why something can't be done, listen to yourself and ask if the excuse sounds reasonable. Try to predict the questions coming from the opposite side. Instead of excuses, provide options. Don't say it why it can't be done; explain what can be done to salvage the situation.
Challenges
Software projects will grow over time and they will obviously have tech debt. However, if we keep the tech debt at bare minimum during the growth phase, it'll be easier to work on the project.
Don't live with broken windows.
A single broken window was the reason of the decay of a whole city. Keep your codebase clean and pretty so the up-comers will pay extra attention to not contaminate it.
Stone soup: the story of hungry soldiers who trick villagers to take out their hoardings by telling them they are cooking a stone soup but it becomes more tasty if we add potato, beef, salad, etc...
Be a catalyst for the change.
The story tries to indicate that if you know what needs to be done but not able to convince people with words, try to build something and give a taste of it.
Boiled frogs: if you throw frogs to a boiling water they'll immediately run away but if you put them into cold water and boil them, they won't.
Remember the big picture.
A software is good enough when it satisfies the expectations of its users. Thus, we should involve users during the decision making process and try to keep efficiency in mind.
Great software today is often preferable to the perfect software tomorrow.
Our knowledge is one of the expiring assets. It becomes out of date as new techniques, languages or technologies aspire. We should focus on forming habits to keep ourselves up to date.
Building the portfolio
Goals
Critical Thinking
Fundamental tips and tricks to follow while approaching problems in the software projects. They are generic concepts that could be applied to any kind of project or company.
Encourage and follow Easier To Change, ETC principle. The wizardry to determine what is easier to change would be achieved in time, but keep your lights on to develop instincts.
It's one of the buzzwords going around. The take-away from the book is: DRY Is more than code!
DRY is about the duplication of knowledge, of intent. It's about expressing the same thing in two different places, possibly in two different ways.
Two systems are orthogonal if they are designed to be decoupled. When we design the components isolated from each other, we gain productivity and reduce risk.
As like everything in this world, the requirements for our software projects might become hard to predict and require lots of changed along the way. To decrease the workload during those inevitable changes, we may hide third-party APIs behind our own abstraction layers and break our code to components, regardless of the architecture and pipeline.
We are trying to shoot in the dark and tracer bullets are a way to see our surroundings to become more prepared. In this context,. a-tracer-bullet software would be the simplified version of the almost-production-ready system, which is presentable to the stakeholders and usable during the implementation. They are similar to MVP in some sense.
They are similar to tracer bullets but without the requirement of being able to work, or production ready quality. Prototypes can even be made of post-it notes or some whiteboard sketches while trying to foresee how the project could be shaped up considering the requirements. While building a prototype, we can ignore correctness, completeness, robustness, and the style. One challenge here is to make sure everyone involved understands the distinction between a prototype and a tracer bullet.
Internal domain languages: RSpec, Phoenix router example
External domain languages: Cucumber (parsed), Ansible (uses YAML)
Similar to the ETC, estimating is also a skill that develops over time. To become a successful estimator, there are several points we can keep in mind:
From the importance of using plain text to becoming more comfortable while using shell, keyboard shortcuts, and tips and tricks to debug our bugs.
HTML, JSON, YAML, XML are considered as plain text since they are parseable with a partial knowledge of the format. The actual problem is the text generated by a program is only readable with a similar kind of that program. Backed up by the experience, the text always outlives the software that produced it.
A benefit of GUIs is WYSIWYG — what you see is what you get. The disadvantage is WYSIAYG — what you see is all you get.
Explore the capabilities of your shell, customize it, use aliases and command completion.
We need to be able to manipulate the text as effortlessly as possible. To do so, we should achieve editor fluency. Avoid repetitive tasks and usage of trackpad/mouse and try to think in "there must be a better way" mindset.
No matter the size or the scope of the project, team... always use it.
The origins of "bug" in software word is coming from an actual bug (moth) caught in the computer system back in the days of COBOL. They captured it and dutifully taped to the log book.
The main purpose should be fixing the problem, not the blame.
When we find ourselves surprised by a bug, we must reevaluate the truths we hold dear. If the bug occurred in this piece of code, another piece which relies on the same concepts may also have a problem.
Learn a text manipulation language (or tool) to make fancy stuff like auto-generating table of contents of this book.
Handwritten notebooks to keep track of what I am working on, take notes on meetings, the things I learnt today and the ideas come up to my mind while dealing with the tasks. Pen and paper helps in a sense since it makes it easier to sketch and scribble.
Most of the humans perceive world as me vs. the rest, so do we. Programmers also look other's code in a similar way. But, why stop there? We should take it one step further and don't trust our code either. No one writes perfect code, there's no perfect software, everybody is going to die, come watch TV!
Similar to real world agreements between people, the interactions between software modules can also be secured by contracts. The DBC term first proposed by Bertrand Meyer as a part of the Eiffel programming language. Programs should document and verify their claims and it that would lead to predictability in case the contract is violated.
DBC is not a concept we can drop in favor of TDD or defensive programming. It's a complementary approach to increase the confidence.
Again, leave the "it can't happen" mentality behind and treat errors as a first class citizen in your mind. They usually indicate something very, very bad has happened.
A dead program normally does a lot less damage than a crippled one.
Check out Erlang's supervisor trees approach to capture errors in a propagated places instead of having verbose exception mapping blocks within the subroutines.
This doesn't apply to the languages I've dealt with but using assertions for exceptions (Java) is actually helpful since they throw traceable exceptions with a recognizable type and message. The book also suggests to keep assertions in code no matter if they're covered with tests since they could be helpful to detect unforeseeable exceptions.
Refers to the allocating and deallocating any kind of resources (memory, transactions, threads, network connections, file, timers, etc) that we are consuming in our programs.
The main responsibility to keep in mind is:
Finish what you start.
Deallocate resources in the opposite orders you allocate them so there won't be any orphans in case of a dependent resource situation.
If process A wants R1 then R2, process B wants R2 then R1, that may cause a deadlock.
There was a ruby code example in the book about this. Refer to page 119-120 or put them here.
Languages with exceptions, or the way handle them can make resource allocation tricky.
let resource
try {
resource = allocateResource()
process(resource)
} finally {
deallocateResource(resource)
}
If resource allocation fails and exception is thrown, finally cause will try to deallocate a thing that was never allocated.
const resource = allocateResource()
try {
process(resource)
} finally {
deallocateResource(resource)
}
We are safer with this approach and the exception of resource allocation will be propagated to the upper scope.
We can also write wrappers for resources for allocation, deallocation and check if they are healthy.
The phrase outrunning the headlights comes from taking turns or going fast with a vehicle the projector of headlines are not able to illuminate the path since it's made for a straight line. The main idea is to take small steps and adjust your next moves based on the feedbacks.
Consider that the rate of the feedback is your speed limit.
Practical approaches for every-day problems that we come across as programmers. Talks about concepts like decoupling, state machines, event handling, thinking programs as data transformers and using configuration.
Linking components in a system together is good if you are trying to build a bridge but it is not the best approach while writing software, which we prefer to be flexible since the quality of a software can be measurable by it's easiness to change over time.
Symptoms of coupling:
function applyDiscount(customer, order_id, discount) {
const totals = customer.orders.find(order_id).getTotals()
totals.grandTotal = totals.grandTotal - discount
totals.discount = discount
}
Problems:
Tell, don't ask principle states that we shouldn't make decisions based on the internal state of the object and then update the object. It should be capable of doing that itself without breaking the encapsulation and exposing the knowledge all over the system.
function applyDiscount(customer, order_id, discount) {
customer
// TDA: we don't care if orders are list or map, we just want to find it
// orders
// .find(order_id)
.findOrder(order_id)
// TDA: pls apply the discount, I don't care who manages it
// .getTotals()
// .applyDiscount(discount)
.applyDiscount(discount)
}
Assuming that customers and orders are one of the top-level concepts of this application, this might be enough.
Ian Holland's guidelines to decrease the train wreck. A method defined in C class should only call:
Try to avoid the usage of global data. It makes the code coupled and also you'll realize the setup function on your tests is also a mess. Creating a singleton with fancy methods wouldn't make it OK.
Underrated and very useful way to represent your systems reaction based on the events it receive. The example on page 140 was great.
Simple way to implement event listeners from a source. One downside is the event source is aware of its listeners so they are theoretically coupled, which is not a big problem if they are in the same context.
More generalized observer pattern which makes the source unaware of its listeners. Events are published to a channel that subscribers are listening. The downside is it can be hard to see what is going on in a system that uses pub-sub heavily.
Useful for combining multiple sources and creating new streams from the events. We can also generate parallel streams from an event source, then re-combine them into one and etc.
Think of a program as a transformer that takes an input and generates and output. Based on the requirements, we can streamline the steps to transform the input and isolate them from each other in order to be re-usable, which leads to this tip:
Don't hoard the state; pass it around!
Pipeline operator and kinda railway-oriented approach is shown here with several examples with Elixir. We can choose to handle errors within the transformation itself (return tuple and use overloading) or handle it in the pipeline (bind operator with monadic return values).
Language X Doesn't Have Pipelines
Thinking in transformations doesn't really require it. We can also streamline our steps as intermediate variables to get the job done.
We can use inheritance to add common functionality from a base class to into child classes; User and Product are both subclasses of ActiveRecord::Base.
This is problematic since both the code that uses the child and child itself are coupled to its parent, its parent's parent and so on. It introduces coupling to the codebase.
We can use inheritance to express the relationship between classes; a Car and a Bicycle are-a-kind-of Vehicle.
This would be problematic once we start introducing more types that can be inherited by Car and Bicycle objects since they can also be Asset, InsuredItem, and so on. It leads to multiple inheritance.
Instead of creating the base class, we can use interfaces or protocols to specify that a class implements one or more set of behaviors.
Interfaces and protocols give us inheritance without polymorphism.
A Car can implement Drivable, Locatable and Insurable behaviors where a Bicycle is only Drivable and Insurable. We can use a List<Drivable> to store instances of both classes.
Using inheritance usually overloads the classes with methods which we wouldn't probably expose (gorilla-banana-jungle problem). We can use delegation as an alternative to inheritance in order to perform respective tasks we'd inherit.
#### instead of
class Account < PersistanceBaseClass
end
#### to not expose Framework API to the consumers of Account class
class Account
def initialize(. . .)
@repo = Persister.for(self)
end
def save
@repo.save()
end
end
#### or take one step further to make `Account` class dumber
class Account
## nothing but account stuff
end
class AccountRecord
## wraps the account with the ability to be fetched and stored
end
Now the AccountRecord is really decoupled but it would make us to write more boilerplate code for common methods such as find, but we can use mixins and traits for that.
Although the naming varies to language, the idea between mixins is to create a set of functions that can be used to extend a class to use them.
mixin CommonFinders
def find(id) { ... }
def findAll() { ... }
end
class AccountRecord extends BasicRecord with CommonFinders
class UserRecord extends BasicRecord with CommonFinders
In addition to the static configuration that we are mostly familiar with (JSON, YAML, etc), the book also suggest to consider using Configuration-As-A-Service approach in order to make it shareable across multiple applications, editable via a GUI tool and dynamic. This dynamism would help us to register the respective parts of our application to the configuration updates and react to the changes without having to restart the whole application.
Concurrency is when the execution of two or more pieces of code act as if they run at the same time.
Parallelism is when they do run at the same time.
Temporal coupling happens when our code imposes a sequence on things that is not required to solve the problem at hand.
Temporal coupling is about time, which is usually ignored while designing the architecture in the linear mindset. We need to allow for concurrency and think about decoupling in the workflows, and we can use activity diagrams to detect concurrencies and maximize parallelism in the architecture.
Identify the opportunities for concurrency and parallelism, then get back to your application. Keep in mind that the activity diagram would only show the opportunities, and exploiting them is up to you. For example, a bartender would need five hands to be able to run the all tasks at once.
The customer-waiter-pie example starts here. You are in a restaurant (customer A) and asked if they have an apple pie, the waiter (X) looks at the display case and says yes. However, another customer (B) and waiter (Y) has the same conversation in the meantime. Who gets the pie?
Problem: to processes can access and write to the same memory at the same time. Since fetching and updating the pie count is not an atomic operation, the underlying value can change in the middle. So, we can't guarantee who gets the pie.
A semaphore is simply a thing that only one person can own at a time. For the physical pie problem, we can place a plastic Leprechaun and introduce the constraint of holding the Leprechaun in one hand to be able to get the pie. Software representation of this is simply a lock.
#### waiter's logic
case_semaphore.lock()
if display_case.pie_count() > 0
promise_pie_to_customer()
display_case.take_pie()
give_pie_to_customer()
end
case_semaphore.unlock()
The problem here is to align everyone on the constraint of using the semaphore, which is on the code level and not really enforcing any constraints.
The lock flow above is not sufficient enough since it delegates the responsibility of protection to the consumer of the resource. We can centralize that check and take the control into the provider's hands by making the resource transactional.
#### API exposed by the provider
def get_pie_if_available()
@case_semaphore.lock()
try {
if @slices.size > 0
update_sales_data(:pie)
return @slices.shift
else
false
end
}
#### update_sales_data may throw, then we'd have a deadlock
ensure {
@case_semaphore.unlock()
}
end
#### waiter's logic
slice = get_pie_if_available()
if slice
#### serve the slice of pie
else
#### say sorrrrry
end
We might say that we have a solution for the pie problem. Now it's the summer season and we add an option to our menu to serve the pie with ice cream. They can still be ordered separately, but the customers prefer to get the combined option. How can we make sure we serve them correctly?
#### waiter's logic
slice = display_case.get_pie_if_available()
scoop = freezer.get_ice_cream_if_available()
if slice && scoop
give_order_to_customer()
end
This wouldn't work because it doesn't ensure the availability of both resources before accessing either of them. We can get the pie but it would be meaningless without the ice cream, and logic to put it back would be another overhaul.
#### waiter's logic
slice = display_case.get_pie_if_available()
if slice {
try {
scoop = freezer.get_ice_cream_if_available()
if scoop
try {
give_order_to_customer()
}
rescue {
freezer.give_back(scoop)
}
end
}
rescue {
display_case.give_back(slice)
}
}
A more elegant way to solve this to create another resource called slice_with_ice_cream and move the resource handling to resource itself, as we did for the slice and scoop.
Random failures are often concurrency issues.
Concurrency problems do not only occur for shared memories, they can also be seen while using any kind of shared resources: files, databases, external services, etc.
Mutual exclusion (mutex) term is used for some kind of exclusive access to shared resources, most languages have libraries to support this.
Actors and processes offers interesting ways implementing concurrency without the burden of synchronizing the state.
An actor is an independent virtual processor that has its own state which is not accessible outside of its context. It waits on the idle state and processes the messages reaching to its mailbox. After the completion, it can consume the queued messages or go back to idle state.
A process is typically a more general-purpose virtual processor, often implemented by the operating system to facilitate concurrency. They are constrained to behave like actors.
Some facts about them:
There is an example of actor system implementation in the book, built with nact.
Blackboard stores some facts about the system, many independent actors can be spawned by the changes on that system, work on their processes then add more to the system.
The book describes the blackboard as a real blackboard that we write the details about a criminal case, and detectives as actors who are trying to solve that case.
Blackboard can be used for collective knowledge gathering systems, such as natural language processing, mortgage application lookup and finding an appropriate time for a set of friends to meet in their favorite pizza spot.
Things to keep in mind while we are on it... Instincts, awareness, performance, refactoring and more!
Lizard Brain → our instincts. Feeling in danger on physical situations is not something made up by our mind, it is actually a response generated by our non-conscious brain. Our lizard brain actually develops over time, by experience and knowledge.
We encounter with the objections from the lizard brain while doing stuff, especially if the page is plain white. First thing to do is to acknowledge that, then take a step back and give time to your brain to organize itself. Do not try to fight or beat it, instead take advantage of that objection to re-think your design, explain it to some other people, and try prototyping instead of solving the problem all together. After you reach a stage that feels like you are in the flow, get rid of that prototype and start over.
It doesn't have to be your code, or just code.
My code doesn't work, no idea why. My code works, no idea why.
Big-O notation, common sense for some algorithms:
Refactor early, refactor often
Residential building metaphor: architect draws up blueprints, contracts dig the foundation and build the structure, tenants move in live happily after, then they call the building maintenance to fix any problems, or hire some other contractors to improve the conditions.
Refactoring should not change the external behavior of the code, so it is not about adding features. It should aim to make it code easier to change.
When we learned something new, understand something better than we did last year, yesterday, or even just a few minutes ago. There are many other things that can be considered as refactoring opportunities:
Testing is not about finding bugs
Your tests will be the first user of your code, and this process would make you think not only as the author. It will also improve the styling while as you will be aiming both for testability and functionality.
Test your software, or your users will
A complementary approach to unit-testing. Uses randomly generated data as inputs to our modules in order to observe how would they behave in edge cases, high load, weird inputs.
The failures we come across during property-based tests are often surprises us, and they might indicate bad assumptions.
Basic principles of security:
The sum of all access points that a system can be compromised.
Don't automatically grab the highest permission level such as root or Administrator. Try to accomplish tasks with the minimum privilege.
The default settings, interface, configuration on your app should be in the most secure way.
Don't leave personally identifiable information, financial data, passwords or other credentials in plain text, whether in a DB or some other external file. Don't check in them to version control, manage keys and secrets decoupled from the system as a part of build end deployment.
Just remember the largest data breaches in history were caused by systems that were behind on their updates.
The first and foremost rule when it comes to crypto is never do it yourself. It doesn't only apply to cyphering sensitive information, you should also prefer reliable 3rd party providers for the authentication part.
Requirements are not really clear on every project, they rarely lie on the surface. They are buried deep beneath layers of assumptions, misconceptions, and politics. Even worse, they don't exist at all.
As programmers, it's one of our qualifications to look for the edge cases, think about the illogical scenarios and help the client to reflect the requirements better. That's what makes our job intellectual and creative. We need to have knowledge about the business itself (sometimes it helps to spend some time in the actual field) and ability to thing about the logical representation when it's reflected by ones and zeroes.
Work with a user to think like a user
Some requirements (like privileges) are actually policies which should be implemented as metadata.
Short feedback loops plays an extremely important rule on this topic. Brian Eno thought he designed the perfect recording engineer keyboard but all the controls are tied to keyboard and mouse unlike the analog fine-tuning options provided by the classical methods.
This might be a bit waste of time since the clients usually don't read them because they are more into the high-level solution of the problem. However, it'd might be helpful for the developers.
Prefer user stories instead of requirements to visualize the progress better, and more perceivable for stakeholders with different technical backgrounds.
Good requirements are abstract, requirements are not architecture, they are the need. Don't oversimplify, nor over exaggerate.
One place that defines all the specific terms and vocabulary used in a project.
In order to think "outside of the box", you need to "find the box" first. The secret to solving impossible puzzles is to identify the real constraints.
When faced with an intractable problem, enumerate all the possible avenues before you, no matter how unusable or stupid it sounds. Consider the Trojan horse, you can't bet "through the front door" was the most logical idea, but it worked!
Give credits to your unconscious brain, feed it with your past experience and don't underestimate the power of rubber ducking.
Start with pair programming, it decreases the trivial approaches and dirty shortcuts, thus improves the quality. Also try mob programming, with those considerations:
The real agility is not following of some rules set on the stone. The true meaning is how you adapt it in your workflows in order to be responsive to change.
Keeping the feedback loop efficient, and iterating with small steps gives us the freedom of throwing away our work and starting over in order to reach the desired outcome.
Small, cross functional, mostly stable entity of its own, that considers the following items:
Teams as a whole should not tolerate broken windows and integrate it as a team practice, thus the newcomers will also adapt it.
Encourage everyone to actively monitor the environment for changes. Stay awake and aware for increased scope.
The team should work on more than just new features, it should be serious about improvement and innovation.
Great project teams have a distinct personality. People look forward to meetings with them, because they know that they'll se a well-prepared performance that makes everyone feel good.
Remember that teams are made of individuals, give each member the ability to shine in their own way. Give them just enough structure to support them and to ensure that the project delivers value.
Know your context, structure and organization before trying to adapt what everyone is doing, or whatever the most popular thing is. Don't hesitate to try things to see what works out for you.
The real goal should be decreasing the delivery time to be continuous, not years, not months, not weeks; hours.
Covers the best practices around version control, continuous delivery, integration, unit testing and so on. One good takeaway from this chapter is; once you found and fix a bug, write tests for it so you make sure it doesn't happen again.
Ask them how will they know what we've all been successful a month (or a year, or whatever) after this project is done?
As programmers, we have the power to build products which would shape the society. Keep your lights on and don't put your humanity into the second plan.
Ask the following questions to yourself.
Recognize when you are doing something against the ideal, and have the courage to say "no!"
Envision the future we could have, and have the courage to create it.