Object-Oriented Programming වලදී මුලින්ම අප ඉගෙන ගන්නේ එහි ඇති concepts 4ක් ගැනයි.
- Inheritance
- Abstraction
- Polymorphism
- Encapsulation
ඔබ මේවා ගැන නොදන්නේ මෙම article එක කියවීමට පෙර ඒවා ගැන දැන ගැනීම වැදගත් වනු ඇත.
ඒවා අවශ්ය වන්නේ යම් programming language එකකින් code ලියන අවස්ථාවේ දී ය. නමුත් ඊට පෙර system එක design කරන අවස්ථාවේදී භාවිතා වන සංකල්පයන් පවතී. මේ ලිපියෙන් සාකච්යඡා කරන්නේ system එකක් design කිරීමේදී සලකා බලන SOLID මූල ධර්මයන් ගැනයි.
S.O.L.I.D Principles
SOLID යන කෙටි නාමය මගින් සංකල්ප 5ක් නිරූපණය වේ.
- S –> Single Responsibility Principle
- O –> Open-Closed Principle
- L –> Liskov Substitution Principle
- I –> Interface Segregation Principle
- D –> Dependency Inversion Principle
දැන් බලමු මොනවද මේ සංකල්ප 5 කියලා.
S – Single Responsibility Principle
Class එකක් වෙනස් කල/විය යුත්තේ එකම එක හේතුවක් නිසා පමණි. එනම්, එක් class එකකට තිබිය යුත්තේ එක් කාර්ය්යක් පමණි.
මේ මගින් කියවෙන්නේ අපි යම් class එකක් නිර්මාණය කරනවා නම්, එය වෙනස් විය යුත්තේ ඊට අදාල වන features එකතු කිරීමටම පමණක් විය යුතුයි.
class Employee {
public:
Employee() {}
virtual ~Employee() {}
bool Initialize() {
// Initialize DB connection to the users table.
}
}
මේ code එකේ තියෙන ප්රශ්නය පැහැදිලිද? Database එකෙන් data ලබාගෙන object එකක් නිර්මාණය කිරීම Employee class එකේ කාර්ය්යක් නොවිය යුතුයි. එය ORM (Object Relational Model) එකක කාර්ය්යක් විය යුතුයි. එමෙන්ම, මෙහි output එක log file එකකට ලිවීම වෙනත් class එකක කාර්ය්යක් විය යුතුයි.
O – Open-Closed Principle
Class එකක් දිගු (extend) කිරීමට විවෘත(open) විය යුතු අතර වෙනස්(change) කිරීමට සංවෘත(closed) විය යුතුය.
පහත උදාහරණය සලකා බලන්න.
class Employee
{
public:
Employee() {}
virtual ~Employee() {}
double getOTRate()
{
if (this->employeeType == 1)
return 50;
else return 0;
}
}
මේ අනුව employee ගේ වර්ගය 1 නම් පැයකට රු. 50 ක අතිකාල දීමනාවක් ලැබෙන අතර, එසේ නැතිනම් අතිකාල දීමනා දෙනු නොලැබේ.
අපට තවත් වර්ගයක employee කෙනෙක් එකතු කිරීමට අවශ්ය නම්, ඉහත if කොටසට else if එකක් යෙදිය යුතුය. නමුත් එසේ කිරීමට සිදු වූ හේතුව functional එකක් නොවන බවත්, එය employee නම් සංකල්පයේ ම වෙනසක් වන බව පැහැදිලි ද? ඒ අනුව Employee class එක පහත ආකාරයට වෙනස් කල හැක.
class Employee {
public:
Employee() {}
virtual ~Employee() {}
virtual double getOTRate() = 0;
}
//
class PermanentEmployee : public Employee {
public:
PermanentEmployee()
: Employee()
{
}
~PermanentEmployee() {}
double getOTRate() { return 0; }
}
//
class ContractEmployee : public Employee {
public:
ContractEmployee()
: Employee()
{
}
~ContractEmployee() {}
double getOTRate() { return 20; }
}
මේ අනුව Employee class එක super class එකක් බවට පත් වී, විවිධ employee වර්ග අනුව sub-classes නිර්මාණය කල හැක. මෙහිදී වැදගත් කරුණ නම්, අලුත් employee වර්ගයක් එකතු වුවද Employee class එක වෙනස් කලයුතු නොවීමත්, එය extend කර අලුත් class එකක් නිර්මාණය කිරීමට හැකි වීමත් ය. ඒ අනුව ඉහත Employee class එක Open-Closed Principle එකට අනුකූල class එකක් බව කිව හැකියි.
L – Liskov Substitution Principle
මේ principle එක define කරලා තියෙන්නේ මේ ආකාරයටයි.
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
ඉහත වාක්යය ඉතා සංකීර්ණ වුවත් එමගින් පැහැදිලි කරන්නේ ඉතා සරල සංකල්පයකි. එනම්,
සෑම sub-class object එකක්ම එහි parent class object එක වෙනුවට භාවිතා කල හැකි විය යුතුයි.
(every subclass/derived class should be substitutable for their base/parent class.)
මෙය උදාරණයක් මගින් තේරුම් ගැනීමට උත්සහ කරමු.
class File {
public:
File() {}
virtual ~File(){};
virtual string readLine();
virtual void writeLine();
}
class DataFile : File {
DataFile()
: File()
{
}
~DataFile() {}
string readLine() { // Read stuff }
void writeLine() { // Write stuff }
}
class ReadOnlyFile : File {
ReadOnlyFile()
: File()
{
}
~ReadOnlyFile() {}
string readLine() { // Read stuff }
void writeLine() { throw NotWritableException(); }
}
මේ අනුව දැක ගත හැක්කේ ReadOnlyFile
class එකේ writeLine()
function අනවශ්ය බවත් එය එකතු කර ඇත්තේ semantics නිවැරදි කිරීමට බවත්ය (පහත code එක බලන්න).
මේ අනුව, පහත code එක run වන විට exception එකක් throw කරමින් crash වන බව පැහැදිලිය.
int main()
{
File* files[2] files[0] = new DataFile();
files[1] = new ReadOnlyFile();
for (File* f : files) {
cout << f->readLine() << endl;
f->writeLine(); // this line will fail
}
}
Loop එක තුලදී writeLine()
call කරන විට program එක crash වේ. ඒ ReadOnlyFile
සඳහා writeLine()
ලෙස function එකක් අනවශ්ය නිසාය. ඒ අනුව, එම class එක Liskov Substitution Principle අනුගමනය නොකරයි.
එය වෙනස් කිරීමට නම් writeLine()
function එක ReadOnlyFile
class එක තුලට අඩංගු වීම වැලැක්විය යුතුයි.
ඒ අනුව writeLine
function එක වෙනම class එකකට add කල හැක.
class File {
public:
File() {}
virtual ~File(){};
virtual string readLine();
}
class WritableFile : public File {
public:
WritableFile() {}
virtual ~WritableFile(){};
virtual string readLine();
virtual void writeLine();
}
class DataFile : public WritableFile {
DataFile()
: File()
{
}
~DataFile() {}
string readLine()
{
// Read stuff
}
void writeLine()
{
// Write stuff
}
}
class ReadOnlyFile : File {
ReadOnlyFile()
: File()
{
}
~ReadOnlyFile() {}
string readLine()
{
// Read stuff
}
}
දැන් File හි ඇත්තේ file කියවීමට අදාල function එක පමණි. File ලිවීමට ද අවශ්ය නම් අපගේ class එක WritableFile
class එකෙන් extend කල යුතුයි. දැන් File
class හි ඕනෑම sub-class එකක් මගින් File class හි භාවිතයන් ආදේශ කල හැකි අතර මේ මගින් Liskov Substitution Principle එක සපුරාලිය හැක.
I – ISP – Interface Segregation Principle
ඔබගේ abstract class/interface එක භාවිතා කරන කෙනෙකු හට එහි ඇති functions implement කිරීමට බල කිරීමක් නොකල යුතුයි. එමෙන්ම ඔහු භාවිතා නොකරන methods මත functionalities රඳා පැවතීම සිදු නොවිය යුතුයි.
පහත Logger
class එක සලකා බලන්න.
class Logger
{
public:
Logger() {}
virtual ~Logger() {}
virtual int write(const string& str) = 0;
virtual int read(string& str) = 0;
virtual string createNewLog() = 0;
}
මෙම class එක extend කරමින් නිර්මාණය කර ඇති FileLogger
, NetworkLogger
සහ ConsoleLogger
classes සලකා බලන්න.
class FileLogger : public Logger {
public:
int write(const string& str)
{
// write str to File
return noOfLines;
}
int read(string& str)
{
// read to str
return noOfLines;
}
string createNewLog()
{
// create a new file
return fileName;
}
}
class NetworkLogger : public Logger {
public:
int write(const string& str)
{
// write str to File
return noOfLines;
}
int read(string& str)
{
// read to str
return noOfLines;
}
string createNewLog()
{
// not required?
return "";
}
}
class ConsoleLogger : public Logger {
public:
int write(const string& str)
{
// write str to File
return noOfLines;
}
int read(string& str)
{
// not required??
return 0;
}
string createNewLog()
{
// not required?
return "";
}
}
ඒ අනුව FileLogger
class එකට read
, write
සහ createNewLog
functions අවශ්ය වුවත්, NetworkLogger
සඳහා createNewLog
ද, ConsoleLogger සඳහා read සහ createNewLog
යන functions අනවශ්ය බව පැහැදිලි වේ. එනම් අප නිර්මාණය කල Logger class එක ISP පිලිපදින්නේ නැත.
ඒ අනුව read, write and createNewLog
යන features වෙන් (segregate) කල විට, පහත classes 3 ලැබේ.
class ReadableLogger {
public:
ReadableLogger() {}
virtual ~ReadableLogger() {}
virtual int read(string& str) = 0;
}
class WritableLogger {
public:
WritableLogger() {}
virtual ~WritableLogger() {}
virtual int write(const string& str) = 0;
}
class ContinuableLogger {
public:
ContinuableLogger() {}
virtual ~ContinuableLogger() {}
virtual string createNewLog() = 0;
}
මෙම classes අදාල පරිදි inherit කිරීමෙන් අපට අවශ්ය sub-classes නිර්මාණය කර ගත හැක.
D – Dependency Inversion Principle
SOLID principles වන අවසාන සංකල්පය වුවත් මෙය ඉතා වැදගත් වන එකකි.
Entities abstraction මත පමණක් යැපිය යුතු අතර කිසිම විටක concretions මත රඳා පැවතිය නොයුතුයි.
ඒ අනුව, low level modules මත high level modules රඳා නොපැවතිය යුතු වන අතර එය abstraction මත පමණක්ම රඳා පැවතිය යුතුයි.
පහත උදාහරණය සලකා බලන්න.
class User
{
public:
virtual bool authenticate ( MySQLDBConnection* conn ) {}
}
ඉහත class එකේදී User කෙනෙක් authenticate කිරීමට MySQL Database Connection එකක් අත්යාවශ්ය බව සලකා ඇත. නමුත්, පසු කාලීනව වෙනත් database එකක් හෝ OAuth මගින් authenticate කල යුතු නම්, මෙම User class එක වෙනස් කල යුතු වනු ඇත (එය Open-Closed Principle එක කඩ කිරීමකි). එනම් User යනු high level class එකක් වන අතර MySQLDBConnection
යනු low level class එකක් බැවින් User class එක MySQLDBConnection
මත රඳා පැවතීම නුසුදුසු වීමයි.
මෙය නිවැරදි කිරීමට parameter එකක් ලෙස සියලුම connections නිරූපණය කල හැකි super class එකක් යොදා ගක යුතුය.
class User {
public:
virtual bool authenticate(Authenticator* auth)
{
// do stuff
return auth->performAuth();
}
}
class Authenticator {
virtual bool performAuth() = 0;
}
class OAuth : public Authenticator {
virtual bool performAuth()
{
// do stuff
}
}
class MySQLAuth : public Authenticator {
virtual bool performAuth()
{
// do stuff
}
}
int main()
{
User u;
u.authenticate(new OAuth());
}
දැන් ඉහත code එක Dependency Inversion Principle එක අනුගමනය කරයි.
සාරාශය
SOLID Principles යනු පරිගණක පද්ධති නිර්මාණයේදී භාවිතා කල හැකි සංකල්ප කිහිපයකි. අප මුලින්ම කතා කල Object-Oriented Programming දී භාවිතා වන මූලික සංකල්ප 4 යනු මෙය implement කිරීමට යොදා ගන්නා උපක්රම බව දැන් ඔබට පැහැදිලි වනු ඇත. එනම්, එම සංකල්ප 4 යනු පින්සලක් මගින් ඉරි ඇඳිය හැකි ආකාර ලෙස සැලකුව හොත්, SOLID principles යනු ඒවා භාවිතයෙන් අංකාර චිත්රයක් අඳින ආකාරය සඳහා වන උපදෙස් ලෙස සැලකිය හැක.
නමුත්, මෙම SOLID principles වුවද යොදා ගත යුතු සීමාවන් ඔබ තීරණය කල යුතුයි. මක්නිසාද යත්, සරල පද්ධතියකට එය පසු කාලීකන වෙනස් නොවන්නේ නම් මහා පරිමාණ design සංකල්ප යොදා ගෙන ඔබගේ code එක සංකීර්ණ කර ගත යුතු නොවන නිසාය.
References
https://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp
https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design