Clean Code’dan Qeydlər: Üçüncü hissə— Funksiyalar

Simuratli
5 min readJun 5, 2020

--

Bir funksiya nə qədər qısa olmalıdır?

Aşağıdakı kod sətirlərinə baxın.

public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
if (suiteSetup != null) {
WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName).append("\n");
}
}
WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
if (setup != null) {
WikiPagePath setupPath =
wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName).append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .")
.append(tearDownPathName).append("\n");
}
if (includeSuiteSetup) {
WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
if (suiteTeardown != null) {
WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -teardown .")
.append(pagePathName).append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}

Bu koda 3 dəqiqə baxdıqda heçnə anlaşılmır. Kodu refactoring etdikdın sonra isə aşağıdakı hala gəlir.

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage) {
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}

Son toxunuşlardan sonra isə:

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}

Fuksiya yazarkən ilk qayda onun qısa olmasıdır. İkinci qayda isə daha da qısatdılmalı olduğudur.

Bugünlərdə normal bir fontla və böyük bir monitorla bir sətirə 150 sətir sığdıra bilirik. Lakin bu o demək deyilki sizin funksiyanız 150 sətir olmalıdır.

Əslində isə funksiyalar 20 sətirdən çox olmamalıdır.

1991-ci ildə Kent Becki ziyarətə getmişdim. Birlikdə biraz kod yazdıq. Mənə bir ara Sparkle adında kiçik bir Java/Swing programı göstərdi. Hər funksiyası 2,3 sətirdən ibarət idi və o anda düşündüm ki funkiyalar belə yazılmalıdır!

Sadəcə 1 şey etməli

Yuxarıdakı kodun ilk halında 1 şeydən çox əməliyyat yerinə yetirdiyi məlumdur. Bufferlər yaradır və səhifəni yükləyərək HTML faylı yaradır. Digər tərəfdən isə son halı 1 şeyi edir. Funksiyalar sadəcə 1 şey etməldirlər. Buradakı problem isə “bir şey”in nə olduğunu tapmaqdır. İlk funksiyamızın son halı 1 şeymi edir?

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}

Əslində 3 şey edir.

  1. Səhifənin bir test səhifəsi olub olmadığına baxır
  2. Əgər elə isə includeSetupAndTeardownPages metodunu çağırır.
  3. HTML faylını yaradır.

Bəs hansı? Method sadəcə 1 işmi yoxsa 3 işmi edir? Methodu aşağıdakı şəkildə ifadə edə bilərik.

“Bir səhifənin test olub olmadığını yoxlayırıq əgər test isə Setup Teardown səhifələrini daxil edirik. Hər 2 vəziyyətdə də html faylını yaradırıq.”

Əgər funksiya sadəcə adında verilən addımları edirsə o funksiya heçnə etmir.

Nümunə kodumuzun ilk versiyasında birdən çox iş görürdük. İkinci versiyasında isə 2 səviyyə soyutlama (two-level abstraction) edirdik. Buna isbat kodumuzun 3 cü və son halıdır. Son halında kodumuzu daha çox kiçildə bilmirik. İf ifadəsini yeni bir method içinə yaza bilərdik lakin bu kodu yenidən yazmaq olardı.

Switch

Qısa bir switch yazmaq olduqca çətindir. 1 şey edən switch də yazmaq çətindir. Təbiətinə görə switch nə qədər iş görür. Ancaq hər switch ifadəsinin alt səviyə bir sinifə daxil edildiyindən və təkrarlanmadığından əmin ola bilərik. Əlbəttə bunu polimorfizmlə edirik.

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);
}
}

Bu funksiyada bir çox problem var

  1. Çox böyük və Employee tipleri əlavə edildikcə artmağa davam edəcək.
  2. Birdən çox şey edir.
  3. Single Responsibility qaydasına tabe deyil.
  4. Closed Prinsipinədə tabe deyil.

Bu kodun problemini həll etmək üçün “Abstract Factory” design pattern dən istifadə etmək və başqa heç kimin görməyinə icazə verməməkdir.

public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}-----------------------------------------------------------------public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}------------------------------------------------------------------public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}

Employee FactoryImpl, Employee törəməlırinin uyğun nümunələrinin yaratmaq üçün switch ifadəsini istifadə edəcək və calculatePay, isPaydaydeliverPay kimi müxtəlif funksiyalar , Employee interfacesindən polimorfik olaraq göndəriləcək.

Məncə switch ifadələri yanlız aşağıdakı hallarda istifadə edilə bilər.

  • Yalnızca bir dəfə görünəcəklərsə
  • Sistemin geri qalanının görəməyəcəyi bir yerdə gizlənərsə
  • Polimorfik obyektləri yaratmaq üçün istifadə ediləcəksə.

