نکاتِ خیلی مهمِ کدنویسیِ تمیز در اندروید

در برنامه نویسی اندروید یکی از نکات مهمی که نمیتونیم ازش غافل بشیم کدنویسی تمیزه. و بدون کد تمیز امکان نگهداری و توسعه اپلیکیشن سخت و حتی میتونه غیر ممکن باشه! برای همین یک سری نکات برای رعایت این اصول وجود داره که توی این مقاله بهش اشاره میکنیم (اینجا نمیخوام درباره کلین کد و اینا حرف بزنم. قضیه ساده تر و مهم تر از این حرفاست که اغلب رعایت نمیشه):


برای شما

1.Project guidelines

1.1 Project structure

پروژه ها باید از ساختار دستورالعمل های پروژه پیروی کنند که در این لینک (Android Gradle plugin user guide) تعریف شده. پروژه ribot Boilerplate یه منبع خوب برای یادگیری این مورده.

1.2 Project structure

  • 1.2.1 Class files

کلاس ها باید با قوانین UpperCamelCase نام گذاری بشوند.

کلاس هایی که از کلاس کامپوننت اندروید ارث بری شدن باید به اسم همون کامپوننت ختم بشن. به عنوان مثال:

SignInActivity,
SignInFragment,
ImageUploaderService,
ChangePasswordDialog.

  • 1.2.2 Resources files

ریسورس ها باید از قوانین lowercase_underscore پیروی کنند.

  • 1.2.2.1 Drawable files

قرارداد اسم گذاری برای برای drawable ها:

قرارداد اسم گذاری برای برای icon ها(Android iconography guidelines):

قرارداد اسم گذاری برای برای selector state ها:

  • 1.2.2.2 Layout files

لایوت ها باید با کامپوننت هاشون مچ بشن. مانند تصویر زیر:

  • 1.2.2.3 Menu files

مشابه بالا نام منو ها باید با نام کامپوننتش مطابقت داشته باشه. به عنوان مثال اگه ما یک menu تعریف کنیم که توی UserActivity استفاده میشه پس اسمش باید activity_user.xml باشه.

اینکه کلمه menu بخشی از اسم منو باشه روش درستی نیست چون که خودش داخل menu directory قرار داره پس مشخص شده که این فایلِ xml یک منو هست.

  • 1.2.2.4 Values files

ریسورس فایل هایی که باید توی پوشه values قرار داشته باشنم. مانند:

strings.xml, styles.xml, colors.xml, dimens.xml, attrs.xml

2.Code guidelines

2.1 Java language rules

  • 2.1.1 Don’t ignore exceptions

هرگز استثناها رو به حال خودشون رها نکنید:

12345void setServerPort(String value) {
    try {
        serverPort = Integer.parseInt(value);
    } catch (NumberFormatException e) { }
}

همونطور که می بینید کد بالا به دلیل این که پیش بینی میکنیم هیچوقت وارد قسمت catch نمیشه اون رو خالی گذاشتیم! بهتره هیچوقت اینکار رو نکینم! راه درست و اصولی اینه که همه exception ها رو هندل کنیم و اونا رو به حال خودشون رها نکنیم.

  • 2.1.2 Don’t catch generic exception

کاری که تو کد پایین انجام شده اشتباهه:

12345678try {
    someComplicatedIOFunction();        // may throw IOException
    someComplicatedParsingFunction();   // may throw ParsingException
    someComplicatedSecurityFunction();  // may throw SecurityException
    // phew, made it all the way
} catch (Exception e) {                 // I'll just catch all exceptions
    handleError();                      // with one generic handler!
}

این کار اصولی نیست که استثناها رو به شکل عمومی هندل کنیم. چون اینکار باعث میشه که exception هایی که هرگز انتظارش رو نداریم(شامل استثناهای زمان اجرا مثل: ClassCastException) در سطح اپلیکیشن اتفاق بیوفته. این به این معنیه که اگه کسی استثنایی اضافه کنه چون ما بهش گفتیم که در صورت بروز هر نوع خطایی به exception عمومی بره پس کامپایلر اون استثنارو هندل نمیکنه!

برای فهم بهتره اینکه چرا نباید اینکار رو بکنیم میتونید به این لینک برید.

  • 2.1.3 Don’t use finalizers

در اینجا finalizer هنگام اجرای زباله روبی (یا همون garbage collected خودمون) اجرای میشه در حالی که اینکار میتونه برای پاک کردن منابع اللخصوص منابع خارجی مفید باشه اما مشخص نیست اون چه زمانی صدا زده میشه(یا حتی اصلا صدا زده میشه یا نه؟!)

