یه برنامه نویس اندروید :/
نکاتِ خیلی مهمِ کدنویسیِ تمیز در اندروید
در برنامه نویسی اندروید یکی از نکات مهمی که نمیتونیم ازش غافل بشیم کدنویسی تمیزه. و بدون کد تمیز امکان نگهداری و توسعه اپلیکیشن سخت و حتی میتونه غیر ممکن باشه! برای همین یک سری نکات برای رعایت این اصول وجود داره که توی این مقاله بهش اشاره میکنیم (اینجا نمیخوام درباره کلین کد و اینا حرف بزنم. قضیه ساده تر و مهم تر از این حرفاست که اغلب رعایت نمیشه):
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
هرگز استثناها رو به حال خودشون رها نکنید:
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) { }
}
همونطور که می بینید کد بالا به دلیل این که پیش بینی میکنیم هیچوقت وارد قسمت catch نمیشه اون رو خالی گذاشتیم! بهتره هیچوقت اینکار رو نکینم! راه درست و اصولی اینه که همه exception ها رو هندل کنیم و اونا رو به حال خودشون رها نکنیم.
- 2.1.2 Don't catch generic exception
کاری که تو کد پایین انجام شده اشتباهه:
try {
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- راه بد:
import foo.*;
2-راه خوب:
import foo.Bar;
اگه به همه کلاس های foo نیاز نداریم پس بهتره فقط اونی که نیاز داریم رو import کنیم.
2.2 Java style rules
- 2.2.1 Fields definition and naming
فیلد ها باید بالای فایل تعریف بشن و از قوانین اسم گذاری زیر پیروی کنند:
- اسم فیلد های پرایویت و غیر استاتیک باید با حرف m شروع بشن.
- اسم فیلد های پرایویت و استاتیک باید با حرف s شروع بشن.
- اسم بقیه فیلدها باید با حرف کوچیک شروع بشن.
- همه حروف فیلد های فاینال و استاتیک باید با حروف بزرگ نوشته بشه و با آندرلاین جدا بشه.
مثال:
public 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 فاصله برای بلاک ها
if (x == 1) {
x++;
}
8 فاصله برای line wrap ها(فارسیش نمیدونم چی میشه:( )
Instrument i =
someLongthat, wouldNotFit, on, one, line);
- 2.2.5 Use standard brace style
براکت ها رو درست توی همون خط بزارید نه یک بعدش.
class MyClass {
int func() {
if (something) {
// ...
} else if (somethingElse) {
// ...
} else {
// ...
}
}
}
استفاده از براکت ها ضروریه درصورتی که کد ما بیشتر از یک خط باشه.
اگه کد از ماکسیمم طول خط کمتره استفاده از براکت ها ضروری نیست.
کد خوب:
if (condition) body();
کد بد:
if (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 ها به یک کلاس، متد یا سازنده اضافه میشوند باید هر کدام در یک خط جداگانه نوشته بشوند.
/* This is the documentation block about the class */
@AnnotationA
@AnnotationB
public class MyAnnotatedClass { }
فیلدها:
زمانی که annotation ها به یک فیلد اضافه میشن باید در یک خط نوشته بشن مگر اینکه طول اونها از ماکسیمم طول خط بیشتر باشه.
@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 تعریف میکنیم.
public class MyClass {
private static final String TAG = MyClass.class.getSimpleName();
public myMethod() {
Log.e(TAG, "My error message");
}
}
لاگ های VERBOSE و DEBUG باید هنگام ساخت نسخه نهایی غیر فعال بشن. حتی خوبه که بقیه لاگ ها هم غیرفعال بشن اما ممکنه که تشخیص بدید که استفاده از لاگ ها ممکنه برای کشف مشکلات مفید باشه که در اون صورت باید مطمئن بیشید که اطلاعاتی مثل آدرس ایمیل، آیدی و... نشت نکنه!
برای اینکه لاگ هارو در نسخه دیباگ نشون بدیم:
if (BuildConfig.DEBUG) Log.d(TAG, "The value of x is " + x);
- 2.2.10 Class member ordering
هیچ راه حل درستی برای ترتیب اعضای کلاس وجود نداره اما میتونیم از یک روش منطقی و ثابت برای خوانایی کدمون استفاده کنیم.
ترتیبی که توصیه میشه به شکل زیره:
- Constants
- Fields
- Constructors
- Override methods and callbacks (public or private)
- Public methods
- Private methods
- Inner classes or interfaces
به عنوان مثال:
public 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 {
}
}
اگه کلاسمون از کامپوننت های اندروید مثل اکتیویتی و فرگمنت ارث بری میکنه و متد های لایف سایکل رو اورراید میکنیم ترتیب درست به شکل زیره:
public 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 اون باید آخرین پارامتر ورودی متدمون باشه.
مثال:
// 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 تعریف کنیم. و نام اونها رو باید به شکل پیشوند بنویسیم. این پیشوند ها باید به شکل زیر باشند.
مثال:
// 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 نوشته میشه. مانند کد زیر:
public static Intent getStartIntent(Context context, User user) {
Intent intent = new Intent(context, ThisActivity.class);
intent.putParcelableExtra(EXTRA_USER, user);
return intent;
}
در مورد فرگمنت ها این متد ()newInstance نام داره و مثل کد زیر نوشته میشه:
public 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 کاراکتر بیشتر بشه. اگه کد ما بیشتر از اینه پس باید با دو روش زیر تقسیم بشه.
- استفاده از یک متغیر محلی یا یک متد.(ترجیحا)
- تقیسم کردن یک خط به چند خط زیر هم.
در این مورد دو تا استثنا وجود داره که ممکنه از 100 کارکتر بیشتر بشه:
- خطوطی که امکان تقسیم شدن رو ندارند. مانند URL ها طولانی در کامنت ها
- تعریف ها مربوط به import ها و package ها.
- 2.2.14.1 Line-wrapping strategies
برای توضیح دادن line_wrap یک فرمول دقیق و مشخصی وجود نداره. اما تعداد کمی قانون متداول وجود داره.
Break at operators
برای شکستن خطوط طولانی میتونیم اون رو قبل از عملگر ها بشکنیم مثل کد زیر:
int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne
+ theFinalOne;
Assignment Operator Exception
یک قانون دیگه وجود داره که در اون میتونیم این شکستن رو بعد از عملگر مساوی(=) انجام بدیم. مثل کد زیر:
int longName =
anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;
Method chain case
زمانی که چند متد به شکل زنجیر وار صدا زده میشن(در واقع وقتی داریم از builder ها استفاده میکنیم) میتونیم بعد از صدا زدن متد و قبل از کاراکتر (.) این شکستن رو انجام بدیم. مثل کد زیر:
Picasso.with(context).load("http://ribot.co.uk/images/sexyjoe.jpg").into(imageView);
Picasso.with(context)
.load("http://ribot.co.uk/images/sexyjoe.jpg")
.into(imageView);
Long parameters case
وقتی یک متد پارامتر های زیادی داره و باعث طولانی تر شدن خط ما میشه ما باید در هر پارامتر و بعد از کاما(,) این شکستن رو انجام بدیم. مثل کد زیر:
loadPicture(context, "http://ribot.co.uk/images/sexyjoe.jpg", mImageViewProfilePicture);
loadPicture(context,
"http://ribot.co.uk/images/sexyjoe.jpg",
mImageViewProfilePicture,
clickListener);
- 2.2.15 RxJava chains styling
در استفاده از متدهای زنجیره ای rxjava باید قبل از نقطه(.) شکستن اتفاق بیوفته. مثل کد زیر:
public 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 هیچ محتوایی در داخلش نداره پس باید اون رو در خودش ببندیم مانند کد زیر:
کد خوب:
<TextView
android:id="@+id/text_view_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
کد بد:
<!-- 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
آیدی باید به شکل پیشوند و با نام المان شروع بشه:
مثال:
<ImageView
android:id="@+id/image_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<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 های مشابه رو با هم گروه بندی کنیم. یک راه خوب برای مرتب سازی اونها به شکل زیره:
- View Id
- Style
- Layout width and layout height
- Other layout attributes, sorted alphabetically
- Remaining attributes, sorted alphabetically
2.4 Tests style rules
- 2.4.1 Unit tests
کلاسهای آزمون باید با نام کلاس مورد نظر مطابقت داشته باشند. مثلا اگه ما کلاس تستی داشته باشیم که مربوط به DataBaseHelper پس باید اسم کلاس تستمون DataBaseHelperTest باشه.
متد تست با Test@ حاشیه نویسی میشه و باید با نام متدی که تست میشه شروع بشه و بعد پیشبینی و/یا رفتار مورد انتظار نوشته بشه.به مثال زیر نوجه کنید:
- Template:
@Test void methodNamePreconditionExpectedBehaviour()
- Example:
@Test void signInWithEmptyEmailFails()
اگه اسم متد تست به اندازه کافی واضحه پس نیازی نیست به اضافه کردن پیشبینی و/یا رفتار مورد انتظار نیست.
گاهی ممکنه کلاسی دارای متدهای بزرگ زیادی باشه، و اغلب نیازه که برای هر کدوم چندین تست نوشته بشه. در این صورت خوبه که کلاس های تست رو به چند کلاس مختلف تقسیم کنیم. برای مثال فرض کنید کلاسی به اسم DataManager وجود داره که دارای متدهای بزرگ زیادیه، پس ما میتونیم اونو به کلاس های تست DataManagerSignInTest ،DataManagerLoadUsersTest و ... تقسیم کنیم.
- 2.4.2 Espresso tests
معمولا هر کلاس تست Espresso برای یک اکتیویتی نوشته شده پس نام اون باید با نام اکتیویتی تطبیق داشته باشه مثل: SignInActivityTest
باید هنگام استفاده از Espressso هر متد رو در خط جداگانه ای نوشت.
onView(withId(R.id.view))
.perform(scrollTo())
.check(matches(isDisplayed()))
خب این مقاله تموم شد.
امیدوارم که خوب ترجمه کرده باشم. لینک اصلی مقاله اینجاست و اینجاست.
اگه جایی رو بد توضیح دادم یا اشتباه نوشتم ممنون میشم بهم بگین.
مطلبی دیگر از این انتشارات
چرا واجب است CMake یاد بگیریم؟
مطلبی دیگر از این انتشارات
برنامه نویسی S.O.L.I.D
مطلبی دیگر از این انتشارات
داستان تلگرام و اشغال همیشگی فضای دیسک - macOS