רוב הקוד שאני רואה של מערכות שעובדות עם רכיבים חיצוניים (כמו בסיס נתונים) משלב את החלק שקורא את המידע עם החלק שמעבד אותו ומסיק מסקנות. זה יכול להיות בגלל אילוצים של ORM או בגלל שאנחנו לא חושבים מספיק על הפרדות לפני שכותבים את הקוד, או שחושבים אבל פוחדים מבעיות תחזוקה.
שתי דוגמאות- הקוד של swapi שמחזיר מידע על מלחמת הכוכבים כולל את המחלקה:
class PeopleViewSet(viewsets.ReadOnlyModelViewSet):
queryset = People.objects.all()
serializer_class = PeopleSerializer
search_fields = ('name',)
def retrieve(self, request, *args, **kwargs):
return super(PeopleViewSet, self).retrieve(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
return super(PeopleViewSet, self).list(request, *args, **kwargs)
הקוד של kutt כולל את הבלוק:
export const get = async (match: Partial<Link>, params: GetParams) => {
const query = knex<LinkJoinedDomain>("links")
.select(...selectable)
.where(normalizeMatch(match))
.offset(params.skip)
.limit(params.limit)
.orderBy("created_at", "desc");
if (params.search) {
query.andWhereRaw(
"concat_ws(' ', description, links.address, target, domains.address) ILIKE '%' || ? || '%'",
[params.search]
);
}
query.leftJoin("domains", "links.domain_id", "domains.id");
const links: LinkJoinedDomain[] = await query;
return links;
};
והבעיה בתבנית היא שנוצרה לנו בעיה בבדיקות. בשביל לבדוק את הלוגיקה אנחנו חייבים בסיס נתונים כי הקוד של הלוגיקה כבר מריץ את השאילתה מול בסיס הנתונים. בשני המקרים אפשר לדמיין הפרדה בין שלושה מנגנונים:
הקוד שבונה את השאילתה לפי פרמטרים
הקוד ששולח שאילתה לבסיס נתונים ומחזיר תשובה
הקוד שמפענח את המידע שחזר ומארגן אותו לצורך שליחה למשתמש
רק (2) צריך את הגישה לבסיס הנתונים, ורק (2) הוא הקוד שאינו ספציפי לפונקציה או אפילו למערכת מסוימת. כשמצליחים להוציא את החלק הזה ולקבל אותו מבחוץ, אנחנו לוקחים צעד ענק קדימה לכיוון קוד שקל יותר לבדוק אותו, כי כבר לא צריך בסיס נתונים בבדיקה.
וכן זה נקרא Dependency Injection לדרייבר של בסיס הנתונים. וכן קשה מאוד להגיע לשם אחרי שהמערכת כתובה.