9 Aralık 2019 Pazartesi

SOLID - Single Responsibility Principle - Sınıfın Tek Bir Sorumluluk Olmalıdır

Giriş
SOLID kurallarının ilki olan Single Responsibility Principle (SRP), aynı zamanda bence anlaması en kolay kavram ancak kodlarken o kadar kolay olmuyor :)

Single Responsibility Principle Nedir - Tek Sorumluluk Kuralı
Single Responsibility Principle kelimesi terminolojiye Robert Cecil Martin - yani Uncle Bob - tarafından sokuldu. Söylemi şöyle.
Gather together the things that change for the same reasons. Separate those things that change for different reasons.
Robert C. Martin'ın bloğunda bir alıntı şöyle.
The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change.
... This principle is about people.
... When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function.
... This is the reason we do not put SQL in JSPs. This is the reason we do not generate HTML in the modules that compute results. This is the reason that business rules should not know the database schema. This is the reason we separate concerns.
Wikipedia açıklaması şöyle.
The single responsibility principle is a computer programming principle that states that every module, class, or function should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class
Bir sınıf tek bir sorumlulukla ilgilenmeli deniyor. Single Responsibility Principle, Separation of Concerns, Cohesion (odaklılık) bence aynı kavramlar.

Veri tabanı tasarımında, Full Normalization Single Responsibility gibi düşünülebilir.

SRP'nin Yanlış Anlaşılması
SRP'de tek sorumluluk denilince sanki tek bir adım olmalı gibi algılanıyor. Ancak bir iş birden fazla adım gerektirebilir. Önemli olan işin birbiriyle mantıksal olarak ilişkili adımlardan oluşması
Açıklaması şöyle.
SRP is not about doing a single atomic action. It's about having a single responsibility, even if that responsibility requires multiple actions... and ultimately it's about maintenance and testability:

-it promotes encapsulation (avoiding God Objects),
-it promotes separation of concerns (avoiding rippling changes through the whole codebase),
-it helps testability by narrowing the scope of responsibilities.
Bir başka açıklama şöyle.
An object having a single responsibility doesn't mean only one thing can happen in here. Responsibilities can nest. But those nesting responsibilities should make sense, they shouldn't surprise you when you find them in here and you should miss them if they were gone.
Benzer bir açıklama şöyle.
A class's single responsibility might be "save a file". To do that, it then may break that responsibility down into a method that checks whether a file exists, a method that writes metadata etc. Each those methods then has a single responsibility, which is part of the class's overall responsibility.
Single Responsibility İçin Örnekler

Örnek
Loader sınıfı Reader  aracılığıyla bir nesneyi okur ve döndürür.
Transformer sınıfı Loader sınıfın okuduğu nesneyi alır ve başka bir nesneye çevirir.
Provider sınıfı Transformer sınıfının çevirdiği nesneyi işleyecek sınıfa sağlar.
Provider sınıfı belki Builder tarafından oluşturulur.

Örnek
Bu sınıf sadece User ile ilgili işlemler sunuyor.
class UserService {
  public User Get(int id) { /* ... */ }
  public User[] List() { /* ... */ }

  public bool Create(User u) { /* ... */ }
  public bool Exists(int id) { /* ... */ }
  public bool Update(User u) { /* ... */ }
}
Single Responsibility Kuralını İhlal Eden Örnekler
Örnek
Bu konudaki en sevdiğim örnek yazıcıya gönderilmek istenen veriyi formatlayan ve yazdıran tek bir sınıfın olması. Hem formatlama, hem de yazdırma işleminin çok farklı sorumluluklar olduğu o kadar aşikar ki, tek bir sınıfın bunları yapmasının SRP'yi neden bozduğu rahatlıkla anlaşılabiliyor.
Örnek
Elimizde şöyle bir kod olsun
var user = ...;
userRepository.Add(user);

userRepository.SaveChanges();
SaveChanges metodu şöyle olsun.
void SaveChanges()
{
  dataContext.SaveChanges();
  logger.Log("User DB updated: " + someImportantInfo);
  foreach (var newUser in dataContext.GetAddedUsers())
  {
    eventService.RaiseEvent(new UserCreatedEvent(newUser ))
  }
}
Açıklaması şöyle.
A better design would be to have a separate process retrieve 'new users' from the repository and send the emails. Keeping track of which users have been sent an email, failures, resends etc etc.

This way you can handle errors, crashes and the like as well as avoiding your repository grabbing every requirement which has the idea that events happen "when something is committed to the database"

The repository doesn't know that a user you add is a new user. It's responsibility is simply storing the user.
Örnek - Clean Code Kitabından
Robert C. Martin'in Clean Code kitabından bir başka örnek şöyle. Kitaba göre eğer yeni bir employee tipi eklenirse bu kod değişir. Bu yüzden SRP kuralı ihlal edilir. Bu tür kodlarda en kolay çözüm arayüzler tanımlayarak, calculate işlemlerini başka sınıflara taşımak. Arayüzlerde Liskov kuralına dikkat etmek gerekir.
public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED:
      return calculateCommissionedPay(e);
    case HOURLY:
      return calculateHourlyPay(e);
    case SALARIED:
      return calculateSalariedPay(e);
    default:
      throw new InvalidEmployeeType(e.type);
    }
}
Single Responsibility Kuralını İhlal Eden Kod Nasıl Düzeltilir
1. Delegation kullanılabilir
2. Arayüzler tanımlanarak kod arayüzü gerçekleştiren yere taşınır.

