µObjects: Where our code meets the user
There's a couple types of places where the code we write interacts with the code someone else wrote. This could be another team, a 3rd party library, or the operating system. When we interact with these components, we're tying our code to their code in some fashion.
The more these libraries are referenced in our code; the tighter we're coupling our code to their code. The issue here is that ... Their code doesn't care about ours.
We've covered interacting with the Operating System and 3rd party libraries.
Finally we have the UI. I feel the UI is the trickiest of the bookends to deal with in an Object Oriented code base. My challenges with UI frameworks is that they are not written for OO code. They're written with a specific, or set of, UI styles in mind. Often locking down the classes as tightly as possible.
Microsoft is a huge offender with this. The UI classes are normally sealed
and the interfaces internal
. It really limits the options available to isolate their code; but it can be done.
We want to isolate the UI as much as possible away from our code. This is the same tactic as applied to the OS and libraries.
This separation gives us a couple things. It's the same big two I bring up a lot - Testability and Maintainability.
For testability; we want an interface. This means that our code only interacts with code we control; which makes it maintainable.
The UI is a place I'll make the argument about swapping out for something else. We may want to change from a TextBox to a Label. Or to a custom control. These changes; when you have an interface for the rest of the code; are much easier to do. There's no need to hunt down all the places that may use it; not compile error hunting.
Change the UI; change the implementation of the interface and BAM! code continues to work.
A lot of the time the UI layer is untestable via Unit Test. UWP has an InitializeComponent
call that throws exceptions when called from a unit test. For this; we want to avoid any logic in a class that can't be put under unit test.
I don't do this as extreme as I'd like - There's a cost to FULL removal of logic from the untestable.
Some ternary statements exist. Largely to toggle visibility of a control. Nothing more than a ternary exists though. As soon as it gets more complex; we remove it.
I'd like to even remove the ternary; and have objects do it; but... I start to feel it's an abstraction without a good enough reason.
Custom Controls
The way most UI's are designed require us to build custom controls. Even if it's a wrapper around an existing control (Interface Overloading - TDD Against UI).
With the µObjects approach; we treat each control as an object with it's own behavior.
This allows us to not have Page (or Activity, or whatever iOS calls theirs) that interacts with the elements on the page directly. All code is acting through the interface of the control; ASKING it to do something. Sometimes nothing has to be asked.
A simple example of this is a login page with a "Remember Username" checkbox.
The two controls we're interested in is the Username textbox and the mentioned checkbox.
A typical example of setting up the remember checkbox (using some of my old android code) is going to look like
...
private void registerUiWidgets{
...
chkRemember = (CheckBox) rootView.findViewById(R.id.remember);
txtUsername = (TextBox) rootView.findViewById(R.id.username);
...
}
...
private void prepareRemeber(){
chkRemember.setChecked(appSettings.getLoginRemember());
chkRemember.setOnCheckedChangeListener((checkBox, checked) -> {
appSettings.setLoginRemember(checked);
});
}
...
prviate void prepareUserName(){
String username = appSettings.getRememberedUsername();
if (username.length() > 0) {
txtUsername.setText(username);
} else {
txtUsername.setText(R.id.username_hint);
}
}
...
This is a lot of logic inside an untestable class.
I have a very µObject approach to this; obviously. It's treating the CheckBox and the TextBox as Objects. They take care of themselves. This is a wrapped example; there are other approaches I've done. (Deal with my I
s)
public interface ISetText{
public void setText(CharSequence str);
}
public interface IResource{
public void into(ISetText item);
}
public interface IUsernameTextBox extends ISetText{
//Something like; into(ISetText item); but not in this example
}
public interface IAppSetting{
Boolean hasUsername();
void usernameInto(ISetText item);
}
public interface IUsernameLoadAction{
void act(IUsernameTextBox ctrl);
}
public class UsernameTextBox extends TextBox implements IUsernameTextBox{
private final IUsernameLoadAction _usernameLoadAction;
public UsernameTextBox(){
this(new UsernameLoadAction());
}
public UsernameTextBox(IUsernameLoadAction usernameLoadAction){
base(...);//I forgot how Java calls to base ctor
_usernameLoadAction = usernameLoadaction;
}
@Override
public void onWhateverWhenCtrlIsInitialized(){
_usernameLoadAction.act(this);
}
}
//Understands the chain of actions to load saved username
public class UsernameLoadAction implements IUsernameLoadAction{
private final IUsernameLoadAction _nextAction;
public UsernameLoadAction(){
this(new SetHintUsernameLoadAction(new SetSavedUsernameLoadAction(new NoOp());
}
public UsernameLoadAction(IUsernameLoadAction nextAction){
_nextAction = nextAction;
}
void act(IUsernameTextBox ctrl){
_nextAction.Act(ctrl);
}
}
public class SetHintUsernameLoadAction implements IUsernameLoadAction{
private final IUsernameLoadAction _nextAction;
private final IResource _resource;
public SetHintUsernameLoadAction(IUsernameLoadAction nextAction){
this(nextAction, new Resource(R.string.username_hint))
}
public SetHintUsernameLoadAction(IUsernameLoadAction nextAction, IResource resource){
_nextAction = nextAction;
_resource = resource;
}
void act(IUsernameTextBox ctrl){
_resource.into(ctrl)
_nextAction.act(ctrl);
}
}
public class SetSavedUsernameLoadAction implements IUsernameLoadAction{
private final IUsernameLoadAction _nextAction;
private final IAppSettings _appSettings;
public SetSavedUsernameLoadAction(IUsernameLoadAction nextAction, IAppSettings appSettings){
_nextAction = nextAction;
_appSettings = appSettings;
}
void act(IUsernameTextBox ctrl){
if(_appSettings.hasUsername()){
_appSettings.userNameInto(ctrl);
}
_nextAction.act(ctrl);
}
}
A common reaction to seeing true Object Oriented Code is, "holy hell - that's a lot of classes".
This extremely simplified example has 5 interfaces and 4 classes. And this is just for loading the Username Textbox.
In a project at work; our login code has 79 classes. I know the exact count because an architect looked at it and his (admitted) knee-jerk reaction was, "79 classes, why so many?!".
I agree; OOP causes a class explosions. I expect µObjects to be a little more explodey - but not too much. This explosion is because everything has a job. It has a responsibility. It does it's one thing; Single Responsibility to the Extreme.
Unlike our first example; we can now test every aspect of our logic. We can KNOW that it all works. This is a HUGE improvement. I'll never have to worry if I broke the Username loading - I run my tests and know that it works.
We can see the isolation of the actual UI element, TextBox
. It's not used anywhere outside of the UsernameTextBox
. This is continues the trend set in isolating Operating System calls and avoiding Asymmetric Marriage - isolate the undesirable into a single location and remove all logic from that location.
When we do that with code we don't control; we vastly increase the maintainability; and, subsequently, the lifespan of the application.
The UI is always going to be a challenge while it's designed for non-oop. Things like databinding and data models kill the OOP approach and decrease the maintainability of code.
Many things can increase the speed of getting functionality out the door... but changing it later suffers.
I don't have a lot of details for the UI; the approach, unlike Operating System calls and avoiding Asymmetric Marriage, is going to be platform and UI implementation specific. Winforms will be different than XAML will be different from ASPX will be different from Andriod Activities will be different from iOS' whatever.
Applying the Single Responsibilit Principle and letting the widget control it's own behavior will vastly improve your code base. The changes it forces into existence will continue to cascade through the code and the joy of µObjects will be understood - even if it hurts a little at first.