Lesson 9 - Shared class members in VB.NET
In the previous exercise, Solved tasks for OOP in VB .NET lessons 5-8, we've practiced our knowledge from previous lessons.
In the previous lesson, Solved tasks for OOP in VB .NET lessons 5-8, we put our knowledge of inheritance and polymorphism to the test. In today's tutorial, we're going to go over Shared class members in VB.NET (those members are often called static in other languages). Until now, we only used data, states, being carried by an instance. Fields, which we've defined, belonged to an instance and were unique to each instance. OOP, however, allows us to declare fields and methods on a class itself. We call these members Shared, sometimes called class members, who are independent of an instance.
WARNING! Today's lesson will show you that Shared members are approaches that actually violate the object-oriented principles. OOP includes them only for special cases and in general everything can be written without Shared members. We always have to think carefully before adding Shared members. Generally, I would recommend for you not to use Shared members ever, unless you're absolutely sure what you're doing. Like global variables, which VB.NET, fortunately, doesn't have, Shared members are something that enable you to write bad code and violate good practices. Today, we'll go over this topic just to make you understand some static methods and classes in .NET, not to write your own. Use this knowledge wisely, there will be less evil in the world.
Shared (class) fields
We can declare various elements as Shared, let's start with fields. As I mentioned in the introduction, Shared elements belong to a class, not to an instance. So the data stored there can be read even if an instance of the class has not been declared. Basically, we could say that Shared fields are shared among all class instances, but even that wouldn't be accurate since they aren't actually related to instances. Let's create a new project, name it something like Shared, and create a simple User class:
Public Class User Private name As String Private password As String Private loggedIn As Boolean Public Sub New(name As String, password As String) Me.name = name Me.password = password loggedIn = False End Sub Public Function LogIn(enteredPassword As String) As Boolean If enteredPassword = password Then loggedIn = True return True Else Return False ' wrong password End If End Function End Class
The class is simple, it represents a user in a system. Each user instance has its own name, password, and carries information about whether the user is logged in. In order for the user to log-in, we call the LogIn() method which takes a password as a parameter. The method verifies whether it's the right password. If the person behind the keyboard is really that user, it logs him/her in. It returns True/False depending on whether the login was successful. In reality, the password would be also hashed, but we won't do any hashing this time around.
When a new user registers, the system tells him/her what the minimum length of their password must be. This number should be stored somewhere. During user registration, we still wouldn't have its instance. The object has not been created and would only be created after the form has been completely filled and submitted. Therefore, we can't use a public minimalPasswordLength field in the User class for this purpose. Either way, we still need a minimum password length stored in the User class somewhere since it, naturally, belongs there. We'll make this value a Shared field using the Shared modifier:
Public Class User Private name As String Private password As String Private loggedIn As Boolean Public Shared minimalPasswordLength As Integer = 6 ... End Class
Now, let's switch to Module1.vb and print the field to the console. We access this field directly on the class:
{VBNET_OOP}
Public Class User
Private name As String
Private password As String
Private loggedIn As Boolean
Public Shared minimalPasswordLength As Integer = 6
Public Sub New(name As String, password As String)
Me.name = name
Me.password = password
loggedIn = False
End Sub
Public Function LogIn(enteredPassword As String) As Boolean
If enteredPassword = password Then
loggedIn = True
Return True
Else
Return False ' wrong password
End If
End Function
End Class
{VBNET_MAIN_BLOCK}
Console.WriteLine(User.minimalPasswordLength)
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
We can see the field really belongs to the class. We can access it from different places in the code without having to create a user. On the contrary, we wouldn't be able to access it on the user instance:
Dim u As User = New User("Thomas White", "passwordissword") Console.WriteLine(u.minimalPasswordLength) ' this line causes an error
Visual Studio will report an error and the code won't be compiled.
Another practical use of Shared fields is to assign unique identification numbers to each user. If we didn't know what Shared members were, we'd have to check every user creation and increment a counter. Instead, we'll create a private Shared field nextId right on the User class. The next user who registers will have their id stored there. The first user's id will be 1, the second 2, and so on. The User will get a new field - id, that will be set in the constructor depending on what the nextId value is:
Public Class User Private name As String Private password As String Private loggedIn As Boolean Private id As Integer Public Shared minimalPasswordLength As Integer = 6 Private Shared nextId As Integer = 1 Public Sub New(name As String, password As String) Me.name = name Me.password = password loggedIn = False id = nextId nextId += 1 End Sub ... End Class
The class stores the next instance's id by itself. We assign this id to a new instance in the constructor and increase it by 1, which prepares it for the next instance. Not only fields can be Shared, this approach could be applied to a variety of class members.
Shared methods
Shared methods are called on the class. All they usually are is utilities that we need to use often and creating an instance every time would be counterproductive. VB.NET implicitly uses Shared members. Have you noticed how we never have to create a console instance in order to write something to it? The WriteLine() method on the Console class is Shared. We're only allowed to run one console at a time, so it would be pointless to create an instance every time we wanted to use it. We've already experienced a similar situation with the Round() method in the Math class. It would be pointless to instantiate a Math class to do such a simple thing as rounding. Shared methods are generally just there to help us where the instantiation is counterproductive or makes no sense at all.
Let's make another example, just to clarify. During user registration, we need to know the minimum password length before we create its instance. Also, it would be great if we could check the password before the program creates the user. A full check, in this case, would include checking whether the length is correct, making sure it doesn't contain accent characters, verifying that there is at least one number in it, and so on. To do this, we'll create a Shared ValidatePassword() method:
Public Shared Function ValidatePassword(password As String) As Boolean If password.Length >= minimalPasswordLength Then ' password validation code (omitted for simplification purposes) Return True End If Return False End Function
We'll call this method on the User class:
{VBNET_OOP}
Public Class User
Private name As String
Private password As String
Private loggedIn As Boolean
Private id As Integer
Public Shared minimalPasswordLength As Integer = 6
Private Shared nextId As Integer = 1
Public Sub New(name As String, password As String)
Me.name = name
Me.password = password
loggedIn = False
id = nextId
nextId += 1
End Sub
Public Function LogIn(enteredPassword As String) As Boolean
If enteredPassword = password Then
loggedIn = True
Return True
Else
Return False ' wrong password
End If
End Function
Public Shared Function ValidatePassword(password As String) As Boolean
If password.Length >= minimalPasswordLength Then
' password validation code (ommited for simplification purposes)
Return True
End If
Return False
End Function
End Class
{VBNET_MAIN_BLOCK}
Console.WriteLine(User.ValidatePassword("passwordissword"))
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
Viewer beware! The ValidatePassword() method belongs to the class. Meaning that we can't access any instance fields in it. Those fields don't exist in the class context, rather in the context of an instance. It wouldn't make any sense to use the user's name in our method! You can try it out if you'd like to get a visual confirmation of its impossibility.
Password validation can be achieved without knowledge of Shared members. We could create a UserValidator class and write methods in it accordingly. Then, we'd have to create its instance to be able to call those methods. It'd be a bit confusing because the "concept of a user" would be unnecessarily split up into two classes. Now, thanks to Shared members, all of the information is neatly organized in one place.
Thinking back, we really don't want the minimalPasswordLength Shared field to be accessible from outside the class. What we'll do is make it private and create a Shared get method for reading. We know this approach well from previous lessons. Let's add the get method that retries both the minimum password length and the instance's id:
Public Shared Function GetMinimalPasswordLength() As Integer Return minimalPasswordLength End Function Public Function GetId() As Integer Return id End Function
Module1.vb should look something like this now:
{VBNET_OOP}
Public Class User
Private name As String
Private password As String
Private loggedIn As Boolean
Private id As Integer
Private Shared minimalPasswordLength As Integer = 6
Private Shared nextId As Integer = 1
Public Sub New(name As String, password As String)
Me.name = name
Me.password = password
loggedIn = False
id = nextId
nextId += 1
End Sub
Public Function LogIn(enteredPassword As String) As Boolean
If enteredPassword = password Then
loggedIn = True
Return True
Else
Return False ' wrong password
End If
End Function
Public Shared Function ValidatePassword(password As String) As Boolean
If password.Length >= minimalPasswordLength Then
' password validation code (omitted for simplification purposes)
Return True
End If
Return False
End Function
Public Shared Function GetMinimalPasswordLength() As Boolean
Return minimalPasswordLength
End Function
Public Function GetId() As Integer
Return id
End Function
End Class
{VBNET_MAIN_BLOCK}
Dim u As User = New User("Thomas White", "passwordissword")
Console.WriteLine("First user's ID: {0}", u.GetId())
Dim v As User = New User("Oli Pickle", "csfd1fg")
Console.WriteLine("Second user's ID: {0}", v.GetId())
Console.WriteLine("Minimum password length: {0}", User.GetMinimalPasswordLength())
Console.WriteLine("Password validation ""password"": {0}", User.ValidatePassword("password"))
Console.ReadKey()
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
The output will be:
Console application
First user's ID: 1
Second user's ID: 2
Minimum password length: 6
Password validation "password": True
Shared constructor
The class could also have a Shared constructor. A Shared constructor is called when the application starts and the class is being registered for use. It can be used for initialization, calculations and so on. Similarly to instance constructors, we are able to create instances of classes and store them into Shared fields in Shared constructors.
Private constructor
If a class contains only utility methods or it doesn't make sense to create an instance of it, for example we'll never run two consoles at a time, we can declare its instance constructor as private. Keep in mind, we can't instantiate such a class (create its instances). Two similar classes that we're already familiar with are Console and Math. Let's attempt to create an instance of the Math class:
Dim m As Math = New Math() ' this code causes error
VS will report an error. Shared classes have all their members set to Shared, so it wouldn't make any sense to create an instance of it because the result would be an empty object.
Static registry
Let's create a simple Shared class with a private constructor. We could create a class containing only utility methods and fields, e.g. Math. However, I've decided I'll have us create a static registry. Which is basically how you share important data between classes without making an instance.
Let's say we're making a more robust application, e.g. a digital journal. The application would be multilingual, its features would include adding bookmarks, choosing folders for storing files, color schemes and even running at system startup. It would have adjustable settings, e.g. picking the first day of the week (Sunday/Monday), which would be accessed from various parts in the code. Without knowledge of Shared members, we'd have to pass the settings instance to all objects (calendar, tasks, notes...) through the constructor.
One workaround would be to use a class of Shared members to store these settings. It would be accessible everywhere in the program, without even having to create an instance. It would contain all the necessary settings that would be used by objects. Our Shared class would end up looking something like this:
Public Class Settings Private Shared language As String = "ENG" Private Shared colorScheme As String = "red" Private Shared runAtStartUp As Boolean = True Private Sub New() End Sub Public Shared Function GetLanguage() As String Return language End Function Public Shared Function GetColorScheme() As String Return colorScheme End Function Public Shared Function GetRunAtStartUp() As Boolean Return runAtStartUp End Function End Class
All fields and methods must be declared with the Shared modifier, also notice the private constructor. I purposely didn't use the public class fields, and made get methods instead, so the values wouldn't be able to be changed. Which is still a bit uncomfortable from a programmer's point of view. Next time we'll learn how to go about doing this in a more efficient way.
Now we'll put our class to use even though we don't have a full journal application. Let's make a Calendar class and verify that we can truly access the Settings there. We'll add a method to it that returns all of the settings:
Public Class Calendar Public Function GetSettings() As String Dim s As String = "" s &= String.Format("Language: {0}{1}", Settings.GetLanguage(), vbCrLf) s &= String.Format("Color scheme: {0}{1}", Settings.GetColorScheme(), vbCrLf) s &= String.Format("Run at start-up: {0}{1}", Settings.GetRunAtStartUp(), vbCrLf) Return s End Function End Class
Last of all, print it all to the console:
{VBNET_OOP}
Public Class Settings
Private Shared language As String = "ENG"
Private Shared colorScheme As String = "red"
Private Shared runAtStartUp As Boolean = True
Public Shared Function GetLanguage() As String
Return language
End Function
Public Shared Function GetColorScheme() As String
Return colorScheme
End Function
Public Shared Function GetRunAtStartUp() As Boolean
Return runAtStartUp
End Function
End Class
Public Class Calendar
Public Function GetSettings() As String
Dim s As String = ""
s &= String.Format("Language: {0}{1}", Settings.GetLanguage(), vbCrLf)
s &= String.Format("Color scheme: {0}{1}", Settings.GetColorScheme(), vbCrLf)
s &= String.Format("Run at start-up: {0}{1}", Settings.GetRunAtStartUp(), vbCrLf)
Return s
End Function
End Class
{VBNET_MAIN_BLOCK}
Dim calendar As Calendar = New Calendar()
Console.WriteLine(calendar.GetSettings())
Console.ReadKey()
{/VBNET_MAIN_BLOCK}
{/VBNET_OOP}
Console application
Language: ENG
Color scheme: red
Run at start-up: True
We see that calendar instance has no problem accessing all program settings.
Again, be careful, this code can be improperly used to share unencapsulated data. We must only use it in specific situations. Most data exchanges should happen using parameterized instance constructors, not through Shared members.
Shared members appear very often in design patterns, we've already mentioned them in our lessons. We'll get in further detail in the approaches that bring object-oriented programming to perfection later on. For now, this will do. I don't want to overwhelm you In the next lesson, Solved tasks for OOP in VB .NET lesson 9, we'll look at what properties are in VB.NET.
In the following exercise, Solved tasks for OOP in VB .NET lesson 9, we're gonna practice our knowledge from previous lessons.