React SOLID Principles: Kodunuzu Daha Temiz ve Daha Sürdürülebilir Hale Getirin

Bu blog yazısında, sağlam ve ölçeklenebilir React uygulamaları oluşturmak için SOLID prensiplerini nasıl kullanabileceğimizi inceleyeceğim. SOLID prensipleri, uygulamamızı yeniden kullanılabilir, bakımı kolay ve ölçeklenebilir tutmamıza yardımcı olan beş tasarım prensibidir. Bu prensipler, yazılım tasarımında esneklik ve sürdürülebilirlik sağlamaktadır.

SOLID prensipleri şunlardır:

  1. Single Responsibility Principle (SRP) – Tek Sorumluluk Prensibi: Bir sınıfın yalnızca bir nedenle değiştirilmesi gerektiğini belirtir. Bu, bir sınıfın yalnızca bir işi yapması gerektiği anlamına gelir.
  2. Open/Closed Principle (OCP) – Açık/Kapalı Prensibi: Yazılım varlıklarının (sınıflar, modüller, fonksiyonlar vb.) genişletilmeye açık ancak değişikliklere kapalı olması gerektiğini ifade eder. Bu, mevcut kodun değiştirilmeden yeni davranışların eklenmesi gerektiği anlamına gelir.
  3. Liskov Substitution Principle (LSP) – Liskov’un Yerine Geçme Prensibi: Türetilmiş sınıfların, temel sınıflarının yerine geçebilmesi gerektiğini belirtir. Bu, alt sınıfların, üst sınıflarının davranışlarını değiştirmeden genişletebileceği anlamına gelir.
  4. Interface Segregation Principle (ISP) – Arayüz Ayırma Prensibi: Büyük arayüzlerin, kullanılmayan yöntemleri uygulamak zorunda kalmadan kullanılabilmesi için daha küçük ve daha spesifik arayüzlere bölünmesi gerektiğini belirtir.
  5. Dependency Inversion Principle (DIP) – Bağımlılık Tersine Çevirme Prensibi: Üst seviye modüllerin alt seviye modüllere doğrudan bağlı olmaması ve her ikisinin de soyutlamalara bağlı olması gerektiğini ifade eder. Bu, bağımlılıkların somut sınıflar yerine arayüzler veya soyut sınıflar üzerinden yapılması gerektiği anlamına gelir.

Single Responsibility Principle (SRP)

Bir bileşenin tek ve net bir amaca sahip olması gerektiğini belirtir. Bu prensip, karmaşık bileşenleri daha küçük bileşenlere ayırmaya teşvik eder.

❌ Yapma: Birden fazla sorumluluğu olan bileşen

const ProductComponent = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <div key={product?.id} className="product">
                    <h3>{product?.name}</h3>
                    <p>${product?.price}</p>
                    <p>${product?.description}</p>
                </div>
            ))}
        </div>
    );
};

Yap: Sorumlulukları daha küçük bileşenlere ayırma

import ProductComponent from './Product';

const Products = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <ProductComponent key={product?.id} product={product} />
            ))}
        </div>
    );
};

// ProductComponent.ts
// Ürün detaylarını render eden ayrı bir bileşen.
const ProductComponent = ({ product }) => {
    return (
        <div className="product">
            <h3>{product?.name}</h3>
            <p>${product?.price}</p>
            <p>${product?.description}</p>
        </div>
    );
};

Bu ayrım, her bileşenin tek bir sorumluluğa sahip olmasını sağlayarak bunların anlaşılmasını, test edilmesini ve sürdürülmesini kolaylaştırır.

Open/Closed Principle (OCP)

Yazılım bileşenlerinin (sınıflar, modüller, fonksiyonlar vb.) yeni davranışlar eklemek için genişletilebilir olmasını, ancak mevcut kodun değiştirilmesini gerektirmeyecek şekilde kapalı olmasını savunur. Bileşeni değiştirmeden işlevselliğini genişletmek için propsları kullanabiliriz.

❌ Yapma: Yeni özellik eklemek için mevcut bileşeni değiştiriyoruz

function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

// Yeni özellik eklemek için mevcut bileşeni değiştiriyoruz.
function Button({ label, onClick, color }) {
  return (
    <button onClick={onClick} style={{ backgroundColor: color }}>
      {label}
    </button>
  );
}

Yap: Yeni özellikleri props ile geçiyoruz

// Mevcut bileşeni değiştirmeden genişletiyoruz.
function Button({ label, onClick, style }) {
  return (
    <button onClick={onClick} style={style}>
      {label}
    </button>
  );
}

function App() {
  return (
    <Button
      label="Click Me"
      onClick={() => alert("Button clicked!")}
      style={{ backgroundColor: 'blue' }}
    />
  );
}

Bu şekilde, mevcut bileşeni değiştirmeden props yardımıyla yeni özelliklik kazandırdık.

Liskov Substitution Principle (LSP)

Bir alt sınıfın, üst sınıfının yerine kullanılabilmesini ve uygulamanın doğru şekilde çalışmaya devam etmesini sağlar. Bu, alt sınıfların üst sınıfların davranışlarını bozmadan genişletilebileceği anlamına gelmektedir.

İyi Kullanım

