me
Published on

Software architect elevator

Authors
  • avatar
    Name
    Omer ATAGUN
    Twitter

Recently i have resigned from my position in bragg group. Long story short, i have met this amazing team of people from LeanIX during sets of interviews and i decided to join them.

Out of all companies that i had applied to, only LeanIX was the one i wanted to be part of thanks to the people i have met during the interview process. I am really excited to join them and i am looking forward to new challenges that are ahead of me.

After i have signed the contract, company sent me a gift package. Not just a regular t-shirt, hoodie and ledger but on top of all these standard goods, they have also shipped me a book called The Software Architect Elevator by Gregor Hohpe.

I was already excited to get my hands on to this book for a reason that a tech company choose to deliver this book to its new employees. I assumed the book should not be just a random boring book, right?

I have started to read the book and i have to say that i am really impressed. I am not going to write a review about the book but i will share some of the quotes that i have liked so far. Maybe i will add some little examples of how i understood or what i have witnessed before. It was nice having validation from the writer that my thought process was not that bad.

Lets talk about the book.

Lets list the subjects that we are going to cover which was mentioned in the book itself. I think i may categorize them as business and technical parts even though they live together in real world.

Technical

Tools

"Make sure that your tools work for you, not the other way around". Such elegant sentence that explains a lot on its own. Presumably, you as a reader and most likely have witnessed this at least once but in reality more than once. By my understanding, this could be literally anything, from your choice of packages to a framework that you have chosen to use. It truly has to work for you and allow you to produce more value in little time. Simple example would be, having an external dependency that changes its api frequently and you have to keep up with it due to these changes are breaking at all time. This is not a good tool for you. It is not working for you, you are working for it. You are spending your time to keep up with it.

Testing

"Propose to a development team that they let you delete 20 arbitrary lines from their source code. Then, they will run their tests--if they pass, they will push the code straight into production. From their reaction, you will know immediately whether their source code has sufficient test coverage.". Makes me conclude that your test coverage should not be measured by percentage but how confident you are to make changes.

I think we can example this very nicely.

const executeWithDelay = (delay, callback = () => undefined) => setTimeout(callback, delay)
const desiredDelay = 1000

jest.useFakeTimers()
jest.spyOn(global, 'setTimeout')

describe('executeWithDelay', () => {
  executeWithDelay(desiredDelay, () => {
    console.log('test')
  })

  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), desiredDelay)
})

// this javascript code is tested now, but is it enough?

// considering there are not any types, we might as well do below.

executeWithDelay('a', 'b')

Lets increase the confidence with typescript.

type anyFunction = () => void

const executeWithDelay = (delay: number, callback: anyFunction) => setTimeout(callback, delay)
const desiredDelay = 1000

jest.useFakeTimers()
jest.spyOn(global, 'setTimeout')

describe('executeWithDelay', () => {
  executeWithDelay(desiredDelay, () => {
    console.log('test')
  })

  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), desiredDelay)
})

We kept the same tests but increased our confidence by typing delay as a number, callback as anyfunction yet i think it is not enough.

Isn't it odd to have negative integers for a delay? Yes, i thought so too. So lets make our number into a more strict type where we only ask for unsigned integer. Hence, we increase our confidence for this feature to work as expected.

type UnsignedInteger<T extends number> = number extends T
  ? never
  : `${T}` extends `-${string}` | `${string}.${string}`
  ? never
  : T

type anyFunction = () => void

const executeWithDelay = <T extends number>(delay: UnsignedInteger<T>, callback: anyFunction) =>
  setTimeout(callback, delay)
// if we try to set desiredDelay to any negative number, it will fail to compile.
const desiredDelay = 1000

jest.useFakeTimers()
jest.spyOn(global, 'setTimeout')

describe('executeWithDelay', () => {
  executeWithDelay(desiredDelay, () => {
    console.log('test')
  })

  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), desiredDelay)
})

// No need to stop here, we can also test
// if the function is called with the correct arguments but
// in our case there are not any arguments.
type UnsignedInteger<T extends number> = number extends T
  ? never
  : `${T}` extends `-${string}` | `${string}.${string}`
  ? never
  : T

