[:tr]Evet arkadaşlar, uzunca bir süredir, modern C++ ile ilgili yazılar paylaşıyordum sizler ile ve yazılım mühendisliğine ilişkin konulara biraz uzak kaldık. Bu yazı ile birlikte, biraz daha bu kavramlara eğilmeye başlayacağım. Daha önce bazı kavramlara ilişkin yazılarım olmuştu, inşallah önümüzdeki dönemde bunların sıklığını arttırmayı planlıyorum. Gelelim bu yazımın (daha doğrusu artık yazı dizisi) konusuna.
Yazımı tek bir kelime ile özetleyecek olursam: SOLID. Pek anlamlı gelmemiş olabilir. S.O.L.I.D. şeklinde yazarsam bazı kelimelerin kısaltmaları olduğunu hemen anlayacaksınız.

Sizleri daha fazla merakta bırakmadan, S.O.L.I.D.’in neyin kısaltması olduğunu sizler ile paylaşayım.
S – Single-responsiblity principle (SRP)
O – Open-closed principle (OCP)
L – Liskov substitution principle (LSP)
I – Interface segregation principle (ISP)
D – Dependency Inversion Principle (DIP)
Bunların her birisi, zamanında Robert C. Martin, kendisi Bob amca olarak da bilinir, tarafından Nesne Yönelimli Yazılım Geliştirme için uyulması gereken asgari prensipler olarak tanımlanmıştır. Daha fazla detay için şu dokümana da göz atabilirsiniz. Nesne yönelimli programlama yaklaşımı, yapısal programlama yaklaşımına göre tekrar kullanılabilirliği/esnekliği arttırıp, karmaşıklığı kotarabilse de, yanlış kullanım, bunun tam tersine de sebep olabilir. Bu prensiplerin temel amacı da aslında bu tarz sıkıntıları gidererek, daha çevik, tekrar kullanılabilir ve idame edilebilir yazılımlar elde etmek.
Bu bağlamda, nesne yönelimli yazılım geliştiren hatta, çoğu yazılımcının bu kavramlara vakıf olması, bana kalırsa oldukça elzem. O sebeple daha fazla vakit kaybetmeden bunların her birisine göz atalım.
Bütün bu prensipleri tek bir yazıda anlatmaktansa, ayrı ayrı yazılarda anlatmaya karar verdim. Hem daha anlaşılır hem de daha kolay okunabilir olacağını düşünüyorum. Bu yazımda Tek Sorumluluk “Single-Responsibility” prensibine göz atacağız.
Diğer SOLID yazılarına aşağıdaki adreslerden ulaşabilirsiniz.
- SOLID 1 – Tek Sorumluluk Prensibi
- SOLID 2 – Açık/Kapalı Prensibi
- SOLID 3 – “Liskov Substitution” Prensibi
- SOLID 4 – Arayüz Ayrıştırma Prensibi
Tek Sorumluluk Prensibi (Single-Responsiblity Principle):