// Üst tür
function Animal(props) {
  return (
    <div>
      <p>{props.name} is an animal.</p>
    </div>
  );
}

// Alt türler
function Fish(props) {
  return (
    <Animal name="Fish" />
  );
}

function Bird(props) {
  return (
    <Animal name="Bird" />
  );
}

Burada, Fish ve Bird bileşenleri Animal bileşeninin yerine geçebilir ve uygulamada herhangi bir soruna yol açmazlar.

❌ Kötü Kullanım

function Fish(props) {
  return (
    <div>
      <p>{props.name} is a fish.</p>
    </div>
  );
}

Burada, Fish bileşeni Animal yerine kullanılamaz çünkü Animal bileşeni name özelliğini beklerken, Fish bileşeni direkt olarak props.name‘i kullanır.

 Interface Segregation Principle (ISP)

Bu prensip, daha küçük ve ihtiyaca özel arayüzler kullanılmasını önerir.

İyi Kullanım

// Kullanıcı bilgilerini gösteren bileşen
const UserInfo = ({ name, email }) => (
  <div>
    <p>Name: {name}</p>
    <p>Email: {email}</p>
  </div>
);

// Kullanıcı fotoğrafını gösteren bileşen
const UserPhoto = ({ photoUrl }) => (
  <img src={photoUrl} alt="User" />
);

// UserProfile bileşeni, UserInfo ve UserPhoto'yu kullanır
const UserProfile = ({ user }) => (
  <div>
    <UserInfo name={user.name} email={user.email} />
    <UserPhoto photoUrl={user.photoUrl} />
  </div>
);

UserProfile bileşenini oluştururken, her bir alt bileşen için ayrı arayüzler (props) tanımlıyoruz. Böylece, her bir alt bileşen sadece ihtiyaç duyduğu veriyi alır.

❌ Kötü Kullanım

// Kullanıcı bilgilerini ve fotoğrafını gösteren bileşenler
// Her ikisi de aynı props nesnesini alıyor, ancak sadece bir kısmını kullanıyor

const UserInfo = ({ user }) => (
  <div>
    <p>Name: {user.name}</p>
    <p>Email: {user.email}</p>
  </div>
);

const UserPhoto = ({ user }) => (
  <img src={user.photoUrl} alt="User" />
);

// UserProfile bileşeni, her iki alt bileşene de aynı user nesnesini geçiriyor
const UserProfile = ({ user }) => (
  <div>
    <UserInfo user={user} />
    <UserPhoto user={user} />
  </div>
);

UserInfo ve UserPhoto bileşenleri, kullanmadıkları verileri de içeren user nesnesini alıyor. Bu, Interface Segregation Principle’a aykırıdır bir davranıştır.

Dependency Inversion Principle (DIP)

Modüllerin veya sınıfların birbirine doğrudan bağlı olmamasını, bunun yerine aralarında abstraction kullanarak iletişim kurmalarını sağlayarak kodun esnekliğini ve değiştirilebilirliğini artıran bir prensiptir.

İyi Kullanım

// API çağrılarını yönetecek arayüz
class DataProvider {
  fetchData() {
    throw new Error("fetchData() method must be implemented.");
  }
}

// Bir API sağlayıcısı için somut uygulama
class ApiProvider extends DataProvider {
  fetchData() {
    // API çağrısı yap
    return "Data from API";
  }
}

// Bileşen, DataProvider arayüzüne bağlıdır
const MyComponent = ({ dataProvider }) => {
  const [data, setData] = useState("");

  useEffect(() => {
    setData(dataProvider.fetchData());
  }, [dataProvider]);

  return <div>{data}</div>;
};

// Kullanım
const apiProvider = new ApiProvider();
<MyComponent dataProvider={apiProvider} />;

Bu sayede, API sağlayıcısını değiştirmek istediğinizde sadece yeni bir sınıf oluşturup DataProvider arayüzünü implemente etmeniz yeterlidir. MyComponent bileşeni üzerinde herhangi bir değişiklik yapmanıza gerek kalmaz, bu da kodunuzun daha esnek ve değiştirilebilir olmasını sağlar.

❌ Kötü Kullanım

// API sağlayıcısının doğrudan kullanımı
const fetchDataFromApi = () => {
  // API çağrısı yap
  return "Data from API";
};

// Bileşen, belirli bir API sağlayıcısına doğrudan bağımlıdır
const MyComponent = () => {
  const [data, setData] = useState("");

  useEffect(() => {
    setData(fetchDataFromApi());
  }, []);

  return <div>{data}</div>;
};

Bu kötü örnekte, MyComponent bileşeni fetchDataFromApi fonksiyonuna doğrudan bağımlıdır. Eğer API sağlayıcısını değiştirmek gerekirse, bileşenin kendisini de değiştirmek gerekecektir. Bu, Dependency Inversion Principle’a aykırı bir davranıştır.

📣 Bu prensiplerin doğru şekilde uygulanması, React projelerinizin kod temizliğini, sürdürülebilirliğini ve genişletilebilirliğini artırarak uzun vadede başarıya ulaşmanızı ve kolay bakım sağlamanızı sağlar. SOLID prensiplerine uygun olarak geliştirilmiş bir kod tabanı, verimliliği artırır ve hataların azalmasına yardımcı olur.