A Factory method tervezési minta nagyon sokszor tud hasznos lenni. Tételezzük fel, hogy egy objektumot kell létrehoznunk, de az objektum létrehozásához több másik komponensre is szükségünk van. Ebben az esetben a Dependency inversiont követve a létrehozandó objektumunknak függőségnek átadhatnánk a szükséges komponenseket. Azonban ez a megközelítés nem biztos, hogy a legszerencsésebb, mivel a konstruktor kód igencsak nagyra nőhet a leimplementálandó logika miatt.
Szerencsésebb lenne, ha az objektum létrehozását kiszerveznénk egy külön metódusba, ami legyártja számunkra az objektumot. A metódus ugyanúgy a new operátor segítségével fogja létrehozni nekünk az objektumot, de a konstruktorba írandó logikát így kiszervezhetjük egy külön komponensnek. Így a kódunk jobban tudja követni a Single responsibility elvet.
A Factory minta számos helyen szerepel a .NET keretrendszerben is. Például a korábban bemutatott Task osztály is rendelkezik egy Factory mezővel, ami ezt a mintát valósítja meg.
Nézzünk egy egyszerű implementációt. Tételezzük fel, hogy a felhasználótól bekérjük a nevét egy szövegként. Ez alapján létre kell hoznunk egy User objektumot, ami külön tároja a keresztnevet és a vezetéknevet:
public class User
{
public string FirstName { get; init; }
public string LastName { get; init; }
public User()
{
FirstName = string.Empty;
LastName = string.Empty;
}
}
public static class UserFactory
{
public static User Create(string name)
{
string[] parts = name.Split(' ');
if (parts.Length == 2)
{
return new User
{
FirstName = parts[0],
LastName = parts[1]
};
}
return new User();
}
}
Mint látható, a User osztály konstruktorából a logika átkerült egy külön osztályba.
A Factory ezen kívül még akkor tud nagyon hasznos lenni, ha a programunkban több hasonló osztályt kell létrehoznunk, amelyek egy közös őssel rendelkeznek. Ebben az esetben a Factory minta nélkül valószínűleg a kódunk egy csomó new utasítással létrehozott objektummal lenne tele, átláthatatlan konstruktor hívások sorozatával.
Az átláthatatlanság mellett a nagyobb baj, hogy ha egy ilyen gyakran használt osztály kontstuktora változik, akkor bizony millió helyen kell majd utána húzni a kódot, ami nem kellemes.
Ennél jóval kulturáltabb, ha az ilyen osztályok létrehozását egy metódusba és osztályba szervezzük, ami az ősosztályból fog visszaadni egy példányt úgy, hogy az azonos részeket már nekünk beállította és csak az eltérő paramétereket adjuk a metódusnak argumentumként. Ezen megoldás használatával csak egy helyen kell átírnunk a konstruktor hívást ha az változik, valamint átláthatóbb kódot kapunk.
interface IUser
{
string Name { get; set; }
}
class Teacher : IUser
{
public string Name { get; set; }
}
class Student : IUser
{
public string Name { get; set; }
}
enum UserType
{
Tanulo,
Tanar,
}
class UserFactory
{
public IUser Create(UserType type, string name)
{
if (type == UserType.Tanulo)
return new Student { Name = name };
else if (type == UserType.Tanar)
return new Teacher { Name = name };
else
throw new InvalidOperationException("unknown type");
}
}
A kódban az IFelhasznalo interfész definiálja a közös őst, amit majd meg kell valósítani, a Tanulo és Tanar osztályok pedig a konkrét implementácók.
A FelhasznaloFactory a nevéből adódóan a factory implementáció. Ez a FelhasznaloTipus enum segítségével gyárt IFelhasznalo implementációkat.
Előnyök
- Open/Closed elvet követi, mivel újabb típusok bevezetése nem töri a kód meglévő részét
- Követi a Single responsibility-t, mivel az osztály létrehozás egy komponens feladata
- Segít elkerülni a szoros kapcsolatot az osztály létrehozó és fogyasztó között
Hátrányok
- A kód komplikáltabb lesz, mivel több új osztály bevezetésével fog járni.