
Stepping into the world beyond university confines, where the theoretical meets the real, you soon realize the stark gap between classroom learning and industry demands. While academia introduces you to programming and fundamentals like Discrete Mathematics and Calculus, the industry demands a nuanced understanding of key principles that streamline code and bolster collaboration. Here, we delve into the transformative trifecta: KISS, DRY, and SOLID principles.
Embrace Simplicity: The KISS Principle
Keep It Simple, Stupid!
In the collaborative sphere of software development, clarity trumps complexity. Picture yourself amid a team, each member crafting different facets of a project. If tasked with overseeing a colleague’s code, a clean, well-documented script is far preferable to a tangled mess of logic. The same principle applies to personal projects. After a brief hiatus—perhaps due to illness—returning to a project with opaque code only compounds the challenge.
Consider the debugging process: a tangled codebase transforms minor debugging into an insurmountable task. Thus, the wise choose simplicity, echoing Martin Fowler’s wisdom:
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
In practice, simplicity ensures that even after time away, one can return to comprehend and continue development without losing stride.
Mastering KISS Techniques
Reflect on a Student
class, tasked with storing module information and corresponding scores. Here’s a straightforward representation:
data class Student(
val name: String,
val age: Int,
val moduleMarks: Map<Pair<String, String>, Pair<Int, Int>>
)
For efficiency, a function identifies high achievers:
fun scholars(students: List<Student>): Map<String, List<String>> {
val scholars = mutableMapOf<String, List<String>>()
students.forEach { student ->
scholars[student.name] = student.moduleMarks
.filter { (_, (a, m)) -> a / m > 0.8 }
.map { ((n, _), _) -> n }
}
return scholars
}
Upon reflection, simplifying this enhances readability. Separate concerns by introducing more abstractions:
data class Student(
val name: String,
val age: Int,
val moduleMarks: Map<Module, Mark>
)
data class Module(
val name: String,
val id: String
)
data class Mark(
val achieved: Double,
val maximum: Double
) {
fun isAbove(percentage: Double): Boolean {
return achieved / maximum * 100 > percentage
}
}
fun scholars(students: List<Student>): Map<String, List<String>> {
val scholars = mutableMapOf<String, List<String>>()
students.forEach { student ->
val modulesAbove80 = student.moduleMarks
.filter { (_, mark) -> mark.isAbove(80.0) }
.map { (module, _) -> module.name }
scholars[student.name] = modulesAbove80
}
return scholars
}
The additional structures make comprehension and debugging significantly more manageable. Each part of the code now reads more like a logical narrative.
Efficiency Through DRY Principle
Don’t Repeat Yourself
In a world where redundancy leads to fragility, avoid repetitive code. Suppose you’re developing a spreadsheet application that frequently requires fetching data objects by location. Initially, your approach might be repetitive:
public class Spreadsheet implements BasicSpreadsheet {
private final Set<Cell> cells;
@Override
public double getCellValue(CellLocation location) {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);
return cell == null ? 0d : cell.getValue();
}
@Override
public String getCellExpression(CellLocation location) {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);
return cell == null ? "" : cell.getExpression();
}
@Override
public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);
// ...
}
// ...
}
Abstraction is your ally. Extract the common logic into reusable components:
public class Spreadsheet implements BasicSpreadsheet {
private final Set<Cell> cells;
@Override
public double getCellValue(CellLocation location) {
return getFromCell(location, Cell::getValue, 0d);
}
@Override
public String getCellExpression(CellLocation location) {
return getFromCell(location, Cell::getExpression, "");
}
@Override
public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
Cell cell = findCell(location);
// ...
}
// ...
private Cell findCell(CellLocation location) {
return cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);
}
private <T> T getFromCell(CellLocation location,
Function<Cell, T> function,
T defaultValue) {
Cell cell = findCell(location);
return cell == null ? defaultValue : function.apply(cell);
}
}
These changes ensure that when a bug emerges, a single amendment rectifies it across all affected contexts.
SOLID Principles: The Pillars of Robust Design
Understanding SOLID isn’t about memorizing acronyms, but about internalizing principles that foster responsive, maintainable code.
S — Single Responsibility
Each class or function should bear one responsibility, enhancing clarity and reducing overlap in functionality. For example:
class Repository(
private val api: MyRemoteDatabase,
private val local: MyLocalDatabase
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()
// Saving data in the cache
var model = Model.parse(response.payload)
val success = local.addModel(model)
if (!success) {
emit(Error("Error caching the remote data"))
return@flow
}
// Returning data from a single source of truth
model = local.find(model.key)
emit(Success(model))
}
}
This code couples networking and caching. Separation of concerns suggests distinct responsibilities:
class Repository(
private val api: MyRemoteDatabase,
private val cache: MyCachingService
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()
val model = cache.save(response.payload)
// Sending back the data
model?.let {
emit(Success(it))
} ?: emit(Error("Error caching the remote data"))
}
}
class MyCachingService(
private val local: MyLocalDatabase
) {
suspend fun save(payload: Payload): Model? {
var model = Model.parse(payload)
val success = local.addModel(model)
return if (success)
local.find(model.key)
else
null
}
}
O — Open/Closed
Your codebase should be amenable to extension yet resilient to modifications. Design your interfaces for easy expansion:
interface PageTag {
val width: Int
val height: Int
}
class ParagraphTag(
override val width: Int,
override val height: Int
) : PageTag
class AnchorTag(
override val width: Int,
override val height: Int
) : PageTag
class ImageTag(
override val width: Int,
override val height: Int
) : PageTag
infix fun PageTag.tallerThan(other: PageTag): Boolean {
return this.height > other.height
}
L — Liskov Substitution
Subtype behavior should seamlessly replace base types, preserving system consistency. Consider differentiating flight capabilities:
open class FlightlessBird {
open fun eat() {
// ...
}
}
open class Bird : FlightlessBird() {
open fun fly() {
// ...
}
}
class Penguin : FlightlessBird() {
// ...
}
class Eagle : Bird() {
// ...
}
I — Interface Segregation
Prevent forced dependencies by segregating interfaces:
interface SystemRunnable {
fun turnOn()
fun turnOff()
}
interface Drivable() {
fun drive()
}
interface Flyable() {
fun fly()
}
interface Pedalable() {
fun pedal()
}
class Car : SystemRunnable, Drivable {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun drive() { /* Implementation */ }
}
class Aeroplane : SystemRunnable, Flyable {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun fly() { /* Implementation */ }
}
class Bicycle : Pedalable {
override fun pedal() { /* Implementation */ }
}
D — Dependency Inversion
Build systems depending on abstractions rather than concrete implementations:
interface CachingService {
suspend fun save(): Model?
}
interface SomeLocalDb() {
suspend fun add(model: Model): Boolean
suspend fun find(key: Model.Key): Model
}
class Repository(
private val api: SomeRemoteDb,
private val cache: CachingService
) { /* Implementation */ }
class MyCachingService(
private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }
class MyAltCachingService(
private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }
class PostgreSQLLocalDb : SomeLocalDb { /* Implement methods */ }
class MongoLocalDb : SomeLocalDb { /* Implement methods */ }
Each step in mastering these principles etches the path to more intuitive, resilient software design. Whether you’re in the trenches of debugging or soaring through architectural decisions, these principles arm you with the clarity and agility to respond adeptly to evolving needs.
I appreciate investing your precious time in this exploration of essential software principles. I hope these insights reshape how you approach coding tasks, guiding you towards writing not just code, but enduring solutions.