The computer, phone or tablet you're reading this on is hardware. The browser that renders it is software. Firmware is somewhere in between. But is it that simple?
I've recently read Clean Architecture by Uncle Bob and loved his definitions of software and hardware:
- hardware is anything that is hard, rigid, difficult to modify
- software on the contrary is soft, malleable, easy to modify
- firmware is firm, not as hard as hardware, nor as soft as software
These definitions might sound simplistic, but their simplicity helps greatly in understanding software development.
When developing software you can't switch out physical components of the machine on which the software is running, nor can you know when the user will do so, so you must develop your software to be as independent of the hardware as possible. You cannot switch out the operating system or drivers of the machine running your software either, so you must ensure that this is separated from your software as well. Some frameworks like Android's java framework, C#'s .NET framework could also be referred to as firmware.
Of course when developing you will have to connect to some hardware or firmware. The important thing to remember is that you want these connections to be isolated from the domain, ideally as a plugin to your software. This will mean that your software can easily be ported to another hardware/firmware without too much work.
But the advantage doesn't only lie in the ease of porting the software. It also lies in the ease of modifying and testing of the software. If the hardware or firmware updates, you have a lot less code to modify and inversely if you modify business logic, you don't need to worry about hardware and firmware bridges. Unit testing pure OO logic is easy to do, but unit testing hardware and firmware is very difficult. This is as true for web developers or game developers as it is for embedded developers.
So what can you do to reduce the coupling between software and firmware/hardware? Move all the code that refers to firmware into a separate project/assembly/module and make it implement an interface that is defined in your software. You'll probably notice you can refactor a lot of the code you just extracted because there is a lot of duplication.
I'll end this with a short example. As many of you know, there's been an update to the GDPR and software must now ask for permissions to use certain data (instead of having those permissions granted to us implicitly). Here's an example of Android code that shows a popup to the user if he hasn't answered the question yet, saves his answer and let's him continue if he accepts or logs him out of the system if he refuses.
//...
public class LoginActivity {
//...
public void login(User user) {
//...
if (user.complianceAcceptanceIsValid()) {
Intent = new Intent(context, DashboardActivity.class);
startActivity(intent);
return;
}
new AlertDialog.Builder(context)
.setTitle("Compliance Acceptance")
.setMessage(R.strings.gdpr_compliance)
.setPositiveButton("Accept", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
user.onComplianceAccepted(Calendar.getInstance().getTime());
Intent intent = new Intent(context, DashboardActivity.class);
startActivity(intent);
}
})
.setNegativeButton("Decline", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
user.logout();
Intent intent = new Intent(context, LoginActivity.class);
startActivity(intent);
}
})
.create()
.show();
}
}
Now what is the problem with this code? You have logic code pertaining to the compliance (checking if the user already answer the question, saving the answer, logging the user out) mixed in with an Android Dialog, mixed in with what to do after the user answer the dialog. It's very hard to test, and if Android decides to change the API for the AlertDialog, or even remove it, you will probably have a lot of changes to make.
So how can we fix this? By moving the Dialog part into another module:
//...
public class LoginActivity {
private ComplianceDialog complianceDialog;
//...
public void login(User user) {
//...
complianceDialog.askForCompliance(user, new Dialog.Handler() {
@Override
public void onAccept() {
Intent intent = new Intent(this, DashboardActivity.class);
startActivity(intent);
}
@Override
public void onDecline() {
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
}
}
}
//...
public class ComplianceDialog {
private Dialog dialog;
public ComplianceDialog(Dialog dialog) {
this.dialog = dialog;
}
public void askForCompliance(User user, final Dialog.Handler handler) {
if (user.complianceAcceptanceIsValid()) {
handler.onAccept();
return;
}
dialog.show("Compliance Acceptance",
R.strings.gdpr_compliance,
"Accept",
"Decline",
new Dialog.Handler() {
@Override
public void onAccept() {
user.onComplianceAccepted(Calendar.getInstance().getTime());
handler.onAccept();
}
@Override
public void onDecline() {
user.logout();
handler.onDecline();
}
}));
}
}
public interface Dialog {
void show(String title, String message, String accept, String decline, Dialog.Handler handler);
public static class Handler {
public void onAccept() { }
public void onDecline() { }
}
//...
public class DialogImpl implements Dialog {
private Context context;
public Dialog(Context context) {
this.context = context;
}
public void show(String title, String message, String accept, String decline, Dialog.Handler handler) {
new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(accept, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
handler.onAccept();
}
})
.setNegativeButton(decline, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
handler.onDecline();
}
})
.create();
.show();
}
}
Now all the user interaction is within one class (ComplianceDialog), the dialog showing is in another class (DialogImpl) and the next activity responsibility is given yet another class (LoginActivity), meaning it can be reused more easily. The ComplianceDialog does not depend on the Android firmware at all. This also allows us to easily test the ComplianceDialog class by mocking or faking the Dialog interface we pass in. The Dialog interface can also be reused anywhere we need a yes/no dialog to be shown (which often happens) so we can probably refactor a lot of the other uses of AlertDialog in the code with this pattern without too much work.