اندرویید از finalizer ها استفاده نمیکنه. پس بجاش میتونیم با هندل کردن درست exception ها اینکار رو انجام بدیم. اما اگه واقعا نیاز به استفاده ازش داشتیم میتونیم یک تابع مانند ()close بنویسیم و با نوشتن یک دستورشرطی هر موقع که نیاز بود ازش استفاده کنیم.

  • 2.1.4 Fully qualify imports

زمانی که میخوایم از کلاسی مثل Bar در پکیج foo استفاده کنیم دو راه داریم.

1- راه بد:

1import foo.*;

2-راه خوب:

1import foo.Bar;

اگه به همه کلاس های foo نیاز نداریم پس بهتره فقط اونی که نیاز داریم رو import کنیم.

2.2 Java style rules

  • 2.2.1 Fields definition and naming

فیلد ها باید بالای فایل تعریف بشن و از قوانین اسم گذاری زیر پیروی کنند:

  • اسم فیلد های پرایویت و غیر استاتیک باید با حرف m شروع بشن.
  • اسم فیلد های پرایویت و استاتیک باید با حرف s شروع بشن.
  • اسم بقیه فیلدها باید با حرف کوچیک شروع بشن.
  • همه حروف فیلد های فاینال و استاتیک باید با حروف بزرگ نوشته بشه و با آندرلاین جدا بشه.

مثال:

12345678public class MyClass {
    public static final int SOME_CONSTANT = 42;
    public int publicField;
    private static MyClass sSingleton;
    int mPackagePrivate;
    private int mPrivate;
    protected int mProtected;
}
  • 2.2.3 Treat acronyms as words

با کلمات اختصاری مثل حروف رفتار کنید. مثلا آیدی رو باید به شکل Id بنویسیم نه ID. مانند عکس زیر:

  • 2.2.4 Use spaces for indentation

از فضاها برای تو رفتگی ها استفاده کنید.

4 فاصله برای بلاک ها

123if (x == 1) {
    x++;
}

8 فاصله برای line wrap ها(فارسیش نمیدونم چی میشه:( )

12Instrument i =
        someLongthat, wouldNotFit, on, one, line);
  • 2.2.5 Use standard brace style

براکت ها رو درست توی همون خط بزارید نه یک بعدش.

1234567891011class MyClass {
    int func() {
        if (something) {
            // ...
        } else if (somethingElse) {
            // ...
        } else {
            // ...
        }
    }
}

استفاده از براکت ها ضروریه درصورتی که کد ما بیشتر از یک خط باشه.

اگه کد از ماکسیمم طول خط کمتره استفاده از براکت ها ضروری نیست.

کد خوب:

1if (condition) body();

کد بد:

12if (condition)
    body();  // bad!
  • 2.2.6 Annotations
  • 2.2.6.1 Annotations practices

برخی از annotation ها با توجه به استاندارد اندروید عبارت اند از:

– هر زمان که میخوایم متدی در کلاس والد رو override کنیم باید از Override@ در بالای متدمون استفاده کنیم.

– ممکنه گاهی متدما یک warning داشته باشه اما ما اطمینان داریم که هشدار برطرفه میشه در این مواقع میتونیم از SuppressWarnings@ استفاده میکنیم. این کار باعث میشه کامپایلر هشدار مربوط به اون متد رو نادیده بگیره.

برای مطالعه بیشتر میتونید به این لینک مراجعه کنید.

  • 2.2.6.2 Annotations style

کلاس ها، متدها، سازنده ها:

زمانی که annotation ها به یک کلاس، متد یا سازنده اضافه میشوند باید هر کدام در یک خط جداگانه نوشته بشوند.

1234/* This is the documentation block about the class */
@AnnotationA
@AnnotationB
public class MyAnnotatedClass { }

فیلدها:

زمانی که annotation ها به یک فیلد اضافه میشن باید در یک خط نوشته بشن مگر اینکه طول اونها از ماکسیمم طول خط بیشتر باشه.

1@Nullable @Mock DataManager mDataManager;
  • 2.2.7 Limit variable scope

دامنه متغیرهای محلی باید حداقل باشه. با انجام اینکار ما خوانایی و قابلیت نگهداری کدمون رو افزایش و احتمال بروز خطا رو کاهش میدیم. هر متغیر باید در محدوده بلاکی تعریف بشه که اون متغیر فقط در اون محدوده تاثیر گذاره.