Açıqlayıcı adlar istifadə edin

Yuxarıdakı nümunəmizdə testableHtml funksiyamızın adını SetupTeardownIncluder.render olaraq dəyişdirdim. Bu ad dahada əlverişlidir çünki etdiyimiz işi izah edir. private metotlarımızın adlarınada eyni şəkildə anlamlı adlar verdim; isTestable ya da includeSetupAndTeardownPages kimi…

Ward’ın prinsipini xatırlayaq : “Programın hər parçası gözlədiyimiz kimi çıxdığında, təmiz kod üzərinde işlədiyinizi anlayırsınız.

Uzun adlar istifadə etməkdən çəkinməyin. Uzun adlar kiçik tapmaca kimi adlardan və şərhlərdən daha yaxşıdır.

Boolean Arqumentlər

Boolean arqumentlər çirkindir. Funksiyalara parametr olaraq keçirmək isə kabusdur. Methodumuzun anlaşıqlığını asanlaşdırı və “Bu funksiya 1 dən çox şey edir ” deyə bağırır. Arqument true isə bir şey elə false isə başqa şey elə.

Obyekt Arqumentlər

Əgər funksiyada 2 yada 3 arqumentdən çoxuna ehtiyac varsa bu arqumentlər obyekt daxilində verilməlidir.

Bu iki nümunəyə baxaq. Obyekt yaradaraq dəyişkənin dəyərini artırnaq yada azaltmaq hiylə kimi görünür ama deyildir:

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

Feillər və Adlar

Bir funksiya üçün yaxşı bir ad seçmək onu yaxşı yazmaq deməkdir.

Tək arqumentli (Monad) vəziyyətlərdə, funksiya və arqument çox yaxşl bir ad və feil cütlüsü olmalıdır. Məsələn write(name) adına sahib bir funksiya olduqca açıqdır. Hətat writeField(name) bizə name’in bir yer (field) olduğunu ve bir yerlərə yazılacaağını deyir .

Yan təsirlər yaratma (Side Effects)

Yan təsirlər bizi aldadır. Funksiya bizə bir şey etməyə söz verir ama gizli şeylər də edir. Bəzən öz dəyişkənlərində belə gizli dəyişikliklər edir. Bəzən bunları funksiyaya keçirilən parametrlər yada qloballar ilə edirlər.

Bu yararsız koda baxaq:

public class UserValidator {
private Cryptographer cryptographer;

public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}

Bu funksiya bir userName’i bir password’ə bərabərləşdirmək üçün standart bir algoritm istifadə edir. Bərabərləşərsə true, bərabərləşməzsə false geri qayıdır. Ancaq 1 yan təsiri var o da Session.initialize() ı çağırmasıdır. checkPassword, adında deyildiyi kimi şifrəni yoxlayır. Ancak Session’ın initialize ediləcəyini demir . Bu halda bunu istifadə edən zaman mövcud hesabı silmiş oluruq.

try/catch Bloklarını Ayırın

try/catch blokları təbiətlərin səbəbi ilə çirkindirlər. Normal işləyişi kodun qarışıqlığını artırır. Bu səbəblə trycatch bloklarının gövdələrindəki funksiyaları ayırmaq lazımdır.

public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}

private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
logger.log(e.getMessage());
}

Bu numünədə delete funksiyası tamamilə error processing ilə əlaqəlidir. Anlaşılması sadədir. deletePageAndAllReferences funksiyası isə tamamilə 1 səhifəni silmə ilə əlaqəlidir. Bunun istifadəsi kodun oxunurluğunu rahatlaşdırır.

Bəs belə methodları necə yaza bilərik?

Kod yazmaq yazı yazmağı bir törəməsidir. Bir məqalə haqqında birşeylər yazdığımızda ilk öncə fikirlərimizi toplayırıq. Daha sonra isə qulağa xoş gələcək şəkildə yazırıq.

Mən kod yazarkən funksiyaların ilk halları olduqca qarışıq olur. Bur sürü iç içə döngülər olur , uzun arqument siyahıları olur. Verdiyim adlandırma isə çox pis olur. Ancaq bu qeyri professional sətirləri əhatə edən testlərim vardır.

Ən sonda koduma yenidən nəzər yetirirəm ,adları dəyişdirirəm. Methodları kiçildirəm və sıraya qoyuram. Sonunda isə bu danışdığım qaydalara uyğun kod almış oluram.

.

--

--

Simuratli
Simuratli

Written by Simuratli

MSc. High Energy and Plasma Physics | B.A. Computer Engineering | Content Creator. https://bento.me/simuratli

Responses (1)