type anyFunction = () => void

const executeWithDelay = <T extends number>(delay: UnsignedInteger<T>, callback: anyFunction) =>
  setTimeout(callback, delay)
// if we try to set desiredDelay to any negative number, it will fail to compile.
const desiredDelay = 1000
const mockCallBack = jest.fn(() => console.log('test'))

jest.useFakeTimers()
jest.spyOn(global, 'setTimeout')

describe('executeWithDelay', () => {
  executeWithDelay(desiredDelay, mockCallBack)
  expect(setTimeout).toHaveBeenLastCalledWith(mockCallBack, desiredDelay)
})

Others would also work as we expected any function to be given, but here in this case, we assure that it is our mock function which is called. Better explicit than implicit.

I think we have done already enough to explain what confidence actually implemented in a test is. You have the imagination, do more if you see fit.

Design

"The set of design decisions about any system that keeps its implementors and maintainers from exercising needless creativity." by Desmon D'Souza mentioned in the book itself by the author.

An approach that will help all the contributors to have boring day to day work. Apprecite the boring solutions. S.O.L.I.D. are one of those to apply for your codebase.

I belive a good example might be protocols where an interface is defined and there is only one way to accomplish a given task. HTTP protocol for example. There is a one way to accomplish but any language can implement it in its own way. Even after sometime, you may iterate and improve its stability, security or performance however you wish to. It is straight forward and boring.

Just for the sake of example, we can present below code.

interface MakeCakeProps {
  color: string
  taste: string
  size: number
}

interface MakeCakeReturn {
  color: string
  taste: string
  size: number
}

interface SomeServiceMakesACakeProps {
  data: MakeCakeProps
}

const someServiceMakesACake = (data: SomeServiceMakesACakeProps) => {
  return {
    color: 'red',
    taste: 'sweet',
    size: 10,
  }
}
export const makeCake = (data: MakeCakeProps): MakeCakeReturn => {
  // tomorrow you would decide to change someServiceMakesACake to something else
  // your implementation detail would change but interface remains the same.
  return someServiceMakesACake({
    data,
  })
  //return someOtherserviceMakesCake(data)
}

Since the executor is encapsulated into another function named makeCake, it is easy to test and maintain. It is boring and it is good. It can also be replaced at anytime without changing the interface or the usage of the function that has been exported. On top of this implementation, you would add a linter rule to make sure that no one is using axios directly.

Abstraction

"If an abstraction takes away too many or the wrong things. It becomes overly restrictive and no longer applicable. If it takes away too few things, it did not accomplish much in terms of simplification and hence is not very valuable."

Can't disagree. Let's try to explain this with an example.

// here we have just another class that is responsible for baking a cake.
class FreddieBakery {
  private ingredients: string[]
  private time: number
  private temperature: number
  // we have asked for ingredients, time and temperature
  constructor({ ingredients, time, temperature }) {
    this.ingredients = ingredients
    this.time = time
    this.temperature = temperature
  }
  // then we abstracted the baking process into a function called bake
  // so implementation detail would not matter to the client.
  // We wanted to make it easy.
  public bake() {
    // is this really enough abstraction or too little? is it even flexible?
    // What if we need temperature to be changed in the middle of the baking process?
    // no matter what, do we have a cancel process ?
    // can i add a new ingredient in the middle of the baking process?
    console.log(`Baking for ${this.time} minutes at ${this.temperature} degrees`)
  }
}

Above example is a good thinking process of whether it is too little or too many. To find the right balance, we should always think what could have been requested more from this feature?

Business

Author covers pretty much all aspects that are needed, but i am not interested in writing those in this blog.

Conclusion

Out of all other books that i had luck to lay my hands on, this one is a great starter pack for a software architect. It is not a book that you would read once and forget about it. It is a book that you should come back to from time to time. Observations, solutions or analysis most likely will remain timeless.

All above examples were my own understanding of the book in which merged with my own experiences, i am sure there could be better explanations but isn't it the point? We all have different experiences and we all have different ways to explain things. Just like a software solution, there are millions of ways to do it but there is always a better way to do it.