Örnek
Elimizde şöyle bir kod olsun. Bu sınıf marketing, analytics ve development ekipleri tarafından kullanılıyor. Yani çok fazla iş yapıyor
class AdsAccount {
  public startCampaign() { ... }
  public calculateCampaignStats() { ... }
  public save() { ... }
}
Şu hale getirebiliriz
class AdsAccount {
  constructor() {
    this.statsCalculator = new StatsCalculator()
    this.campaignLauncher = new CampainLauncher()
    this.campaignSaver = new CampaignSaver()
  }

  public startCampaign() {
    this.campaignLauncher.startCampaign(...)
  }

  public calculateCampaignStats() {
    this.statsCalculator.calculateCampaignStats(...)
  }

  public save() {
    this.campaignSaver.save(...)
  }
}

class StatsCalculator {
  public calculateCampaignStats() { ... }
  private getCampaignImpressions() { ... }  
}

class CampaignLauncher {
  public startCampaign() { ... }
  private getCampaignImpressions() { ... }  
}

class CampaignSaver {
  public save() { ... }
}
Örnek
Elimizde şöyle bir kod olsun. Burada transfer() metodundaki if/else cümleleri SRP kuralını ihlal ediyor.
class BankCard {
  topup() { ... }
  withdraw() { ... }
}
class SavingsCard {
  withdraw() { ... }
}

class Wallet {
  transfer(amount: number, sender: BankCard | SavingsCard, receiver: BankCard) {
    if (sender instanceof BankCard) {
      // ...
    }
    if (sender instanceof SavingsCard) {
      // ...
    }
  }
}

const wallet = new Wallet()
const senderSavingsCard = new SavingsCard()
const receiverBankCard = new BankCard()

wallet.transfer(1000, senderSavingsCard, receiverBankCard)
Şöyle yaparız
interface IWithdrawable {
  withdraw(): void
}

interface IRechargeable {
  topup(): void
}

class BankCard implements IRechargable, IWithdrawable {
  topup() { ... }
  withdraw() { ... }
}

class SavingsCard implements IWithdrawable {
  withdraw() { ... }
}

class Wallet {
  transfer(amount: number, sender: IWithdrawable, receiver: IRechargable) {
    // ...
  }
}

const wallet = new Wallet()
const senderSavingsCard = new SavingsCard()
const receiverBankCard = new BankCard()

wallet.transfer(1000, senderSavingsCard, receiverBankCard)
Single Responsibility Yeni İstekte Kendini Belli Eder
Açıklaması şöyle.
The idea is to minimize the footprint of future potential changes, restricting code modifications to one area of code per area of change.
Değişiklik isteği şöyle olabilir.
New business requirement: Users located in California get a special discount.
Example of "good" change: I need to modify code in a class that computes discounts.
Example of bad changes: I need to modify code in the User class, and that change will have a cascading effect on other classes that use the User class, including classes that have nothing to do with discounts, e.g. enrollment, enumeration, and management.
Değişiklik isteği şöyle olabilir.
New nonfunctional requirement: We'll start using Oracle instead of SQL Server 

Example of good change: Just need to modify a single class in the data access layer that determines how to persist the data in the DTOs. 
Bad change: I need to modify all of my business layer classes because they contain SQL Server-specific logic.
Single Responsibility ve Annotation
Bazı durumlarda dilin verdiği destek sayesinde, normalde Single Responsibility kuralını ihlal eden kodlar kabul edilebilir hale geliyor. Örneğin serialization, XML, JSON gibi formatlara yazıp okuma işleri artık hep annotation ile yapılıyor. Bu bilginin sınıfın içinde olması, yukarıdaki örnekteki gibi SRP kuralını ihlal eder denmiyor çünkü annotation kod gibi değil meta data gibi düşünülüyor.

Single Responsibility ve Multiple Inheritance
Composition Over Inheritance yazısına taşıdım.

Single Responsibility ve Kod Yapısı
Single Responsibility kuralı sınıf sayısının artmasına sebep oluyor. Bu durumda paketlerin nasıl düzenlendiği önem kazanıyor. Paketleri işlev olarak bölmek iyi bir fikir olabilir. Mesela Domain nesneleri paketi, Serialization paketi gibi.

Single Responsibility ve DRY İlişkisi
Bu kural ile DRY (Don't Repeat Yourself) biraz alakalı. DRY kuralı hiç bir zaman aynı iki şeyin tekrar etmemesi gerektiğini söylüyor. Açıklaması şöyle.
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system"
Örneğin şu iki sınıf LoginPage'i ortak kullandığı için DRY kuralını uygular.
public class HomePage{
  public LoginPage loginPage;
}

public class EditInfoPage{
  public LoginPage loginPage;
}
Bu kural sadece nesneye yönelik programlama için değil, her türlü programlama için geçerli. DRY kuralının tersi yani çoklama yapan şeyler ise WET olarak anılırlar.

Single Responsibility daha üst seviye bir kural. DRY ise kod seviyesinde kalıyor. İki kuralın birbirini tamamlamasına örnek şu olabilir.

İki sınıf tek bir işi yapıyorlarsa Single Responsibility kuralına uyarlar. Ancak her ikisi de aynı işi yapıyorlarsa DRY kuralı ihlal ederler. DRY kuralı gereğince aynı kodu ortak bir yere toplamak gerekir. Böylece tek bir işi yapan iki sınıf ve tekrar etmeyen bir kod ortaya çıkar. DRY kuralında birbirine çok benzeyen ancak farklı işleri yapan kodlara dikkat etmek gerekir. Bu tür kodları birleştirmek istenmeyen sonuçlara sebep olabilir.

Hiç yorum yok:

Yorum Gönder