سلام، دنیا

سلام دنیا

شما می‌توانید کدهای این بخش را اینجا ببینید

یک رسم قدیمی است که اولین مسئله‌ی برنامه‌نویسی در یک زبان Hello, World یا سلام‌ دنیا باشد.

  • هر جا که می‌خواهید یک فولدر بسازید

  • فایل hello.go را ایجاد کنید و محتوای زیر را در آن بنویسید

package main

import "fmt"

func main() {
	fmt.Println("Hello, world")
}

برای اجرای آن دستور go run hello.go را بزنید.

نحوه‌ی کار

زمانی که شما یک کد گو می‌نویسید، شما یک بسته‌ی main دارید که در آن تابع main تعریف شده است. شما می‌توانید کدهای مرتبط به هر بخش را در بسته‌ی خاص خود بگذارید.

برای تعریف تابع شما از کلمه‌ی کلیدی func باید استفاده کنید و بعد از آن نام تابع را بنویسید.

با دستور import "fmt" ما می‌توانیم از یک بسته که دارای تابعPrintln هست استفاده کنیم.

چگونه تست کنیم

چگونه جنین چیزی را می‌توان تست کرد؟ خوب است که کدهای «دامنه» را از دنیای بیرون جدا کرد. fmt.Println مربوط به دامنه‌ی شما نیست. اما رشته‌ای که می‌فرستیم جزو دامنه‌ی کد ما هست.

پس می‌توان این دو را از هم جدا کرد.

package main

import "fmt"

func Hello() string {
	return "Hello, world"
}

func main() {
	fmt.Println(Hello())
}

ما دوباره یک تابع جدید ساختیم اما این بار پس از تعریف تابع از کلمه‌ی کلیدی string استفاده کردیم که یعنی وظیفه‌ی تابع بازگرداندن یک رشته است.

حال یک فایل جدید با نام hello_test.go می‌سازیم که در آن تست‌های مربوط به تابغ Hello را می‌نویسیم.

package main

import "testing"

