There are a few practices that will help you write solid, maintainable, reusable code. A lot of software practitioners of greater fame than mine have written about these before, but it doesn't hurt to repeat good advice.
Note that I in no way claim this to be the one and only true way to software development nirvana, but just some practices that work for me. Your own opinions and experiences on the topic will be greatly appreciated, so feel free to comment. |
Keep It Simple, Stupid Reusable code should be kept low-feature. It can be a challenge to avoid feature creep and dependency bloat, so make an effort to follow the KISS rule. The more sophisticated stuff you add, the more you will need to "fix" and not to forget: Test! - when inheriting from the class.
Design for extendabilityWhen you design a reusable class, you really have to define the scope of what you want to achieve. Decide on the basic feature set, then evaluate the possible offspring to this class and conceivable additional features, but to adhere to the KISS rule - you should not add the features until you need them. Instead, spend a little time on evaluating the impact of the features and put in any necessary hooks and leave enough comments to allow you to rediscover why you put them in in the first place.
Later, if you absolutely have to add a feature and cannot add it in an inherited class - make sure you don't break existing behavior in class instancing. A common mistake is to change a default property value in the base class constructor, which some derived class assumed had a specific state or value.
...and on the point of assumptions...
Never Assume, Always AssertI bet you never heard that one before! :) Use Asserts to check your assumptions. It will save you from a number of embarrassing moments of broken code. The purists probably want this done in unit testing, but it can help add clarity to your code if it is done in the actual library code. Think of it as
design by contract.
procedure TSimpleClass.ActOn(aParameter:TSomeClass; aValue:Integer);
begin
// Ensure aParameter is assigned and of correct type
Assert(Assigned(aParameter) and (aParameter is TSomeClass));
// and that aValue is in legal range
Assert(aValue >= 0);
...
Using asserts like these will help you catch runtime problems faster. They also will remind you of what conditioning you need to do before you call the method, and what assumptions you can make within the routine itself.
Avoid DependenciesWhen you start pulling in code from other libraries or classes, you should consider the level of dependency that you introduce. A simple extra nice-to-have attribute may come with a payload of extra code that hardly ever will be called. If possible, put the extra attribute in a separate descendant class in a separate unit. The less code you have to compile in, the better.
Embrace PolymorphismWhen you create classes that are intended as foundation for descendant classes, you should consider how to best support polymorphic instancing.
For example - if you are writing a container class that will hold a polymorph collection of elements - you may want to have a virtual class function that return the default class type used to instance new elements in the container, instead of just instancing the base element class. Doing so will allow descendant classes make reliable assumptions about the safety of typecasting within the extended methods in the descendant container class.
unit BasicClass;
interface
type
TElementClass = class of TBaseElement;
TBaseElement = class(TObject)
end;
TBaseContainer = class(TObject)
class function ElementClass:TElementClass; virtual;
function ValidElement(anElement:TBaseElement):Boolean;
end;
implementation
{ TBaseContainer }
class function TBaseContainer.ElementClass: TElementClass;
begin
Result := TBaseElement;
end;
function TBaseContainer.ValidElement(anElement: TBaseElement): Boolean;
begin
Result := anElement is ElementClass;
end;
end.
"But..." - you will say;
"you can just override methods in the descendant class for the same effect?". Yes, you can and it works well when you add new functionality, but what about functionality that already has been implemented in class higher in your inheritance tree?
unit AdvancedClass;
interface
uses
BasicClass;
type
TAdvancedElement = class (TBaseElement)
end;
TAdvancedContainer = class(TBaseContainer)
class function ElementClass:TElementClass; override;
end;
implementation
{ TAdvancedContainer }
class function TAdvancedContainer.ElementClass: TElementClass;
begin
Result := TAdvancedElement;
end;
end.
The base class won't have an understanding of it's descendants, so if you want it to be able to work with content from descendant classes, you would either have to override/redo the functionality or - do as above - allow the base class to work with their own child classes as well.
program ExtendableClasses;
{$APPTYPE CONSOLE}
uses
SysUtils,
BasicClass in 'BasicClass.pas',
AdvancedClass in 'AdvancedClass.pas';
procedure Main;
var
Element1, Element2 : TBaseElement;
Container1, Container2 : TBaseContainer;
begin
Container1 := TBaseContainer.Create;
Container2 := TAdvancedContainer.Create;
Element1 := Container1.ElementClass.Create;
Element2 := Container2.ElementClass.Create;
Writeln('E1: ', Element1.ClassName);
Writeln('E2: ', Element2.ClassName);
Writeln('E1 valid in C1: ', Container1.ValidElement(Element1));
Writeln('E2 valid in C1: ', Container1.ValidElement(Element2));
Writeln('E1 valid in C2: ', Container2.ValidElement(Element1));
Writeln('E2 valid in C2: ', Container2.ValidElement(Element2));
end;
begin
try
try
Main;
except
on E:Exception do
Writeln(E.Classname, ': ', E.Message);
end;
finally
Write('Press Enter: ');
Readln;
end;
end.
This particularly goes for dialogs and forms that use a polymorph class - make a virtual class function to enable overriding the class instancing. Avoid the rigidly defined TSomeInheritableClass.Create constructs and call the virtual class type function instead.
Note on constructors When I finally "got" OOP as opposed to structured programming, it was hard to drop the structured way of thinking when it came to instancing. Most of my classes would only have a constructor that took a ton of parameters to save a few lines at the point of instancing that class.
While this might work well for monomorphic classes, it really will not be good for a class hierarchy, and it took me some time to get to the habit of having a parameterless Create. Today, I almost wish for a compiler hint if I should happen to write constructors that require parameters. |
Hide detail with LayersThink in APIs and layers and try to propagate only content between the layers, and not details that are specific to connectivity, storage, etc. A good layered design will allow you to rip out and replace parts of the architecture without massive rewrites.
When you design your APIs or layers, you should consider leaving space or hooks for piggybacking in more content in yet-to-be defined formats where appropriate.
Isolate Data from the GUIThe less your GUI know about your data, the better. Vise versa, the less your data knows about the GUI, the better. Use a mediating class that does the job of knowing both and how to tie them together. This will ease the job of making the data structures as well as the GUI reusable.
It can be particularly useful to tie your business logic to your data and avoid implementing biz.logic in the GUI presentation. Your data will be easier to move to a different presentation form when the rules are not hard-coded in some draw routine. You don't want to have duplication of biz.logic code between the display routines, the report routines, the export routines, etc.
-
Thats it for now. More on the topic of Layers and GUI abstraction to follow.
Also, part 3 of the reusable grid.