Libor Bešenyi (Solution Architect)
Let’s have a look at some techniques in OOP, which can be applied in real modelling of relationships rewritten into computer language. The best known techniques, although they may appear trivial, hide awesome power in OOP and enable abstraction of complex real-life situations within a couple of lines of source code.
Unfortunately, they are often misused and instead of clarity and readability of the model we get a huge confusing mess. As it is with every useful thing in life, when OOP is improperly used, it can cause more problems than good. Let us therefore dwell on these methods for bit and let’s think first, before plunging into programming. It tends to be a difficult task for programmers, however, this small investment at the beginning really pays off!
PS: Whenever you are lost in your classes, you hide or overlap inherited parts, it’s the first sign that your object model does not reflect reality ... That does not mean that there is no object model for reality. It’s worth to go back to diagrams and think about things from a different angle.
Encapsulation
What do we mean by encapsulation? It means hiding something before the class consumer. For example, a car does not move without the engine. The consumer is the driver, who does not buy the engine but the car. He does not care how the car works. Encapsulation works precisely on this principle. If the consumer does not want to (or should not or need not) know how the class works and only needs to use it, we can encapsulate complex logic in new packaging. It is then important to create an interface to match the consumer needs without having to start editing the encapsulated logic. We therefore need to create a comfortable interface.
Encapsulation layer thus creates an "extra" layer- however, by its appropriate use we can also get a "helper," in case it is necessary to replace the "engine" in the future. Business logic tends to be encapsulated too (this is not an OOP encapsulation anymore, although the principle is similar). All business systems use database systems to store data in the database. Unencapsulated access may be that throughout the application, where we need to save or load data, we directly access the database. Thus, our logic can "run around" the whole program - not to mention the duplication or interference of another member we did not know about. For example, there is a new rule - when the invoice is saved, information must be written in the log. One programmer programs in the module "AddInvoice". But the other’s task is to write a new module "ImportInvoices". When importing, of course, new invoices are created - but because it is done by another programmer, who was unaware of the request to log the record, the logging would be missing during the direct access to the database in this case (which would come up during live operation and detecting errors and repair would be rather expensive).
In case of encapsulated principle (let’s ignore store procedures / stored proc / for now) one global class would be reserved for communication with the database, which should have programmed methods such as AddInvoice (). In case of logging requirement, it would not be programmed for a specific (each) module, but only in the method (because all of the modules would save the invoices right through this method). This reduces the risk of working in a team (of ignorance of how the internal processes work - not everyone can know everything about robust systems).
In this case, centralization and encapsulation of work with database is also advantageous, if we choose for example to change the database system. Then we would edit only the "inside" of procedures and we would not need to interfere with the already tested modules - they would still call the same interface, the modules would not have to be concerned with what would be happening inside.
As for the encapsulation within the OOP, we may mention for example the operation of some special device. If the program has to control some special device, we will create a class where the communication with the device is programmed. The functioning of the process may be of importance for the consumer. Imagine a situation that a banking company was created long ago. It had just one employee who kept the registry in his PC. Let’s limit ourselves to banking operations of deposits and withdrawals from the account (through the employee in the bank).
Initially, a system with two layers was used. The application layer entered the data and the data layer stored them in the file. In this case, the application layer is the consumer, which needs to enter two operations (deposits and withdrawals). If we encapsulated the logic, class BankOperations could contain two methods: Deposit (float amount, int userID) and Withdraw (float amount, int userID). The application does not need to (nor it should) deal with the way how to work with the data in the file - it just needs to perform these operations and, in case of an error, the methods are required to return an error message („Not possible to withdraw 200€ - the amount is not available on the account"). If the bank hired new employees, it would be necessary to switch to some kind of transaction data storage systems (database systems). The program would not need to be changed only the method bodies would have to be replaced from work with the database. In the future, if the bank based new branches and the system would need to operate in multiple physical locations – the methods could change from databases to communication with central server to manage the data etc. In this way, the communication layer would keep changing, but not the application layer - which can save you money if the system is robust, or critical (minimizing errors in expanding the system).
At this point, we come to the visibility of members. Should the class encapsulate some more complex logic, you might have to work with specific "providers" – e.g. communication with the database / soap communication, etc. These helpers are primarily dedicated to the logic of encapsulation (e.g. if we know that the database can only be used through the database layer, we must ensure that this is the case throughout the application - otherwise our efforts to encapsulate disappear in hybrid architecture).
So how do we hide the individual members? It is determined by the keyword private / protected / public. Public is the "highest" visibility. Private is visible only for the given class and protected only for the derived classes (not for an instance). We have already mentioned an example of a public member:
public class BasicClass { private string textC; protected string TextB; public string TextA; } public class DerivedClass : BasicClass { public void DoSomething() { TextC = "something"; TextB = "something"; textA = "something"; ERROR } } // ... private void button1_Click(object sender, EventArgs e) { DerivedClass class = new DerivedClass(); class.TextC = "Something"; class.TextB = "Something"; ERROR class.textA = "Something"; ERROR }
As we can see, private is entirely visible only within one class, we can use protected when inheriting between classes, but it's hidden in the instance of the class (for the consumer). Public is always visible. Of course, these rules also apply to functions / methods / constants ...
Note: There is one more type of visibility, and it is "internal". It acts as public, but ONLY in the context of a particular project (* .VSPROJ). Thus, it is visible in the assembly, where it is defined. We can interconnect assemblies between each other (e.g. via DLL or SOAP). Internal members, however, will not be visible to the integration of structures outside the project where they were defined. This is how you can "encapsulate" logic within the DLL. Let’s imagine DLL that serves as the communication module for applications within the company. This DLL encapsulates a wide range of services of the central server. To avoid having to program communication anew for each corporate system, it is encapsulated into a single DLL. This DLL will be filled with communicating with the server (e.g. SOAP), but auxiliary classes / methods MUST NOT be published from this DLL (again the same scenario - if we encapsulate something, we must propose appropriate communication interface - if the consumer tries to communicate with the encapsulated device, it means that something is wrong with the interface).
Polymorphism
This is probably the most important and most striking part of the OOP! Polymorphism is closely related to inheritance - first we have to imagine how the inherited object looks in reality. Inheritance and derivation of classes serves the programmer to better design repeating parts. It is therefore only design aid, which we do not need to use and we can achieve a similar effect (except for polymorphism). Let’s look at a simple example of each of these derived classes:
public class BasicClass { public string TextA; } public class DerivedClass : BasicClass { public string TextB; }
As we know, the derived class is actually the intersection of both definitions, and therefore the new class, which looks as follows, is its complete equivalent:
public class IntersectionClass { public string TextA; public string TextB; }
If so, then why inherit classes at all? That's a fair question that we should ask whenever we design any hierarchy of classes! Indeed, many programmers use inheritance to facilitate the work (repetitive code), which always leads to a non-transparent code, none of the team wants to interfere in, because with each change something can unexpectedly go wrong!
I am clearly in favour of the use of OOP in the event that the hierarchy has polymorphic importance. In other cases, it is more transparent to keep duplicate code! So, what is polymorphism? According to the translation it is variety - which is not so accurate. Polymorphism is related to one keyword: override - it's a little more accurate, as this may mean 'substitute' as well.
Override: virtual & base
Now we must look at the aforementioned example of inheritance in a more abstract way. "Intersection" of two inheriting classes is the final class - we can imagine that we will write each class on a transparent film and we lay these films on top of each other. As a result, we get the intersection of all inherited classes. This is the work of inheritance. When using class, it will obviously depend on the visibility of members, which we have already discussed in the previous chapter.
But so far, we only know how class interface is "inherited". Class is of course inherited along with the interface. In this sense, OOP is in my opinion better represented in Delphi (Pascal) - where the class definition is not related to logic (body of methods).
So we know that inheritance can create the intersection of two classes - not only variables, but also methods:
public class BasicClass { public void TextA() { MessageBox.Show("A"); } } public class DerivedClass : BasicClass { public void TextB() { MessageBox.Show("B"); } } // ... private void button1_Click(object sender, EventArgs e) { DerivedClass class = new DerivedClass(); class.TextA(); class.TextB(); }
But what happens when we need to "overlay" the body of method? First, we must consider whether we allow someone to rewrite "our" body. If we decide that it might help the consumer, we need to mark the method as virtual:
public class BasicClass { public virtual void Text() { MessageBox.Show("A"); } }
The derived class that wants to replace the body of the method must rewrite it. Keyword override is used for this situation:
public class DerivedClass : BasicClass { public override void Text() { MessageBox.Show("B"); } }
The result will be "B" - now create an instance of the derived class. The very fact that we call the method Text () defined in BasicClass means nothing, because its body was replaced from the derived class! Sometimes it is necessary to ensure the launch of the original code. It always calls through keyword base. Base essentially works as if we turned the current "film" and we communicated again with the IMMEDIATE descendant of the class:
public override void Text() { MessageBox.Show("B"); base.Text(); }
We will understand why the "immediate" with "triple" inheritance from this example:
public class BasicClass { public virtual void Text() { MessageBox.Show("A"); } } public class DerivedClass1 : BasicClass { public override void Text() { MessageBox.Show("B"); } } public class DerivedClass2 : DerivedClass1 { public override void Text() { MessageBox.Show("C"); base.Text(); } }
The result will then display "C" and "B" in the event that we create an instance of the class DerivedClass2. Because it calls its content from a derived class, which in turn completely overlaps its ancestor (no longer calls it).
We showed you how the constructor works. I would like to stop at this point – the constructor is automatically virtual (we can rewrite its contents). There is a quite significant limitation in C #, in terms of the body of the ancestor. It can be ignored, or the call must take place AT THE END. In virtual methods, as can be seen, we can call the code from the ancestor in any place - but in case of the constructors, it is not possible, since the call is already in the definition:
public class BasicClass { private string displayedText; public BasicClass(string text) { displayedText = text + " -> A"; } public void Text() { MessageBox.Show(displayedText); } } public class DerivedClass : BasicClass { public DerivedClass(string text) : base(text + " -> B") { } } // ... private void button1_Click(object sender, EventArgs e) { BasicClass BasicClass = new BasicClass("text"); BasicClass.Text(); // result Text -> A DerivedClass DerivedClass = new DerivedClass("text"); DerivedClass.Text(); // result Text -> B -> A }