متغیر های محلی باید زمانی تعریف بشن که برای اولین بار استفاده میشن. همه متغیر های محلی زمانی که تعریف میشن باید دارای مقدار اولیه باشن. اگه اطلاع کافی برای مقداردهی اولیه یک متغیر محلی نداریم باید تا زمان تعریف کردن اون دست نگهداریم.

  • 2.2.8 Order import statements

ترتیب import ها: ترتیب import ها رو خود اندروید استدیو رعایت میکنه پس لازم نیست نگران این مورد باشید اما دونستنش خالی از لطف نیست.

ترتیب به این صورته:

1 – import های اندروید
2 – import های third parties(com, junit, net, org)
3 – import های java و javax
4 – import های همون پروژه

  • 2.2.9 Logging guidelines

استفاده از متد های لاگ برای شناسایی مشکلات در اختیار برنامه نویس ها قرار میگیره.

Log.v(String tag, String msg) (verbose)

Log.d(String tag, String msg) (debug)

Log.i(String tag, String msg) (information)

Log.w(String tag, String msg) (warning)

Log.e(String tag, String msg) (error)

یک قانون کلی وجود داره که ما از اسم کلاس ها به عنوان tag استفاده میکنیم و اون رو بالای یک کلاس به شکل static final تعریف میکنیم.

1234567public class MyClass {
    private static final String TAG = MyClass.class.getSimpleName();

    public myMethod() {
        Log.e(TAG, "My error message");
    }
}

لاگ های VERBOSE و DEBUG باید هنگام ساخت نسخه نهایی غیر فعال بشن. حتی خوبه که بقیه لاگ ها هم غیرفعال بشن اما ممکنه که تشخیص بدید که استفاده از لاگ ها ممکنه برای کشف مشکلات مفید باشه که در اون صورت باید مطمئن بیشید که اطلاعاتی مثل آدرس ایمیل، آیدی و… نشت نکنه!

برای اینکه لاگ هارو در نسخه دیباگ نشون بدیم:

1if (BuildConfig.DEBUG) Log.d(TAG, "The value of x is " + x);
  • 2.2.10 Class member ordering

هیچ راه حل درستی برای ترتیب اعضای کلاس وجود نداره اما میتونیم از یک روش منطقی و ثابت برای خوانایی کدمون استفاده کنیم.

ترتیبی که توصیه میشه به شکل زیره:

  1. Constants
  2. Fields
  3. Constructors
  4. Override methods and callbacks (public or private)
  5. Public methods
  6. Private methods
  7. Inner classes or interfaces

به عنوان مثال:

123456789101112131415161718192021222324public class MainActivity extends Activity {
    private static final String TAG = MainActivity.class.getSimpleName();

    private String mTitle;
    private TextView mTextViewTitle;

    @Override
    public void onCreate() {
        ...
    }

    public void setTitle(String title) {
    	mTitle = title;
    }

    private void setUpView() {
        ...
    }

    static class AnInnerClass {

    }

}

اگه کلاسمون از کامپوننت های اندروید مثل اکتیویتی و فرگمنت ارث بری میکنه و متد های لایف سایکل رو اورراید میکنیم ترتیب درست به شکل زیره:

12345678910111213141516public class MainActivity extends Activity {

	//Order matches Activity lifecycle
    @Override
    public void onCreate() {}

    @Override
    public void () {}

    @Override
    public void () {}

    @Override
    public void onDestroy() {}

}
  • 2.2.11 Parameter ordering in methods

توی اندروید ممکنه ما متدی بنویسیم که از Context استفاده میکنه. در این صورت Context باید اولین پارامتر ورودی متدمون باشه.

اگه متد ما نیاز به یک اینترفیس callback داره برعکسِ Context اون باید آخرین پارامتر ورودی متدمون باشه.

مثال:

12345// Context always goes first
public User loadUser(Context context, int userId);

// Callbacks always go last
public void loadUserAsync(Context context, int userId, UserCallback callback);
  • 2.2.12 String constants, naming, and values

المان های زیادی در اندروید وجود داره که برای استفاده از اونها ممکنه مجبور بشیم که ثابت های رشته ای تعریف بکنیم. حتی در برنامه های کوچک. مانند SharedPreferences، Bundle یا Intent

باید اونها رو به شکل final static تعریف کنیم. و نام اونها رو باید به شکل پیشوند بنویسیم. این پیشوند ها باید به شکل زیر باشند.

مثال:

12345678// Note the value of the field is the same as the name to avoid duplication issues
static final String PREF_EMAIL = "PREF_EMAIL"
static final String BUNDLE_AGE = "BUNDLE_AGE"
static final String ARGUMENT_USER_ID = "ARGUMENT_USER_ID"

