There should be code in VBA that allows excluding document in class module
I am starting to use classes in VBA and am appreciating some of the fantastic information that is already available on SO.
As far as I can tell, what seems to be missing is an explanation of what the code in the class should do, or what I suspect it should NOT do. For example:
Suppose I have a document and want to insert / modify a table. In this example, I would like:
- check if table exists
- if the table doesn't exist:
- add a table at a specific location
- add information to the table (i.e. add rows)
- if the table exists
- add / remove information to / from the table
- sort the table according to some criteria
As far as "sorting" is concerned, I think a class module is well suited for determining the order in which information should be placed in a table based on some criteria.
But ideally:
- Should I use a class module (or a 2nd class module) to check and edit a document?
OR
- Would it be better to check and / or edit using a regular module?
Or does it not matter? If there is a preferred way, what are the advantages / disadvantages of each approach?
[Edit - next question here: Understanding classes in VBA - help improve these comments ]
source to share
First, the prestige to enter the wonderful PLO rabbit hole!
Short answer: It depends.
(very) long answer:
You need to avoid pulling a worksheet [that exists at compile time] from the collection Application.Worksheets
(or Application.Sheets
) and use that worksheet CodeName
. VBA creates a reference to a global scale object that you must use, named after each worksheet CodeName
.
The way this code will compile is Sheet1
not declared anywhere:
Option Explicit
Sub Test()
Debug.Print Sheet1.CodeName
End Sub
The problem with implementing only worksheet functionality other than that worksheet code using this "free" variable of the global scope object is that a separate module is now associated with that object Sheet1
.
Class module depending on the worksheet. Any worksheet.
You want focused, cohesive modules - high cohesion. And low communication.
By writing a specific worksheet in another module (be it a standard one or a class module), you create a dependency and increase the linkage, which reduces testability - consider this code in Class1
:
Public Sub DoSomething()
With Sheet1
' do stuff
End With
End Sub
Now Class1
can only work with Sheet1
. It 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 that specific object, we tell the world to "give me some worksheet and I'll do my thing with it." This is at the method level.
If a class means working with one specific sheet and provides multiple methods that perform different actions on that sheet, having a parameter ByVal sheet As Worksheet
for each individual method doesn't make much sense.
Instead, you'll be injecting 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 the methods of this class can work with Sheet
... The only problem is that the client code consuming this class must now remember Set
that the property is Sheet
, otherwise errors can be expected. This is bad design IMO.
One solution might be to take the Dependency Injection Principle further and actually depend on abstractions; we formalize the interface we want to expose for this class using another class module that will act as an interface - the class IClass1
doesn't implement anything, it just defines stubs for what is displayed:
'@Interface
Option Explicit
Public Property Get Sheet() As Worksheet
End Property
Public Sub DoSomething()
End Sub
Our class module Class1
can now implement this interface, and if you've followed this far, hopefully I don't lose you here:
NOTE. Module and member attributes are not visible in VBE. They are presented here with the 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 class module Class1
presents two interfaces:
-
Class1
available from instancePredeclaredId
:-
Create(ByVal pSheet As Worksheet) As IClass1
-
Self() As IClass1
-
-
IClass1
accessible from the interfaceIClass1
:-
Sheet() As Worksheet
-
DoSomething()
-
Now the calling code might look like this:
Dim foo As IClass1
Set foo = Class1.Create(Sheet1)
Debug.Assert foo.Sheet Is Sheet1
foo.DoSomething
Because it is written against an interface IClass1
, the calling code only "sees" the Sheet
and members DoSomething
. Because of the attribute VB_PredeclaredId
Class1
, a function Create
can be accessed through a Class1
default instance , much like Sheet1
it can be accessed without instantiation (the UserForm classes also have this default instance).
This is the factory design pattern: we use the default instance as a factory, whose role is to create and initialize the implementation of an interface IClass1
that Class1
just happens.
If Class1
completely disconnected from Sheet1
, there is absolutely nothing wrong with being in Class1
charge of everything that needs to happen on any sheet that it initialized with.
Deals with communication. Cohesion is another problem: if you find yourself Class1
growing hairs and tentacles and becoming responsible for so many things that you don't even know about, which he wrote more about, chances are that the principle of single responsibility will hit, and that the interface IClass1
has there are so many unrelated members that the Segregation Principle also takes a beating, and the reason for this is probably because the interface was not designed with Open / Closed in mind.
The above cannot be implemented with standard modules. Standard modules don't work very well with OOP, which means tighter coupling and hence lower testability.
TL; DR:
There is more than one "correct" way to create something.
- If your code can be tightly coupled to a particular worksheet , prefer to implement the functionality for that worksheet in that worksheet for better cohesion . Still use specialized objects (classes) for specialized tasks: If your working code is responsible for setting up a database connection, sending a parameterized query over the network, fetching the results and dumping them into a worksheet, then you are doing it Wrong ™ and now testing it code in isolation, without getting into the database, is impossible.
- If your code is more complex and cannot provide a close relationship to a specific worksheet or if the worksheet does not exist at compile time, implement the functionality in a class that can work with any worksheet and they have a class responsible for the model of that worksheet created at run time.
IMO a standard module should only be used to expose entry points (macros, UDFs, Rubberduck test methods and Option Private Module
some common utility functions) and contain quite a bit of code that just initializes objects and their dependencies, and then it goes away completely.
source to share