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:
- 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.
- 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.
- 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.
- 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.
- 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.