func TestHello(t *testing.T) {
	got := Hello()
	want := "Hello, world"

	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

ماژول‌های گو؟

گام بعدی اجرای تست است. دستور go test را در ترمینال بزنید. اگر تست‌ها پاس شد احتمالا دارید از نسخه‌های قدیمی‌تر گو استفاده می‌کنید. اگر از نسخه‌ی ۱.۱۶ یا جدیدتر استفاده می‌کنید پس احتمالا کد شما اصلا اجرا نمی‌شود، و شما چنین خطابب در ترمینال می‌بینید:

$ go test
go: cannot find main module; see 'go help modules'

مشکل چیست? اگر بخواهیم در یک کلمه مشکل را بگوییم، مشکل, ماژول‌ها هستند. خوشبحتانه راه حل آن ساده است. دستور go mod init hello را در ترمینال وارد کنید. این دستور فایلی با محتوای زیر برای شما تولید می‌کند:

module hello

go 1.16

این فایل به go اطلاعات اساسی مربوط به کد شما را می‌دهد. اگر تصمیم دارید برنامه‌ی خود را به دیگران بدهید، شما هم باید بگویید که کد را از کجا بگیرد، هم اینکه این کد چه نیازمندی‌هایی برای اجرا دارد. در حال حاضر ماژول شما حداقل‌ها را دارد، و فعلا می‌تواند اینگونه باشد. برای آنکه اطلاعات بیشتری درباره‌ی ماژول داشته باشید می‌توانید به مستندات گو در مورد ماژول مراجعه کنید. حالا که تست می‌تواند اجرا شود به سراغ ادامه‌ی کار می‌رویم.

در بخش‌های بعدی بخاطر داشته باشید فبل از دستورات go test و go build باید ابتدا دستور go mod init SOMENAME‍ را بزنید تا ماژول ساخته شود.

بازگشت به تست

حالا اگر go test را بزنید تست شما باید موفق باشد، برای اینکه مطمئن‌تر باشید، می‌توانید رشته‌ی موجود در want را تغییر دهید؛ تست باید خطا بدهد.

اگر متوجه شده باشید ما نیازی به نصب فریمورک برای تست کدمان نداریم، در گو خود زبان امکان تست را برای شما فراهم کرده و با همون دستور زبان گو می‌توان تست نوشت.

نوشتن تست

نوشتن تست مانند نوشتن تابع هست، فقط باید چند شرط را رعایت کرد.

  • نام فایلی که تست در آن نوشته می‌شود باید به شکل xxx_test.go باشد

  • نام تابع تست باید با Test شروع شود

  • تابع تست تنها یک آرگومان می‌گیرد و آن t *testing.T است

  • برای استفاده از *testing.T شما نیاز دارید با زدن import testing, کتابخانه‌ی مربوط به تست را در کد خود ایمپورت کنید.

فعلا کافیست بدانید که t که از نوع *testing.T بود درواقع یک رابط برای صحبت با فریمورک تست هست و می‌توان دستوراتی همچون ()t.Fail را وارد کرد.

ما یک سری دستورات جدید را دیدیم:

دستور if

دستور if در زبان گو مانند همین دستور در دیگر زبان‌ها می‌شد

تعریف متغیر

ما با دستور varName := value یک متغیر جدید تعریف می‌کنیم که به ما اجازه می‌دهد از یک سری مغادیر چندن بار استفاده کنیم تا خوانایی کد بالا برود.

دستور t.Errorf

ما متد Errorf را که برای t تعریف شده صدا می‌زنیم تا به وسیله‌ی آن اعلام کنیم که تست ما موفق نبوده. حرف f از کلمه‌ی فرمت گرفته شده که به ما اجازه می‌دهد یک رشته داشته باشیم که به جای %q مقدار متغیر را در آن بگذاریم، این باعث می‌شود متن خطای ما خوانا باشد، تا در صورت داشتن خطا مشکل را سریع‌تر بفهمیم.

شما می‌توانید در مورد این مدل کار با رشته در مستندات گو بیشتر بخوانید. برای تست استفاده از %q بسیار مناسب است چرا که مقدار متغبر را داخل دابل کوتیشن می‌گذارد.

ما جلوتر به تفاوت تابع و متد می‌پردازیم.

مستندات گو

یکی دیگر از ویژگی‌های ممتاز گو مستندات آن هست شما می‌توانید با زدن دستور godoc -http :8000 مستندات گو را در سیستم خود داشته باشید. اگر به آدرس localhost:8000/pkg بروید شما تمام بسته‌های نصب شده روی سیستم خود را می‌بینید.

بیشتر کتابخانه‌های استاندارد گو مستندات عالی‌ای دارند که همراه مثال هست. بد نیست به آدرس http://localhost:8000/pkg/testing/ بروید و در مستندات مربوط به کتابخانه‌ی تست چرخی بزنید.

اگر در سیستمتان دستور godoc را ندارید احتمالا بخاطر آن است که از نسخه‌های جدیدتر گو استفاده می‌کنید. شما می‌توانید با زدن دستور go install golang.org/x/tools/cmd/godoc@latest برنامه‌ی نشان دادن مستندات را نصب کنید.

سلام بر تو

جالا که تست ما کار می‌کند می‌توانیم خیالمان از بابت امنیت کدمان راحت باشد.

در آخرین مثالمان ما تست را بعد از نوشتن کد اصلی نوشتیم، تا نگاهی داشته باشیم به چگونگی کارکرد تست. از اینجا به بعد ما تست را قبل از نوشتن کد تابع اصلی می‌نویسیم.

در گام بعدی می‌بینیم که چگونه نام شخص را موقع سلام کردن مشخص کنیم.

بیایید ابتدا نیازمندی خود را در تست مشخص کنیم. این یک برنامه‌نویس تست محور مقدماتی هست و به ما اجازه می‌دهد تا مطمئن شویم تست ما واقعا چیزی که ما می‌خواهیم را تست می‌کند. گاهی اوقات هنگام کد نوشتن تست شما حتا اگر با خطا هم مواجه باشد موفق است، در این مواقع شما به درستی تست نمی‌کنید. باید مطمئن شوید که تستتان در صورت وجود مشکل خطا بدهد.

package main

import "testing"

func TestHello(t *testing.T) {
	got := Hello("Chris")
	want := "Hello, Chris"

	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

حال دستور go test را بزنید شما باید خطای زمان کامپایل بگیرید

```text ./hello_test.go:6:18: too many arguments in call to Hello have (string) want () ```

وقتی با یک زبان برنامه‌نویسی ایستا (Statically typed programming languages) مانند گو سروکار دارید، بهتر است به حرف کامپایلر گوش کنید، در این زیان‌ها کامپایلر می‌داند کد چگونه کار می‌کند تا شما مجبور نباشید آن را بدانید.

در اینجا کامپایلر به شما می‌گوید نیاز دارید چه تغییری بدهید تا کد درست کار کند، ما باید تابع Hello را تغییر دهیم تا یک آرگومان بگیرد.

تابع Hello را به شکل زیر تغییر دهید تا یک آرگومان بگیرد.

```go func Hello(name string) string { return "Hello, world" } ```

اگر دوباره تست خود را اجرا کنید خطا می‌گیرید چرا که هنگام صدا زدن تابع Hello باید یک آرگومان هم وارد کنید.

```go func main() { fmt.Println(Hello("world")) } ```

حالا موقع اجرای تست چنین پیامی باید ببینید

```text hello_test.go:10: got 'Hello, world' want 'Hello, Chris'' ```

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

حال وقت آن است که با تغییر تابع Hello کاری کنیم که نتیجه‌ی مقبول ما را بدهد.

```go func Hello(name string) string { return "Hello, " + name } ```

حالا وقتی تست را اجرا کنیم تست با موفقیت پاس می‌شود. براساس چرخه‌ی برنامه‌نویسی تست محور حالا وقت ری‌فکتور می‌باشد.

بادداشتی برای استفاده از سورس کنترل

در این نقطه اگر از یک سورس کنترل استفاده می‌کنید(که باید بکنید) بهتر است کد خود را commit کنید چرا که در این نقطه ما یک نرم‌افزار قابل اجرا داریم که تست هم برای آن نوشته شده.

البته من بنا ندارم کد را به مخزن بفرستم چراکه می‌خواهم آن را ری‌فکتور کنم. ولی بهتر است تغییرات را کامیت کنید که اگر هنگام ری‌فکتور به مشکل خوردید به راحتی به ورژن موفق قبلی برگردید.

چیز خیلی زیادی برای زی‌فکتور کردن نیست اما بد نیست اینجا با مفهوم ثابت‌ها آشنا شوید.

ثابت‌ها

ثابت‌ها مانند زیر تعریف می‌شوند.

const englishHelloPrefix = "Hello, "

حالا ما می‌توانیم کد خود را ری‌فکتور کنیم

const englishHelloPrefix = "Hello, "

func Hello(name string) string {
	return englishHelloPrefix + name
}

بعد از ری‌فکتور کردن یک بار دیگر تست‌ها را اجرا کنید تا مطمئن شوید چیزی خراب نشده باشد.

خوب است کمی درباره‌ی ساختن ثابت فکر کنید تا کاربرد آن در برنامه و حتا پرفورمنس را بهتر درک کنید.

سلام دنیا...دوباره

خوب است در گام بعدی کاری کنیم که اگر یک رشته‌ی خالی به کد دادیم به ما «Hello, World» تحویل دهد نه «Hello, »

کارمان را با نوشتن تست جدید شروع می‌کنیم

func TestHello(t *testing.T) {
	t.Run("saying hello to people", func(t *testing.T) {
		got := Hello("Chris")
		want := "Hello, Chris"

		if got != want {
			t.Errorf("got %q want %q", got, want)
		}
	})
	t.Run("say 'Hello, World' when an empty string is supplied", func(t *testing.T) {
		got := Hello("")
		want := "Hello, World"

		if got != want {
			t.Errorf("got %q want %q", got, want)
		}
	})
}

حالا ما یک مفهوم دیگر را معرفی می‌کنیم، زیر مجموعه‌ها. گاهی حوب است که تست‌هایمان را حول یک چیز گروه‌بندی کنیم و بعد یک زیرمجموعه داشته باشیم که سناریوهای متقاوت از آن چیز را تست کند.

خوبی این روش این است که شما می‌توانید یک بخش از کد را به اشتراک بگذارید تا در دیگر تست‌ها از آن استفاده شود.

حالا بیایید با استفاده از if کاری کنیم تستمان درست کار کند.

const englishHelloPrefix = "Hello, "

func Hello(name string) string {
	if name == "" {
		name = "World"
	}
	return englishHelloPrefix + name
}

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

مهم هست که تست‌های شما به شکل شفاف هر آنچه مورد نیاز هست را تست کرده باشد. اما این موضوع باعث تکرار کردن در کد می‌شود.

ری‌فکتور تنها برای کد اصلی نیست!

جالا که تست‌ها موفق هستند، خوب است که خود تست‌ها را ری‌فکتور کنیم

func TestHello(t *testing.T) {
	t.Run("saying hello to people", func(t *testing.T) {
		got := Hello("Chris")
		want := "Hello, Chris"
		assertCorrectMessage(t, got, want)
	})

	t.Run("empty string defaults to 'world'", func(t *testing.T) {
		got := Hello("")
		want := "Hello, World"
		assertCorrectMessage(t, got, want)
	})

}

func assertCorrectMessage(t testing.TB, got, want string) {
	t.Helper()
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

ما اینجا چه کار کردیم؟

ما بخشی از کد را که چند بار تکرار می‌شد، در یک تابع مجزا گذاشتیم، از این پس هر وقت خواستیم برابری دو رشته برای تست را بسنجیم، این تابع را صدا می‌زنیم.

برای این دست توابع کمکی، خوب است که از testing.TB به عنوان آرگومان استفاده کنیم، این یک اینترفیس است که جای هر دوی *testing.T و *testing.B کار می‌کند و ما هم برای تست و هم برای بنچمارک می‌توانیم از آن استفاده کنیم.(نگران نباشید اگر معنی کلماتی مانند اینترفیس را نمی‌دانید. در آینده به آن‌ها می‌پردازیم).

تابع t.Helper() برای این نیاز است که بگوییم این یک تابع کمکی است. تا در صورت وجود خطا شماره‌ی خط خطا را به گونه‌ای می‌دهد انگار که داخل تابع تست به مشکل خورده است. اگر متوجه نمی‌شوید کاری کنید تست خطا بدهد، و خط t.Helper() را کامنت کنید تا ببینید متن خطا به چه شکل است. کامنت راهی فوق‌العاده برای اضافه کردن اطلاعات بیشتر به کد هست، یا در این مورد خاص حذف کردن یک خط از کد بدون اینکه واقعا کد را حذف کنید. برای این کار دو اسلش به ابتدای خط اضافه کنید، احتمالا رنگ کد در ادیتورتان تغییر کند.

بازگشت به سورس کنترل

حالا ما از نتیجه‌ی تغییراتمان راضی هستیم پس بهتر است همینجا دوباره کد را کامیت کنیم.

نظم

بیاییم یک دور چرحه را مرور کنیم

  • نوشتن تست

  • حذف خطاهای کامپایلر

  • اجرای تست و اطمینان از گرفتن خطا، چک کردن معنادار بودن متن خطا

  • نوشتن کد کافی برای پاس شدن تست

  • ری‌فکتور

شاید به نظر این کارها مسخره باشد، اما تعهد به چرخه بسیار مهم است.

نه تنها باعث می‌شود مطمئن شوید تست‌های مناسبی نوشته‌اید، بلکه کمک می‌کند یک ساختار خوب داشته باشید و با امنیت ری‌فکتور کنید.

اینکه بگذارید تست خطا بدهد مهم است، چرا که می‌بینید متن خطای شما به چه شکلی است. اگر نتوانید خطا را به درستی یفهمید کار شما به عنوان برنامه‌نویس بسیار سخت می‌شود.

وقتی از سرعت تست‌هایتان مطمئن شدید، و ابزارهای مورد نیاز برای اجرای راحت‌تر تست‌ها را آماده کردید. شما به راحتی می‌توانید این چرحه را تکرار کنید.

با ننوشتن تست شما باید به شکل دستی از درستی کدتان مطمئن شوید و این در بلند مدت به شدت از شما زمان می‌برد

##ادامه بدهید! نیازمندی‌های بیشتر

حالا نیازمندی‌های ما بیشتر شده ما نیاز داریم یک آرگومان دیگر را هم دریافت کنیم، ما باید زبانی که با آن به شخص سلام می‌دهیم را هم مشخص کنیم، و اگر زبانی که داده شده را در لیست زبان‌هایمان نداریم به همان انگلیسی سلام کنیم.

ما باید خیالمان راحت باشد که با برنامه‌نویسی تست محور به راحتی و با اطمینان می‌توان این خاصیت را اضافه کرد.

یک تست برای زبان اسپانیایی اضافه می‌کنیم.

```go t.Run("in Spanish", func(t *testing.T) { got := Hello("Elodie", "Spanish") want := "Hola, Elodie" assertCorrectMessage(t, got, want) }) ```

یادتان باشد چرخه را دور نزنید، اول تست می‌نویسیم. وقتی سعی می‌کنید تست را اجرا کنیپ کامپایلر خطا می‌دهد چرا که سعی دارید با دو آرگومان Hello را صدا بزنید.

```text ./hello_test.go:27:19: too many arguments in call to Hello have (string, string) want (string) ```

با اضافه کردن یک آرگومان دیگر به Hello این مشکل را برطرف کنید

```go func Hello(name string, language string) string { if name == "" { name = "World" } return englishHelloPrefix + name } ```

وقتی سعی کنید تست‌ها را اجرا کنید این بار خطا می‌دهد که دیگر تست‌های موجود به اندازه‌ی کافی آرگومان ندارند.

./hello.go:15:19: not enough arguments in call to Hello
    have (string)
    want (string, string)

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

```text hello_test.go:29: got 'Hello, Elodie' want 'Hola, Elodie' ```

ما از یک if استفاده می‌کنیم که اگر زبان اسپانیایی بود کلمه‌ی سلام را به زبان اسپانیایی بنویسد

func Hello(name string, language string) string {
	if name == "" {
		name = "World"
	}

	if language == "Spanish" {
		return "Hola, " + name
	}
	return englishHelloPrefix + name
}

حالا تست‌ها باید پاس شوند.

حالا زمان ری‌فکتور است. شما باید متوجه بعضی مشکلات بشوید، رشته‌های جادویی که بعضی از آن‌ها چندین بار تکرار شده. سعی کنید خودتان کد را ری‌فکتور کنید و مطمئن بشید هر بار پس از ری‌فکتور تست‌ها را اجرا کنید تا چیزی خراب نشده باشد.

	const spanish = "Spanish"
	const englishHelloPrefix = "Hello, "
	const spanishHelloPrefix = "Hola, "

	func Hello(name string, language string) string {
		if name == "" {
			name = "World"
		}

		if language == spanish {
			return spanishHelloPrefix + name
		}
		return englishHelloPrefix + name
	}

فرانسوی

  • یک تست بنویسید که اگر زبان "French" وارد شد برای سلام "Bonjour, " بگیرید

  • ببینید خطا می‌خورد، و مطمئن شوید متن خطا خوانا است

  • کمترین تغییر قابل قبول را در کد بدهید تا تست‌ها پاس شود

احتمالا چیزی که می‌نویسید مانند زیر باشد

func Hello(name string, language string) string {
	if name == "" {
		name = "World"
	}

	if language == spanish {
		return spanishHelloPrefix + name
	}
	if language == french {
		return frenchHelloPrefix + name
	}
	return englishHelloPrefix + name
}

دستور switch

وقتی کلی دستور if دارید معمولا بهتر است به جای آن از دستور switch استفاده کنید. ما می‌توانید کد را به شکلی ری‌فکتور کنیم که از این دستور استفاده شود تا خوانایی کد بیشتر شود و در آینده به راحتی از زبان‌های بیشتری بشود پشتیبانی کرد.

func Hello(name string, language string) string {
	if name == "" {
		name = "World"
	}

	prefix := englishHelloPrefix

	switch language {
	case "French":
		prefix = frenchHelloPrefix
	case "Spanish":
		prefix = spanishHelloPrefix
	}

	return prefix + name
}

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

یک...ری‌فکتور...دیگر؟

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


const (
	french  = "French"
	spanish = "Spanish"

	englishHelloPrefix = "Hello, "
	spanishHelloPrefix = "Hola, "
	frenchHelloPrefix  = "Bonjour, "
)

func Hello(name string, language string) string {
	if name == "" {
		name = "World"
	}

	return greetingPrefix(language) + name
}

func greetingPrefix(language string) (prefix string) {
	switch language {
	case french:
		prefix = frenchHelloPrefix
	case spanish:
		prefix = spanishHelloPrefix
	default:
		prefix = englishHelloPrefix
	}
	return
}

چند مفهوم جدید:

  • در امضای تابع ما برای مقدار بازگشتی اسم مشخص کردیم (prefix string).

  • این کار یک متغیر به اسم prefix در تابع شما ایجاد می‌کند

    • مقدار این متغیر صفر می‌باشد، اینکه صفر چه باشد براساس نوع متغیر مشخص می‌شود مثلا int عدد صفر است و string رشته‌ی خالی

      • شما تنها با نوشتن عبارت return مقدار مورد نظر را برمی‌گردانید و نیازی به نوشتن return prefix نمی‌باشد.

    • در مستندات تابع شما این موضوع نشان داده می‌شود تا کد شما خواناتر باشد.

  • عبارت default وقتی اجرا می‌شود که هیچ‌کدام از گزینه‌های موجود در switch اجرایی نشده باشند.

  • نام این تابع با حروف کوچک شروع شده، در گو اگر بخواهیم تابع را از خارج از بسته‌ی کد صدا بزنیم(به اصطلاع تابع عمومی باشد) نام آن را با حروف بزرگ شروع می‌کنیم و در صورتی که بخواهیم تنها از داخل بسته صدا زده شود نام آن را با حروف کوچک شروع می‌کنیم(تابع خصوصی است). ما نمی‌خواهیم توابع داخلی بسته برای جای دیگر نشان داده شود پس نام آن را با حروف کوچک شروع می‌کنیم.

  • همچنین ما می‌توانید ثابت‌ها را در یک گروه با یک دیگر تعریف کنیم، این کار خوبی هست که ثابت‌های مرتبط با هم یکجا تعریف شوند، تا خوانایی کد بالا برود.

جمع‌بندی

چه کسی فکر می‌کرد که بتوان برای تابع Hello, world این همه کار کرد؟

تا به اینجا شما باید اطلاعات زیر را بدانید:

بخشی از سینتکس گو

  • نوشتن تست

  • تعریف تابع با آرگومان‌ها و مقدار بازگشتی

  • دستورات if، const و switch

  • تعریف متغیر و ثابت

فرایند برنامه‌نویسی تست محور و اهمیت تکرار فرایند

  • نوشتن یک تست که به خطا می‌خورد و دیدن به خطا خوردن آن، تا از یک طرف مطمئن شویم تستی که نوشتیم به تابع ما مرتبط هست و با مشکل داشتن تابع خطا می‌دهد، و از طرفی متن خطا را ببینیم تا مطمئن شویم که متن خطا خوانا است.

  • نوشتن کمترین کد مورد نیاز برای پاس شدن تست تا بدانیم که کد ما کار می‌کند.

  • ری‌فکتور کد با در نظر گرفتن تست‌ها تا از بابت راحتی تغییر مطمئن شویم و بدانیم ساختار کدمان درست تعریف شده.

در موردی که بررسی کردیم ما از تابع Hello() شروع کردیم، و سپس به تابع‌های Hello("name") و Hello("name", "french") رسیدیم و این کار را در گام‌های کوچک و قابل فهم انجام دادیم.

البته این کارها نسبت به مسائل اصلی دنیای واقعی بسیار ساده هست، اما اصول آن یکی است. برنامه‌نویسی تست محور مهارتی هست که یادگیری آن نیاز به تمرین دارد، اما به شکستن مسئله به مسائل کوچک‌تر کار شما برای نوشتن نرم‌افزار خیلی راحت‌تر می‌شود.

Last updated