// Intent-related items use full package name as value
static final String EXTRA_SURNAME = "com.myapp.extras.EXTRA_SURNAME"
static final String ACTION_OPEN_USER = "com.myapp.action.ACTION_OPEN_USER"
  • 2.2.13 Arguments in Fragments and Activities

زمانی که میخوایم دیتایی رو ازطریق intent یا bundle به اکتیویتی یا فرگمنت منتقل کنیم باید کلیدها برای دو مقدار متفاوت از قوانین بالا پیروی کنه.

زمانی که یک اکتیویتی یا فرگمنت انتظار یک آرگومان رو داره، در این صورت باید یک متد public static برای تسهیل ایجاد intent یا فرگمنت مناسب بنویسیم.

در مورد اکتیویتی ها این متد با اسم ()getStartIntent نوشته میشه. مانند کد زیر:

12345public static Intent getStartIntent(Context context, User user) {
	Intent intent = new Intent(context, ThisActivity.class);
	intent.putParcelableExtra(EXTRA_USER, user);
	return intent;
}

در مورد فرگمنت ها این متد ()newInstance نام داره و مثل کد زیر نوشته میشه:

1234567public static UserFragment newInstance(User user) {
	UserFragment fragment = new UserFragment();
	Bundle args = new Bundle();
	args.putParcelable(ARGUMENT_USER, user);
	fragment.setArguments(args)
	return fragment;
}

نکته اول: این متدها باید بالای کلاس و قبل از متد ()onCreate قرار بگیرند.

نکته دوم: اگه از روش های بالا استفاده کردیم کلید ها باید به صورت private تعریف بشند چون دیگه بیرون از کلاس به اونها نیازی نیست.

  • 2.2.14 Line length limit

طول خطوط کد نباید از 100 کاراکتر بیشتر بشه. اگه کد ما بیشتر از اینه پس باید با دو روش زیر تقسیم بشه.

  1. استفاده از یک متغیر محلی یا یک متد.(ترجیحا)
  2. تقیسم کردن یک خط به چند خط زیر هم.

در این مورد دو تا استثنا وجود داره که ممکنه از 100 کارکتر بیشتر بشه:

  1. خطوطی که امکان تقسیم شدن رو ندارند. مانند URL ها طولانی در کامنت ها
  2. تعریف ها مربوط به import ها و package ها.
  • 2.2.14.1 Line-wrapping strategies

برای توضیح دادن line_wrap یک فرمول دقیق و مشخصی وجود نداره. اما تعداد کمی قانون متداول وجود داره.

Break at operators

برای شکستن خطوط طولانی میتونیم اون رو قبل از عملگر ها بشکنیم مثل کد زیر:

12int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne
        + theFinalOne;

Assignment Operator Exception

یک قانون دیگه وجود داره که در اون میتونیم این شکستن رو بعد از عملگر مساوی(=) انجام بدیم. مثل کد زیر:

12int longName =
        anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;

Method chain case

زمانی که چند متد به شکل زنجیر وار صدا زده میشن(در واقع وقتی داریم از builder ها استفاده میکنیم) میتونیم بعد از صدا زدن متد و قبل از کاراکتر (.) این شکستن رو انجام بدیم. مثل کد زیر:

1Picasso.with(context).load("http://ribot.co.uk/images/sexyjoe.jpg").into(imageView);

123Picasso.with(context)
        .load("http://ribot.co.uk/images/sexyjoe.jpg")
        .into(imageView);

Long parameters case

وقتی یک متد پارامتر های زیادی داره و باعث طولانی تر شدن خط ما میشه ما باید در هر پارامتر و بعد از کاما(,) این شکستن رو انجام بدیم. مثل کد زیر:

12loadPicture(context, "http://ribot.co.uk/images/sexyjoe.jpg", mImageViewProfilePicture);

1234loadPicture(context,
        "http://ribot.co.uk/images/sexyjoe.jpg",
        mImageViewProfilePicture,
        clickListener);
  • 2.2.15 RxJava chains styling

در استفاده از متدهای زنجیره ای rxjava باید قبل از نقطه(.) شکستن اتفاق بیوفته. مثل کد زیر:

123456789101112131415public Observable<Location> syncLocations() {
    return mDatabaseHelper.getAllLocations()
            .concatMap(new Func1<Location, Observable<? extends Location>>() {
                @Override
                 public Observable<? extends Location> call(Location location) {
                     return mRetrofitService.getLocation(location.id);
                 }
            })
            .retry(new Func2<Integer, Throwable, Boolean>() {
                 @Override
                 public Boolean call(Integer numRetries, Throwable throwable) {
                     return throwable instanceof RetrofitError;
                 }
            });
}