Prensibin isminden de anlaşılacağı gibi, bu kavramın en önemli amacı: ilgili kodun, nesne yönelimli yazılımlar için, sınıfın, sadece tek bir sorumluluğu olmasıdır. Bu biraz da aslında, yazılım tasarımı için önemli olan tutarlılık/kohezyon (“cohesion“) kriteri ile de yakından ilintilidir. Bu kavram, verilen bileşenin metot ve verilerinin birbirleri ve gerçekleştirilecek olan yegane fonksiyonalite ile ne kadar ilintili olduğunu ifade eder. Yüksek düzeyde kohezyon, iyi bir yazılım tasarımı elde etmede büyük katkı sağlar.
Bob amca, bu prensibe dair ayrıca “sınıfı değiştirmemiz için sadece bir sebebimiz olmalı” gibi oldukça ünlü bir tanım da yapmıştır (bunun detayları için verdiğim şu adrese bir göz atabilirsiniz). Bir diğer ifade ile sınıf içerisindeki elemanların değişimi aynı sebepten olmalı ve değişiklik için farklı sebeplere ihtiyaç duyulan şeyler ayrılmalıdır. Peki sebepten kasıt nedir? Burada kısaca bir örnek üzerinden bunu anlatmaya çalışayım.
Örneğin elimizde, uygulamamızın dış dünya ile haberleşmesinden sorumlu bir sınıf olduğunu düşünelim ve çok temel olarak aşağıdaki kabiliyetleri içerdiğini farz edelim:
1 2 3 4 5 6 7 8 9 | class ExternalCommunication { public: // Soket mesaji gonder bool sendTCPMessage(const char* rawData); // En son alinan mesajlari kaydet void saveMessageHistoryAsXML(); }; |
Yukarıda verdiğim sınıfa baktığımızda, temel olarak iki fonksiyon görülmekte: TCP/IP soketi üzerinden haberleşme ve haberleşme geçmişinin kaydedilmesi. Mesaj gönderilmesi ve mesaj geçmişin kaydedilmesinin her birini ayrı bir kabiliyet olarak düşünebiliriz. Bu bağlamda olabilecek değişiklikleri öngördüğümüzde, burada kayıt için XML formatı kullanılırken, JSON’ı destekleme durumu olabilir. Aynı zamanda TCP yerine seri kanal ya da UDP kullanma durumu olabilir. Gördüğünüz üzere, bu sınıf değişmesi için birden fazla sebep ortaya çıktı! SRP’ye göre bunlardan sadece birinin değişikliğe sebebiyet vermesini bekliyoruz. Şimdi bu kod parçasını SRP’ye uygun hale getirmeye çalışalım.
Öncelikli olarak mesaj geçmişini yazan işlevi ayrı bir sınıfa alalım. Bunu da yaparken birden fazla kayıt formatı olabileceğini düşünelim. Bu durumu kotarmak için aşağıdaki gibi sınıfları oluşturabiliriz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class IMessageHistoryRecorder { public: virtual void saveMessageHistory(const std::vector<MessageData>& hisory) = 0; }; class XMLMessageHistoryRecorder : public IMessageHistoryRecorder { public: virtual void saveMessageHistory(const std::vector<MessageData>& hisory) { std::cout <<"Message history is recorded as XML file!" << '\n'; } }; class JSONMessageHistoryRecorder : public IMessageHistoryRecorder { public: virtual void saveMessageHistory(const std::vector<MessageData>& hisory) { std::cout <<"Message history is recorded as JSON file!" << '\n'; } }; |
Artık yeni bir mesaj kayıt formatı kullanmamız gerektiğinde, ExternalCommunication sınıfını değiştirmemize gerek kalmayacak. Benzer şekilde haberleşme sınıfı için de aşağıdaki sınıfları tanımlayıp kullanabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | class ICommunicationItem { public: virtual bool initialize() = 0; virtual bool sendMessage(const char* rawData) = 0; }; class TCPSocketCommunication : public ICommunicationItem { public: virtual bool initialize() { // Soket haberleşmesine ilişkin ilklendirmeler } virtual bool sendMessage(const char* rawData) override { // Soket uzerinden mesaj gonderme kodu } // Sokete iliskin ozellesmis metotlar void setSocketParameters(const string ipAddress, int portNo) { // Soket parametreleri } }; class SerialCommunication : public ICommunicationItem { public: virtual bool initialize() { // Seri kanal uzerinden haberleşmeye ilişkin ilklendirmeler } virtual bool sendMessage(const char* rawData) override { // Seri kanal uzerinden mesaj gonderme } // Seri kanala iliskin ozellesmis metotlar void setSerialPortParameters(eBaudRate baudRate, eDataBit dataBits, eParity parity,) { // Seri kanal parametreleri } }; |
Nihai olarak sınıfımız aşağıdaki gibi olacak ve SRP’i da sağlamış ve oldukça genişleyebilir bir sınıf elde etmiş olduk 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | class ExternalCommunication { public: // mesaji gonder bool sendMessage(const char* rawData) { ... if(nullptr != mCommunicationMedium) mCommunicationMedium->sendMessage(rawData); ... } // En son alinan mesajlari kaydet void saveMessageHistory() { ... if(nullptr != mMessageRecorder) { mMessageRecorder->saveMessageHistory(mRecordedMessageData); mRecordedMessageData.clear(); } ... } // Mesajlari kaydetmek için kullanacağımız sınıf atayalım void setMessageRecoder(IMessageHistoryRecorder* recorder) { mMessageRecorder = recorder; } // Haberleşme alt yapısına ilişkin sınıfları gecirelim void setCommunicationItem(ICommunicationItem* commItem) { mCommunicationMedium = commItem; } protected: IMessageHistoryRecorder* mMessageRecorder{nullptr}; ICommunicationItem* mCommunicationMedium{nullptr}; std::vector<MessageData> mRecordedMessageData; }; |
Yukarıdaki örnek yanında, Bob amcanın kullandığı örneği de sizler ile paylaşmak istiyorum. Aşağıda, örneğe ilişkin sınıf diyagramını görebilirsiniz.

Burada görüldüğü üzere Rectangle sınıfımızın en az iki adet sorumluluğu var. Bunlar: GraphicalApplication tarafından, grafiksel kullanıcı arayüzü (GUI) aracılığı ile bir dikdörtgen çizdirilmesi (draw() API’si) ve ComputationalGeometryApp tarafından dikdörtgenin alanının hesaplanılarak kullanılması (area() API’si). İlk bakışta herhangi bir problem görülmeyebilir, hadi biraz daha yakından bakalım.
Buradaki göreceğimiz ilk sıkıntı, sadece dikdörtgen alan hesaplamasına ihtiyaç duyan ComputationalGeometryApp sınıfının, dolaylı yoldan GUI’ye de bağımlılık içermesidir. Benzer şekilde GUI ile ilgili bir değişiklikten ötürü ComputationalGeometryApp uygulamasına da bu değişiklik yansıyabilir. Bu da aslında değişim için sadece bir sebep olmalı kuralını ihlal ediyor.
Peki bu sıkıntıları nasıl giderebiliriz? Hemen bunu gideren tasarıma göz atalım:

Görüleceği üzere bağımlılıkların yukarıda gösterildiği şekilde ayarlanması ile iki uygulama arasındaki bağımlılık da ortadan kalkmış oldu ve tasarımımız da SRP ile uyumlu hale geldi.
Yukarıdaki örneklerden sonra akla şu tarz bir soru gelebilir? Peki gerçekten ilgili sınıfımızın birden fazla sorumluluğu olup olmadığını nasıl anlayabiliriz? Bu noktada imdadımıza, yazımın başlarında bahsettiğim kohezyon kavramı yetişiyor. Buna örnek teşkil etmesi açısından SRP is a Hoax sayfasında verilen Java örneğine göz atacağız:
1 2 3 4 5 | class AwsOcket { boolean exists() { /* ... */ } void read(final OutputStream output) { /* ... */ } void write(final InputStream input) { /* ... */ } } |
Şimdi bu sınıf özelinde SRP’yi körü körüne uygulamaya kalktığımızda, muhtemelen elimizde ExistanceChecker, ContentReader ve ContentWriter gibi sınıflar olacaktır. Basit bir şekilde Aws sınıf üzerinde bir veri okuyup, görüntülemek için aşağıdaki gibi bir kodu kullanmamız gerekecektir:
1 2 3 | if (new ExistenceChecker(ocket.aws()).exists()) { new ContentReader(ocket.aws()).read(System.out); } |
Görüleceği üzere, çoğu zaman bu sınıflar birlikte kullanılacaklar. Ayrıca bu sınıflar, birbirleri ile yüksek kenetlenme (coupling) ve düşük seviyede kohezyon sergiliyorlar. O sebeple, bu sınıfı aslında bu şekilde ayırmak doğru değil.
Normal şartlarda da bu prensibi kullanırken, sınıflarınızı özellikle kohezyon seviyelerini iyi değerlendirmeniz gerekiyor.
Bu soru ile birlikte ilk prensibimizi tamamlamış oluyoruz. Haftaya ikinci prensipte görüşmek üzere, kendinize iyi bakın.
Kaynaklar:
- https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html
- Designing Object Oriented Applications using UML, 2d. ed., Robert C. Martin, Prentice Hall, 1999.
- http://www.yegor256.com/2017/12/19/srp-is-hoax.html
- https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf
- https://www.wikiwand.com/en/Object-oriented_programming
- https://en.wikipedia.org/wiki/Structured_programming
[:en]Yes, friends, I have been sharing posts about C++ and have not written many posts about software engineering stuff. This will change from this post and I will try to increase the frequency of such topics. Let us, talk about the topic of this post (in fact post series now on).
So if I would summarize my post with one word, it would SOLID. It may not sound familiar, but if I say S.O.L.I.D. then you may immediately undestand that it is an acronym.

Let me share with you what S.O.L.I.D. stands for.
S – Single-responsiblity principle (SRP)
O – Open-closed principle (OCP)
L – Liskov substitution principle (LSP)
I – Interface segregation principle (ISP)
D – Dependency Inversion Principle (DIP)
Each of these has been defined as the minimum principles to be followed for Object Oriented Software Development by Robert C. Martin who is also known as Uncle Bob. You can also refer to this document for more details. Although the object-oriented programming approach can increase the reusability / flexibility compared to the structured programming approach and handle the complexity, misuse can cause the contrary. The main purpose of these principles is to eliminate such problems and to obtain more agile, reusable and maintainable software.
In this context, it is essential to me that most developers, especially those who develop object oriented software, should be familiar with these principles. So let’s take a look at each of these without wasting any more time.
Instead of explaining all these principles in a single article, I decided to explain them in separate articles. I think, it will be more understandable and easier to read. In this article, we will look at the principle of Single Responsibility.
For other SOLID principles, you can check out following links:
- SOLID 1 – Single-responsibility Principle
- SOLID 2 – Open-closed Principle
- SOLID 3 – “Liskov Substitution” Principle
Single-Responsiblity Principle:

As its name implies, the most important purpose of this principle is: for object oriented software, each class should only have one responsibility.
This is, in fact, also closely related to the cohesion criterion, which is important for software design. This concept expresses how much the method and data of the given component relate to each other to perform the designated class functionality. The high level of cohesion makes a great contribution to achieving good software design.
Uncle Bob has also made a very famous definition of this principle: “We only have one reason to change the class” (you can take a look at this address for details). In other words, the change of the elements in the class must be for the same reason. If different reasons are required for different changes, then those should be separated. So what do we mean by reason? Let me try to give a brief example here.
For instance, let us assume that we have a class which is responsible for external communication and have following capabilities:
1 2 3 4 5 6 7 8 9 | class ExternalCommunication { public: // Send socket message bool sendTCPMessage(const char* rawData); // Save last received messages void saveMessageHistoryAsXML(); }; |
When we look at the class that I gave above, there are basically two functions: communication via TCP / IP socket and recording of communication history. We can think of sending each message and recording the message history as a separate capability. The possible changes that may happen, there may be a need for supporting JSON instead of XML format for recording. It may also be the case of using serial channels or UDP instead of TCP. As you can see, there are multiple reasons for this class for change! According to SRP, we expect only one of them to cause change. Let’s try to make this piece of code suitable for SRP.
First of all, let us move history recording capability to a separate class. As we design this class, let us also consider multiple formats. We can develop following classes for this purpose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class IMessageHistoryRecorder { public: virtual void saveMessageHistory(const std::vector<MessageData>& hisory) = 0; }; class XMLMessageHistoryRecorder : public IMessageHistoryRecorder { public: virtual void saveMessageHistory(const std::vector<MessageData>& hisory) { std::cout <<"Message history is recorded as XML file!" << '\n'; } }; class JSONMessageHistoryRecorder : public IMessageHistoryRecorder { public: virtual void saveMessageHistory(const std::vector<MessageData>& hisory) { std::cout <<"Message history is recorded as JSON file!" << '\n'; } }; |
When we need to use a new message recording format, we no longer need to change the ExternalCommunication class. Similarly, we can define and use the following classes for the communication purpose.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | class ICommunicationItem { public: virtual bool initialize() = 0; virtual bool sendMessage(const char* rawData) = 0; }; class TCPSocketCommunication : public ICommunicationItem { public: virtual bool initialize() { // Initializations associated with socket communication } virtual bool sendMessage(const char* rawData) override { // Code for sending message via socket } // Socket communication specific methods void setSocketParameters(const string ipAddress, int portNo) { // Soket parameters } }; class SerialCommunication : public ICommunicationItem { public: virtual bool initialize() { // Initializations associated with serial communication } virtual bool sendMessage(const char* rawData) override { // Code for sending message via serial channel } // Serial channel communication specific methods void setSerialPortParameters(eBaudRate baudRate, eDataBit dataBits, eParity parity,) { // Serial channel parametreleri } }; |
Our final class will be as follow which also supports SRP. We have achieved a class that is quite expandable 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | class ExternalCommunication { public: // Send socket message bool sendMessage(const char* rawData) { ... if(nullptr != mCommunicationMedium) mCommunicationMedium->sendMessage(rawData); ... } // Save last received messages void saveMessageHistory() { ... if(nullptr != mMessageRecorder) { mMessageRecorder->saveMessageHistory(mRecordedMessageData); mRecordedMessageData.clear(); } ... } // Assign instance that will be used for message recorder void setMessageRecoder(IMessageHistoryRecorder* recorder) { mMessageRecorder = recorder; } // Assign instance that will be used for communication void setCommunicationItem(ICommunicationItem* commItem) { mCommunicationMedium = commItem; } protected: IMessageHistoryRecorder* mMessageRecorder{nullptr}; ICommunicationItem* mCommunicationMedium{nullptr}; std::vector<MessageData> mRecordedMessageData; }; |
In addition to this code example, I also want to share the example that is given by Uncle Bob. You can find class diagram related with that example below.

The first problem we will see here is that the ComputationalGeometryApp class, which only needs a rectangular area calculation, also includes indirect dependency on the GUI. Similarly, due to a change in GUI, this change may cause another change in ComputationalGeometryApp. This, in fact, violates the rule that there must be only one reason for change.
So how can we handle these issues? Let us look the resultant class diagram that resolve these issues:

As it can be seen, by adjusting the dependencies as shown above, the dependency between the two classes has also disappeared and our design has become compatible with SRP.
After the above examples, one might ask: So how can we really tell if our class has more than one responsibility? At this point, the concept of cohesion that I mentioned at the beginning of my writing comes to our rescue. As an example, we will take a look at the Java example given at “SRP is a Hoax” page:
1 2 3 4 5 | class AwsOcket { boolean exists() { /* ... */ } void read(final OutputStream output) { /* ... */ } void write(final InputStream input) { /* ... */ } } |
Now, when we try to blindly implement SRP in this class, we probably have classes like ExistanceChecker, ContentReader and ContentWriter. To read and display data on the Aws class in a simple way, we will need to use code like the following:
1 2 3 | if (new ExistenceChecker(ocket.aws()).exists()) { new ContentReader(ocket.aws()).read(System.out); } |
As you can see, these classes will often be used together. In addition, these classes exhibit high coupling and low cohesion. Therefore, it is not right and feasible to separate these classes in this way.
When using this principle under normal circumstances, you need to assess the cohesion levels of your classes carefully.
With this question, we have completed our first principle. See you next week on principle two, take care of yourself.
References:
- https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html
- Designing Object Oriented Applications using UML, 2d. ed., Robert C. Martin, Prentice Hall, 1999.
- http://www.yegor256.com/2017/12/19/srp-is-hoax.html
- https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf
- https://www.wikiwand.com/en/Object-oriented_programming
- https://en.wikipedia.org/wiki/Structured_programming
[:]
