I am starting to use Classes in VBA and appreciate some of the fantastic information that is already available on SO.
As far as I can tell, what seems to be lacking is an explanation of what the code in a class should or, as I suspect, should NOT do. For example:
Lets say I have a document and wish to insert / modify a table. In this example I'd like to:
With respect to 'sorting' I imagine that a class module would be well suited to determining the order that information should be put into a table based on some criteria.
But ideally:
OR
Or doesn't it matter? If there is a preferred way then what are the advantages / disadvantages of each approach?
Difference 2 – Number of copies When you create a variable in a normal module there is only one copy of it. For a class module, there is one copy of the variable for each object you create. For the normal module variable there will only be one copy of this variable in our application.
Standard Code Modules, also called simply Code Modules or just Modules, are where you put most of your VBA code. Your basic macros and your custom function (User Defined Functions) should be in these modules. For the novice programmer, all your code will be in standard modules.
Class module in VBA can be defined as the module that helps to create your own objects with your own properties and methods like worksheets and range objectives of excel. In simple terms with the help VBA class module, we can create objects with own properties.
You use the CodeModule object to modify (add, delete, or edit) the code associated with a component. Each component is associated with one CodeModule object.
First off, kudos for entering the wonderful rabbit hole of OOP!
Short answer: It depends.
(very) long answer:
You'll want to avoid pulling a worksheet [that exists at compile-time] from the Application.Worksheets
(or Application.Sheets
) collection, and use that sheet's CodeName
instead. VBA creates a global-scope object reference for you to use, named after every worksheet's CodeName
.
That's how this code gets to compile, with Sheet1
not being declared anywhere:
Option Explicit
Sub Test()
Debug.Print Sheet1.CodeName
End Sub
The problem with implementing worksheet-specific functionality anywhere other than in that worksheet's code-behind, using that global-scope "free" object variable, is that the separate module is now coupled with that Sheet1
object.
You want focused, cohesive modules - high cohesion. And low coupling.
By writing worksheet-specific code in another module (be it a standard or a class module), you're creating a dependency and increasing coupling, which reduces testability - consider this code in Class1
:
Public Sub DoSomething()
With Sheet1
' do stuff
End With
End Sub
Now Class1
can only ever work with Sheet1
. This would be better:
Public Sub DoSomething(ByVal sheet As Worksheet)
With sheet
' do stuff
End With
End Sub
What happened here? Dependency Injection. We have a dependency on a specific sheet, but instead of coding against that specific object, we tell the world "give me any worksheet and I'll do my thing with it". That's at method level.
If a class means to work with a single specific worksheet, and exposes multiple methods that do various things with that worksheet, having a ByVal sheet As Worksheet
parameter on every single method doesn't make much sense.
Instead you'll inject it as a property:
Private mSheet As Worksheet
Public Property Get Sheet() As Worksheet
Set Sheet = mSheet
End Property
Public Property Set Sheet(ByVal value As Worksheet)
Set mSheet = value
End Property
And now all methods of that class can work with Sheet
... the only problem is that the client code consuming that class now needs to remember to Set
that Sheet
property, otherwise errors can be expected. That's bad design IMO.
One solution could be to push the Dependency Injection Principle a notch further, and actually depend on abstractions; we formalize the interface we want to expose for that class, using another class module that will act as the interface - that IClass1
class doesn't implement anything, it just defines stubs for what's exposed:
'@Interface
Option Explicit
Public Property Get Sheet() As Worksheet
End Property
Public Sub DoSomething()
End Sub
Our Class1
class module can now implement that interface, and if you've been following this far, hopefully I don't lose you here:
NOTE: Module and member attributes are not visible in the VBE. They're represented here with their corresponding Rubberduck annotations.
'@PredeclaredId
'@Exposed
Option Explicit
Implements IClass1
Private mSheet As Worksheet
Public Function Create(ByVal pSheet As Worksheet) As IClass1
With New Class1
Set .Sheet = pSheet
Set Create = .Self
End With
End Function
Friend Property Get Self() As IClass1
Set Self = Me
End Property
Private Property Get IClass1_Sheet() As Worksheet
Set IClass1_Sheet = mSheet
End Property
Private Sub IClass1_DoSomething()
'implementation goes here
End Sub
This Class1
class module presents two interfaces:
Class1
members, accessible from the PredeclaredId
instance:
Create(ByVal pSheet As Worksheet) As IClass1
Self() As IClass1
IClass1
members, accessible from the IClass1
interface:
Sheet() As Worksheet
DoSomething()
Now the calling code can look like this:
Dim foo As IClass1
Set foo = Class1.Create(Sheet1)
Debug.Assert foo.Sheet Is Sheet1
foo.DoSomething
Because it's written against the IClass1
interface, the calling code only "sees" the Sheet
and DoSomething
members. Because of the VB_PredeclaredId
attribute of Class1
, the Create
function can be accessed via the Class1
default instance, pretty much exactly like Sheet1
gets accessed without creating an instance (UserForm classes have that default instance, too).
This is the factory design pattern: we're using the default instance as a factory whose role is to create and initialize an implementation of the IClass1
interface, which Class1
just so happens to be implementing.
With Class1
completely decoupled from Sheet1
, there's absolutely nothing wrong with having Class1
responsible for everything that needs to happen on whatever worksheet it's initialized with.
Coupling is taken care of. Cohesion is another problem entirely: if you find Class1
is growing hair and tentacles and becomes responsible for so many things you don't even know what it was written for anymore, chances are that the Single Responsibility Principle is taking a beating, and that the IClass1
interface has so many unrelated members that the Interface Segregation Principle is also taking a beating, and the reason for that is probably because the interface wasn't designed with the Open/Closed Principle in mind.
The above couldn't be implemented with standard modules. Standard modules don't play quite well with OOP, which means tighter coupling and thus lower testability.
There isn't one single "right" way to design anything.
IMO a standard module should only be used to expose entry points (macros, UDFs, Rubberduck test methods, and with Option Private Module
, some common utility functions), and contain fairly little code that merely initializes objects and their dependencies, and then it's classes all the way down.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With