This article is a brief review of the last half year in my professional life, and all these topics are flowing in my mind for months. It's better to release the reins, let the paw-prints appear on the paper.
I took a long work gap, which went through the whole hot summer last year, to have a vacation, meanwhile, it's a certain time when introspection happens. After this, I joined a new company, same position as a web developer, but got to learn a new language Ruby in a different industry both of these I never had experienced before.
Time goes faster than you think, and you also have sailed far far away along your new direction. I looked back and asked my self what's the balance of diving into a new language, and what are the skillful tricks of being a more professional software engineer, while inertia was still forcing the ship forward to the blue water. It's not about the definition of enough or success in a career field. However it's like to introduce the way of how to take less effort, but make a better outcome. Fortunately, I got the following three practices which are short and should be asked to yourself before writing down your first line of code in every project.
Less is More
This should be the most famous slogan I've ever read in recent years, especially in product design books. This sentence is firstly created in the minimalism movement, architect Ludwig Mies van der Rohe (1886–1969) adopted the motto "Less is more" to describe his aesthetic.[^1] We don't have to work as artists, but there is a popular book describes coding as an art, same as painting, [^2]at least that is a theoretical status to describe what's the well performed coding work should be like.
Language Advantage
When I first got a chance to write the If-Condition in ruby, I was fascinated with the well-designed syntax sugar. See the code first.
def greet(name)
return if name.empty?
puts "Hi #{name}!"
end
From the code above, you can see the If-Condition is written like typing a sentence in your normal article, do nothing and return if the name is empty while trying to greet someone. Anyway, it's the first piece of code coming to my brain as usual. But how about this?
def greet(name)
puts "Hi #{name}!" if !name.empty?
end
This If-Condition is written explicitly, just as What You See Is What You Get, and you don't even have to think about the meaning of this line before you understand it. You do the same thing with less code if you try a slight effort while writing down your thought, so cool.
Less Code
Another example is shared in a session from my colleague, the topic is about how to get better performance of testing. One of the rules, which I think is very helpful, is that writing down fewer test cases by using PRIVATE keyword more widely.
class Greeting
def say(name)
puts "Hi #{name}!" if validate(name)
end
def validate(name)
return true if !name.empty?
end
end
# Hi Martin!
Greeting.new.say 'Martin'
To get 100% of test coverage, we have to create at least two test cases for #say and #validate. As a contrast, we can use the PRIVATE keyword instead.
class Greeting
def say(name)
puts "Hi #{name}!" if validate(name)
end
private
def validate(name)
return true if !name.empty?
end
end
# Hi Martin!
Greeting.new.say 'Martin'
Under this situation, we could achieve the 100% of test coverage by only adding one test case. Less code, but more productive.
Premature Optimization is the Root of All Evil
First of all, let me explain what is Premature Optimization when we are talking about coding. Premature Optimization can be defined as optimizing before we know that we need to. Optimizing up front is often regarded as breaking YouArentGonnaNeedIt (YAGNI).[^3]
While I have to clarify that this is not an excuse of being complacent with your code to avoid optimization. Most of our time is focusing on writing code, reviewing code, refactoring code, and all this kind of normal working flows are going to be excluded in this topic. The original quota of this is focusing on software efficiency optimization, which we talk less about during modern web developing, but It can still be considered as a golden sentence on code organization. Here I have two rules for you to determine whether it's prematurely optimized.
Don't Optimize Without a Clear Benefit
Snippet A
class TestParam
def initialize
@arguments = {
author: 'Mark Twain',
book: 'The Adventures of Tom Sawyer',
}
end
def author
@arguments[:author]
end
def book
@arguments[:book]
end
end
# output: Mark Twain
puts TestParam.new.author
# output: The Adventures of Tom Sawyer
puts TestParam.new.book
Snippet B
class TestParam
def initialize
@arguments = {
author: 'Mark Twain',
book: 'The Adventures of Tom Sawyer',
}
end
def param(key)
@arguments[key]
end
end
# output: Mark Twain
puts TestParam.new.param :author
# output: The Adventures of Tom Sawyer
puts TestParam.new.param :book
Comparing A with B, B has fewer code lines, less method, but the code structure of A is more semantic. When you read the code, you'll know that you can only get two types of data from the instance, author name and book title. However, you can't get what are the exactly available items from B immediately after reading the code. It's an important rule that not optimize without a clear benefit by sacrificing code readability and maintainability.
Don't optimize without profiling
Today we've got a lot of profiling tools, static code analysis tools, tracing profiling tools, APM (Application Performance Monitoring ) tools, hence it's much easier for us to do profiling.
During the past months of running online services, I do have chances to refactor code and improve the performance. For example, after inspecting a high latency API, I just found out it's because of fetching lots of cache items one by one on our Redis server. The optimization solution is quite easy by combining multiple Redis requests to only one to reduce the network time-consuming.
On the other hand, I created a background job to synchronize user data from another service last week. In this job, it retrieves all the data for about 5k items by sending an API, and at the same, the unavailable records should also be deleted from DB. My solution is very straightforward by computing the two collections in memory, for the data from API and the data from DB. We really don't need to worry about the memory usage for now, since each item in the collection is about 200 bytes, it's only 2MB for 5K items.
In a Scrum training, our coach told us that new requirements and details are emerging during a Sprint. This theory is also suitable for coding. In a nutshell, this rule can be more sample that not try to optimize until the predictable bottleneck shows up.
To Be or Not to Be
Premature Optimization is one of the famous anti-patterns that we should be aware of. On the contrary, should we always follow design patterns? Lots of patterns are widely implemented in my past projects, like Singleton, Factory Method, IoC. I've read many articles about why we should follow the design patterns to avoid system design failed. Even during the earlier years in my career, colleagues are always talking about design pattern.
I was shocked that the first thing came in my mind is to choose which design pattern I'm going to use when I wrote down the code, but not to write down a simple working code. The intent of pattern is to provide a general solution for a commonly occurring problem in software design.[^4] In other words, design pattern cannot satisfy the unique of projects.
Straightforward above design patterns
Here is a code piece from https://github.com/storeon/router/ shows how to create routes based on the module, the code is straightforward.
router.createRouter([
['/', () => ({ page: 'home' })],
['/blog', () => ({ page: 'blog' })],
['/blog/post/*', (id) => ({ page: 'post', id })],
[
/^blog\/post\/(\d+)\/(\d+)$/,
(year, month) => ({ page: 'post', year, month })
]
])
Let's speak, what would you do if you are going to write a blog system and forget the design patterns. The pseudocode code of my solution is like this:
function response(uri)
render 'post' and return if uri.matchs '/post'
render 'category' and return if uri.matchs '/category'
render 'home' and return if uri.matchs '/'
end
Since the routes of blog system are very simple, the straightforward way works better and the code is more readable. To handle a new route is also very simple by adding a new line of route code.
If you get a chance to see the API between different languages of Youtube, you can see they are the following the same structure and workflow, even it's not a good practice in those languages. I've seen a project that defines vast of fundamental interfaces as an influence of Google API style. In the code, the thought often lost in business logic and implementing different interfaces, although it's just to interact with a third party service which should be an easy thing to do.
You may wonder what is the best practice of coding, the answer there is not a general best practice for every project. Hence, no pattern fits every project, just ask yourself the sentences above and make the best choice depending on your own experience.
[^1]: https://en.wikipedia.org/wiki/Minimalism#cite_note-25 Less but better. [^2]: https://www.goodreads.com/book/show/41793 Hackers_Painters [^3]: http://wiki.c2.com/?PrematureOptimization Premature Optimization [^4]: https://sourcemaking.com/design_patterns Design Patterns