As you learn and improve, you'll be able to do more - including writing more code!
You'll also often work with other developers, sharing your code and collaborating on implementation. At this point, itโs vital to ensure you are writing clear, well-structured code. And, to use a popular term in software engineering: clean code!
Introducing "clean code"
So, whatโs clean code?
Clean code is code that's easy to understand and easy to change. Like a good book, it communicates the story of what you want the computer to do.
Why does it matter?
Your code needs to work as intended and solve problems efficiently. To do this, your code must be easily testable, modifiable, and expandable by other humans - your team members!
Beyond creating an enjoyable environment for developers to work in, it enables you to implement efficient and elegant solutions in short periods of time.
How do you achieve that "clean code" state?
There are two components to writing clean code:
the writing style
the code architecture
The writing style was covered in previous courses! ๐
One of the things to remember about writing style is to use comments in your code.
Comments are also useful for marking the code for quick navigation.
Remember: always apply best practices to your writing!
The code architecture is about how the logical components are designed. Object-oriented languages offer powerful techniques; however, without a proper system, the code can become messy and unsustainable.
SOLID principles
To keep our logical components, objects in particular, in order, follow these five "SOLID" principles:
Single responsibility principle
Open/closed principle
Liskov substitution principle
Interface segregation principle
Dependency inversion principle
Does this list look a bit intimidating? It might at first, but let's look at the simplicity behind the fancy terms. ๐ช
Single responsibility principle
This first SOLID principle dictates that a class should be responsible for as little as possible - ideally, a single task with a number of subtasks. Think of it as an assembly line - each part of the line does one operation.
This applies to classes as well as other code sequences - like structures, protocols, enums, methods, closures, etc. If a block of code is responsible for multiple tasks, they need to be separated into just a few functions, and then those functions can each be called an umbrella function or closure.
When designing more complex code entities - classes, structures, protocols - focus on a single context. And their components - properties/variables and methods - must serve that one context and not more.
Here's an example:
class Podcast {
var title: String {
return "iOS tips & tricks"
}
var author: String {
return "Podcast master"
}
var guest: String {
return "iOS guru"
}
var track: String? {
return nil
}
func play() {
}
}
We will play it eventually... but, playing does not need to be attached to the content! What if we just need to upload, download, etc.?!
So, let's alter:
class Podcast {
var title: String {
return "iOS tips & tricks"
}
var author: String {
return "Podcast master"
}
var guest: String {
return "iOS guru"
}
var track: String? {
return nil
}
}
class Player {
func play(podcast: Podcast) {
}
}
Now Podcast doesn't care about player, and the player will take care of playing! โจ
Open/closed principle
This second SOLID principle means that a code component should be open for extension and closed for modification.
Letโs take a bicycle as an example. ๐ฒ Various attachments can be purchased to modify or add functions, but the original bike frame will remain unchanged.
Let's imagine a monster:
class Attachment {
// ...
}
class Basket: Attachment {
// ...
}
class Parachute: Attachment {
// ...
}
class Bike {
func attach(basket: Basket) {
// ...
}
func attach(parachute: Parachute) {
// ...
}
}
When another fancy attachment comes out, we'd need to modify the bike. ๐ง
Instead, we could subclass the basic bike:
class Bike {
func attach(attachment: Attachment) {
// ...
}
}
class CatBike {
func attach(attachment: Attachment) {
// attach a basket
}
}
class FlyingBike {
func attach(attachment: Attachment) {
// attach a parachute
}
}
Any other bike extremists out there? ๐
Liskov substitution principle
This third SOLID principle addresses the potential dangers of using inheritance and suggests avoiding dramatic modifications in subclasses that break parent functionality. To go back to our bicycle example, if you get on a bike with an attachment, you still should be able to peddle and steer like you would on the original version of the bicycle.
Let's create a "monster" example:
class Bike {
func changeChain() {
// ...
}
}
class Unicycle: Bike {
override func changeChain() {
// oops, theer's no chain, throw an exception !!!
}
}
Even though both are kind of bikes, this structure makes little sense. Perhaps they could have a common parent class or reverse subclassing.
Interface segregation principle
This fourth SOLID principle says: donโt impose unnecessary requirements. For example, if you wanted to take your cat for a ride, youโd need a small cat carrier attached to your bike. How frustrating would it be if all cat carriers came with a parachute attachment that you would also have to mount on your bike! ๐
Here's a monster version:
class AttachmentMonster: Attachable {
var basket: [AnyObject]
var parachute: [AnyObject]
// ...
}
class PoorBike {
// ....
func attach(attachment: Attachable) {
// use attachment variable
}
}
let poorBike = PoorBike()
let attachmentMonster = AttachmentMonster()
poorBike.attach(attachment: attachmentMonster)
In the code above we can see that in order to attach a basket to the bike to place a cat, we also need to attach a parachute - try to imagine that ๐ฑ!
How about this instead:
class Basket: Attachable {
// ...
}
class Parachute: Attachable {
// ...
}
class HappyBike {
// ....
func attach(attachment: Attachable) {
// use attachment variable
}
}
let happyBike = HappyBike()
// day 1
let basket = Basket()
happyBike.attach(attachment: basket)
// day 1
let parachute = Basket()
happyBike.attach(attachment: parachute)
The convenience is now undeniable! ๐
Dependency inversion principle
This fifth and final SOLID principle suggests making code components the least-dependent possible using abstraction and dependency injection.
Continuing with the bicycle example, it doesnโt matter which attachment will be connected to it as long as it has a matching mechanism. An attachment, in turn, doesn't need to know which bike (or other vehicles) it would be connected to... as long as it has a matching mechanism.
So, a monster version could be:
class CatCarriageAttachment {
// ...
}
class BikeMonster {
var wheels: [Any]
var attachment: CatCarriageAttachment
init() {
wheels = ["front", "back"]
attachment = CatCarriageAttachment()
}
func attach() {
// use attachment variable
}
}
In the code above, we've made our Bike class responsible for a particular attachment. It makes it harder to test and maintain, and more so, makes it hard to manage the kind of attachments the bike can have. ๐
Instead, we could pass on Attachment object to the attach function. And, to make it even better, we can use protocols for that:
protocol Attachable {
// ...
}
class Attachment: Attachable {
// ...
}
class CatCarriage: Attachment {
// ...
}
class Bike {
var wheels: [Any]
init() {
wheels = ["front", "back"]
}
func attach(attachment: Attachable) {
// use attachment variable
}
}
This time around, we have separate classes for bike and attachments. Now the Bike class can benefit from using attachments; however, it is no longer responsible for the attachment particulars! Just notice the difference! ๐
Does this sound much simpler now? ๐ I hope so!
In the next chapters of this course, weโll be paying attention to many code design decisions and ensure optimal implementation.
Let's recap!
Clean code is code that's easy to understand and easy to change.
Writing clean code is done via two channels:
Writing style
Code architecture
Following SOLID principles facilitates the production of clean code:
Single responsibility principle - responsible for a single task;
Open/closed principle - open for extension, closed for modification;
Liskov substitution principle - maintain functionality throughout inheritance;
Interface segregation principle - include only what's necessary;
Dependency inversion principle - make components independent of each other.