2.3 XML style rules

  • 2.3.1 Use self closing tags

اگه یک المان در XML هیچ محتوایی در داخلش نداره پس باید اون رو در خودش ببندیم مانند کد زیر:

کد خوب:

1234<TextView
	android:id="@+id/text_view_profile"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content" />

کد بد:

123456<!-- Don\'t do this! -->
<TextView
    android:id="@+id/text_view_profile"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >
</TextView>
  • 2.3.2 Resources naming

آیدی ریسورس ها باید به شکل lowercase_underscore انجام بشه.

  • 2.3.2.1 ID naming

آیدی باید به شکل پیشوند و با نام المان شروع بشه:

مثال:

12345<ImageView
    android:id="@+id/image_profile"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

12345<menu>
	<item
        android:id="@+id/menu_done"
        android:title="Done" />
</menu>
  • 2.3.2.2 Strings

نام رشته باید با یک پیشوند از بخشی که به اون تعلق داره شروع بشه. برای مثال:

registration_email_hintیاregistration_name_hint

اگه یک رشته به هیچ بخش مشخصی تعلق نداره پس باید از قوانین زیر پیروی کنه:

  • 2.3.2.3 Styles and Themes

برخلاف بقیه ریسورس ها استایل ها باید با قانون UpperCamelCase.نام گذاری بشن.

  • 2.3.3 Attributes ordering

به عنوان یک قاعده کلی باید سعی کنید attribute های مشابه رو با هم گروه بندی کنیم. یک راه خوب برای مرتب سازی اونها به شکل زیره:

  1. View Id
  2. Style
  3. Layout width and layout height
  4. Other layout attributes, sorted alphabetically
  5. Remaining attributes, sorted alphabetically

2.4 Tests style rules

  • 2.4.1 Unit tests

کلاسهای آزمون باید با نام کلاس مورد نظر مطابقت داشته باشند. مثلا اگه ما کلاس تستی داشته باشیم که مربوط به DataBaseHelper پس باید اسم کلاس تستمون DataBaseHelperTest باشه.

متد تست با Test@ حاشیه نویسی میشه و باید با نام متدی که تست میشه شروع بشه و بعد پیشبینی و/یا رفتار مورد انتظار نوشته بشه.به مثال زیر نوجه کنید:

  • Template:
1 @Test void methodNamePreconditionExpectedBehaviour()
  • Example:
1@Test void signInWithEmptyEmailFails()

اگه اسم متد تست به اندازه کافی واضحه پس نیازی نیست به اضافه کردن پیشبینی و/یا رفتار مورد انتظار نیست.

گاهی ممکنه کلاسی دارای متدهای بزرگ زیادی باشه، و اغلب نیازه که برای هر کدوم چندین تست نوشته بشه. در این صورت خوبه که کلاس های تست رو به چند کلاس مختلف تقسیم کنیم. برای مثال فرض کنید کلاسی به اسم DataManager وجود داره که دارای متدهای بزرگ زیادیه، پس ما میتونیم اونو به کلاس های تست DataManagerSignInTest ،DataManagerLoadUsersTest و … تقسیم کنیم.

  • 2.4.2 Espresso tests

معمولا هر کلاس تست Espresso برای یک اکتیویتی نوشته شده پس نام اون باید با نام اکتیویتی تطبیق داشته باشه مثل: SignInActivityTest

باید هنگام استفاده از Espressso هر متد رو در خط جداگانه ای نوشت.

123onView(withId(R.id.view))
        .perform(scrollTo())
        .check(matches(isDisplayed()))

خب این مقاله تموم شد.

امیدوارم که خوب ترجمه کرده باشم. لینک اصلی مقاله اینجاست و اینجاست.

اگه جایی رو بد توضیح دادم یا اشتباه نوشتم ممنون میشم بهم بگین.

نویسنده مطلب: Mahdi Sadeghi

منبع مطلب

به فکر سرمایه‌گذاری هستی؟

با هر سطحی از دانش در سریع‌ترین زمان با آموزش گام به گام، سرمایه گذاری را تجربه کن. همین الان میتونی با لینک زیر ثبت نام کنی و ۱۰ درصد تخفیف در کارمزد معاملاتی داشته باشی

ثبت نام و دریافت جایزه
ممکن است شما بپسندید
نظر شما درباره این مطلب

آدرس ایمیل شما منتشر نخواهد شد.