معماری نرمافزار نقش مهمی در موفقیت پروژههای نرمافزاری دارد.
معماری خوب باعث میشود نرمافزار قابلیت گسترش، نگهداری آسان، امنیت و کارایی بهتری داشته باشد.
معماری نرمافزار، اصول، الگوها، ساختارها و تصمیمات مهمی را تعیین میکند که در فرآیند طراحی و توسعه نرمافزار بهکار میروند.
اهمیت معماری نرمافزار در توسعه نرمافزارهای پیچیده و با دامنههای ویژه بسیار بزرگ است و در واقع در تمام مراحل عمر نرمافزار اثرگذار است.
در کل، معماری نرمافزار به ایجاد یک توانمندی برای مدیریت پیچیدگیها، بهرهوری بالا، کاهش هزینهها و ارتقاء کیفیت نرمافزار کمک میکند و بر اهمیت بلندمدت نرمافزار تأثیر مثبت میگذارد.
معمار خبره با در نظر گرفتن الزامات فنی و کسبوکاری، معماری مناسبی طراحی میکند که باعث پیادهسازی و نگهداری آسانتر و هزینه کمتر نرمافزار میشود.
بنابراین سرمایهگذاری روی معماری نرمافزار یک تصمیم استراتژیک برای موفقیت بلندمدت پروژههای نرمافزاری است.
معماری تمیز یا “Clean Architecture” یک الگوی معماری نرمافزاری است که توسط رابرت مارتین (با نام مستعار ددی مارتین) ارائه شده است و به منظور سازماندهی منطق و اجزاء مختلف نرمافزار با هدف دستیابی به کد قابل نگهداری، تستپذیری و توسعهپذیری بهتر ایجاد شده است.
این معماری با تمرکز بر تفکیک منطق کاربری از جزئیات فنی و وابستگیها، باعث ایجاد نرمافزارهایی میشود که قابلیت انعطافپذیری و تغییرات در آنها بسیار بیشتر است.
لایه منطق کسبوکار: شامل منطق کسبوکار و قوانین حاکم بر دامنه مسئله است. این لایه کاملاً مستقل از جزئیات پیادهسازی است.
لایه منطق اپلیکیشن: شامل منطق کاربردی و ارتباط بین دامنههای مختلف است. این لایه از لایههای پایینتر مانند فریمورکها مستقل است.
لایه دسترسی به دادهها: مسئولیت آن ذخیره و بازیابی دادهها از دیتابیسها و سایر منابع است.
لایههای پایینتر: شامل جزئیات مربوط به فریمورکها، درایورهای دیتابیس و غیره است.
مزایای معماری تمیز:
تفاوت با سایر معماریها:
در مقایسه با معماری لایهای، در معماری تمیز تمرکز بیشتری بر منطق کسبوکار وجود دارد. همچنین وابستگی لایهها به یکدیگر کمتر است.
نسبت به معماری میکروسرویسها، معماری تمیز یکپارچگی بیشتری دارد و از تکهتکه شدن بیش از حد جلوگیری میکند.
در مقایسه با معماریهای غیرمتمرکز مانند اورکید، معماری تمیز همبستگی قویتری بین منطق کسبوکار و پیادهسازی دارد.
معماری تمیز برای فناوریهایی مانند برنامهنویسی فلاتر، توسعه وب، موبایل و غیره بسیار مفید است. زیرا:
در مجموع، معماری تمیز با تمرکز بر منطق کسبوکار، انعطافپذیری بالاتری را برای حوزههای مختلف از جمله برنامهنویسی فلاتر فراهم میکند و پیادهسازیها را برای تغییرات آینده آماده میسازد.
بعد از آشنایی با معماری تمیز نوبت به پیاده سازی عملی آن در فریمورک فلاتر رسیده است.
این آموزش برای افراد مبتدی پیشنهاد نمیشود و حتما باید به خوبی آموزش فلاتر را گذرانده باشید.
معماری تمیز شامل یک اصول اولیه است و بعد از آن هر شخص با توجه به این اصول میتواند پیاده سازی های مخصوص به خود را به روش های مختلف داشته باشد.
این معماری شامل کدنویسی های بسیار بیشتری نسبت به سایر معماری ها میباشد.
بنابراین در انتخاب آن دقت کنید به دلیل اینکه مناسب هر نوع پروژه ای نیست و در خیلی از موارد بهتر است از سایر معماری ها استفاده شود.
در ابتدا ساختار پوشه بندی پروژه را مشخص میکنیم.
هر بخشی از برنامه و به عنوان یک قابلیت یا feature در نظر میگیریم. بنابراین هر قسمت از برنامه شامل یک پوشه داخل پوشه feature میباشد.
پوشه core هم شامل کلاس های مشترک برای فیچرهای مختلف میباشد.
در این پروژه از پکیج های مختلفی برای طراحی معماری تمیز استفاده میکنیم.
برای بخش تزریق وابستگی از پکیج get_it کمک میگیریم.
همچنین در بخش مدیریت State از الگوی Bloc و کیوبیت استفاده خواهیم کرد.
بنابراین نیاز دارید تا تمام پکیج های زیر را در پروژه خود اضافه کنید.
flutter_bloc: ^8.1.3
equatable: ^2.0.5
get_it: ^7.6.0
dio: ^5.3.2
تزریق وابستگی (Dependency Injection) یک الگوی طراحی نرمافزار است که به ما امکان میدهد وابستگیهای یک شیء را از خارج تأمین کنیم.
در این الگو، کلاسها به جای اینکه خودشان نمونهای از کلاسهای مورد نیازشان را ایجاد کنند، آنها را از طریق Constructor یا Setter دریافت میکنند.
مزایای تزریق وابستگی:
تزریق وابستگی یک الگوی کلیدی در توسعه نرمافزارهای خوب و انعطافپذیر است و استفاده از آن توصیه میشود.
ابتدا در پوشه core فایل جدیدی به نام services_locator ایجاد میکنیم.
کدهای زیر را در آن قرار میدهیم.
final GetIt sl = GetIt.instance;
void setUpServices(){
sl.registerSingleton<ApiService>(ApiServiceImpl());
sl.registerSingleton<FetchPersonRemoteDS>(FetchPersonRemoteDSImpl());
sl.registerSingleton<FetchPersonRepository>(FetchPersonRepoImpl());
sl.registerSingleton<FetchPersonUseCase>(FetchPersonUseCase());
}
در متد setUpServices وابستگی های کل پروژه را تعریف میکنیم.
این کلاس ها هنوز ساخته نشده اند و به همین دلیل برنامه شامل خطا میباشد.
برای یادگیری کامل این بخش میتوانید به آموزش تزریق وابستگی در فلاتر مراجعه کنید.
ساختار پوشه های پروژه ها ما به این شکل خواهد بود.
در این کلاس های مربوط به ارتباط با سرور را طراحی میکنیم.
برای شروع کلاسی به نام ApiResponse را ایجاد میکنیم.
این کلاس شامل فیلدهای کد سرور, اطلاعاتی که از سرور دریافت میشود و وضعیت میباشد.
class ApiResponse<T> {
Status status;
T? data;
int? server_code;
ApiResponse.initial(this.server_code) : status = Status.INITIAL;
ApiResponse.loading() : status = Status.LOADING;
ApiResponse.completed({this.data,this.server_code}) : status = Status.COMPLETED;
ApiResponse.error({this.data,this.server_code}) : status = Status.ERROR;
@override
String toString() {
return "Status : $status \n ServerCode : $server_code \n Data : $data";
}
}
enum Status { INITIAL, LOADING, COMPLETED, ERROR }
در این قسمت فایل دیگری نیز به نام api_service میسازیم.
در این فایل یک کلاس abstract به نام ApiService ایجاد میکنیم که شامل متدی به نام getPersonList میباشد.
برای هر درخواستی که به یک اندپوینت ارسال میکنیم یک متد در این کلاس میتوانیم ایجاد کنیم.
در همین فایل یا فایل دیگری کلاسی به نام ApiServiceImpl بسازید که وظیفه آن پیاده سازی متدهای کلاس ApiService میباشد.
abstract class ApiService{
Future<Response> getPersonList(String url);
}
class ApiServiceImpl implements ApiService{
@override
Future<Response> getPersonList(String url) async{
var response = await Dio().get(url);
return response;
}
}
در پوشه core یک پوشه به نام Constants ایجاد میکنم تا موارد ثابتی مثل متن ها, رنگ ها, استایل و… را در آن قرار دهیم.
به همین دلیل یک فایل به نام string_const ایجاد میکنیم و آدرس وب سرویس را در آن ثبت میکنیم.
class StringConst{
static String api_url = 'https://mocki.io/v1/d4867d8b-b5d5-4a48-a4ab-79131b5809b8';
}
این لایه وظایفی مهم مثل دریافت اطلاعات را انجام میدهد. در واقع بخشی از منطق برنامه در این قسمت پیاده سازی میشود.
این لایه هم مثل لایه دیگر معماری تمیز از چندین بخش تشکیل شده است.
این بخش مسئول دریافت اطلاعات مورد نیاز از منابع مختلف میباشد.
این منابع میتواند اطلاعاتی باشد که از دیتابیس داخلی فلاتر دریافت میشود و یا یک وب سرویس آنلاین باشد.
در این مثال ما از یک وب سرویس استفاده میکنیم.
یک پوشه به نام data_source میسازیم و فایلی تحت عنوان fetch_person_remote_ds در آن قرار میدهیم.
کدهای این فایل به شکل زیر است.
abstract class FetchPersonRemoteDS{
Future<ApiResponse> fetchPerson();
}
class FetchPersonRemoteDSImpl implements FetchPersonRemoteDS{
final ApiService apiService = sl<ApiService>();
@override
Future<ApiResponse> fetchPerson()async {
// TODO: implement fetchPerson
try{
Response response = await apiService.getPersonList(StringConst.api_url);
if(response.statusCode==200){
Iterable data = response.data;
List<PersonModel> person_list =
data.map((e) => PersonModel.fromjson(e)).toList();
return ApiResponse.completed(data: person_list,server_code: response.statusCode);
}else{
return ApiResponse.error(data: 'Error!!!',server_code: response.statusCode);
}
}on DioException catch(e){
return ApiResponse.error(data: e.response!.data,server_code: e.response!.statusCode);
}
}
}
در این فایل یک کلاسی به نام FetchPersonRemoteDS ایجاد میکنیم که در نقش اینترفیس عمل میکند.
متدهایی که برای دریافت اطلاعات لازم است را درون این کلاس ثبت میکنیم.
در این مثال یک متد به نام fetchPerson داریم که نوع بازگشتی آن هم کلاس ApiResponse است.
در کلاس FetchPersonRemoteDSImpl هم متد مورد نظر را بازنویسی میکنیم.
در اینجا از کلاس ApiService یک نمونه میسازیم تا به متدی که درخواست به سرور را ارسال میکند دسترسی داشته باشیم.
سپس براساس مقدار بازگشتی از سرور آبجکت درست از ApiResponse را بازگردانی میکنیم.
در ادامه در همین لایه پوشه مدل را ایجاد میکنیم و فایل person_model را در آن میسازیم.
در واقع در این معماری برای مدل های خودمون دو نوع کلاس میسازیم یکی در این لایه و دیگری در لایه domain.
کدهای کلاس مدل به شکل زیر هستند.
class PersonModel extends PersonEntity{
String? name;
String? city;
PersonModel({this.name, this.city}): super(city: city,name: name);
factory PersonModel.fromjson(Map<String,dynamic> map){
return PersonModel(
name: map['name'],
city: map['city']
);
}
}
این کلاس از PersonEntity ارث بری میکند که در بخش های بعدی آن را میسازیم.
برای اینکه تغییرات هر لایه روی لایه دیگر تاثیر نگذارد در هر دو لایه یک نوع کلاس مدل داریم.
در این لایه پوشه جدیدی به نام repository میسازیم و فایل fetch_person_repoimpl را در آن قرار میدهیم.
کدهای این فایل بصورت زیر میباشد.
class FetchPersonRepositoryImpl implements FetchPersonRepository{
final FetchPersonRemoteDS fetchPersonRemoteDS = sl<FetchPersonRemoteDS>();
@override
Future<ApiResponse> FetchPerson() async{
var response = await fetchPersonRemoteDS.fetchPerson();
return response;
}
}
این کلاس پیاده سازی کلاس FetchPersonRepository میباشد که وظیفه آن دریافت اطلاعات از منبع اطلاعاتی ما یعنی Data source میباشد.
در این لایه ما موارد مختلف را بصورت قراردادی تعریف میکنیم.
یعنی بخش هایی که برای کار کردن برنامه لازم است را به صورت اینترفیس طراحی میکنیم و پیاده سازی اکثر آنها در بخش Data اتفاق میافتد.
برای شروع پوشه ای به نام Entity در این بخش ایجاد کنید و فایلی تحت عنوان person_entity در آن قرار دهید.
کدهای این فایل بصورت زیر است:
class PersonEntity{
String? name;
String? city;
PersonEntity({this.name, this.city});
}
پیش تر گفتیم که دو نوع کلاس مدل داریم و این نوع دوم آن میباشد.
در لایه قبلی پوشه ای به نام repository داشتیم که در اینجا هم آن را ایجاد میکنیم و فایل fetch_person_repo را در آن قرار میدهیم.
abstract class FetchPersonRepository{
Future<ApiResponse> FetchPerson();
}
در لایه قبلی پیاده سازی این کلاس را انجام دادیم و در این قسمت فقط مواردی که نیاز به پیاده سازی شدن دارند به بصورت اینترفیس در فلاتر تعریف میکنیم.
مورد بعدی که نیاز است ساخت پوشه ای به نام usecase میباشد.
usecase ها عملکردها یا اکشن های مورد نیاز برنامه میباشند. برای مثال در اینجا اکشنی که نیاز داریم ارتباط با سرور برای دریافت اطلاعات میباشد.
usecase به عنوان پل میان دو لایه عمل میکند.
فایلی به نام fetch_person_usecase ایجاد کنید و کدهای زیر را در آن قرار دهید.
class FetchPersonUseCase{
final FetchPersonRepository fetchPersonRepository = sl<FetchPersonRepository>();
Future<ApiResponse> FetchPerson(){
return fetchPersonRepository.FetchPerson();
}
}
این لایه شامل بخش طراحی کاربری و مدیریت State برنامه میباشد.
برای مدیریت State از الگوی Cubit استفاده میکنیم.
به همین دلیل دو کلاس بصورت زیر ایجاد میکنیم.
کلاس اول PersonState میباشد. برای درک بهتر این بخش به آموزش الگوی Bloc در فلاتر مراجعه کنید.
abstract class PersonState{}
class PersonDone extends PersonState {
final List<PersonEntity> person_list;
PersonDone(this.person_list);
}
class PersonInit extends PersonState{}
class PersonLoading extends PersonState{}
class PersonError extends PersonState{
var data;
PersonError(this.data);
}
بخش بعدی طراحی کلاس Cubit میباشد.
class PersonCubit extends Cubit<PersonState>{
PersonCubit():super(PersonInit());
final FetchPersonUseCase _fetchPersonUseCase = sl<FetchPersonUseCase>();
void FetchPersonList() async{
emit(PersonLoading());
ApiResponse apiResponse = await _fetchPersonUseCase.FetchPerson();
if(apiResponse.status == Status.COMPLETED){
emit(PersonDone(apiResponse.data));
}else{
print("api error ${apiResponse.data}");
emit(PersonError(apiResponse.data));
}
}
}
در اینجا با نمونه ای که از کلاس FetchPersonUseCase ایجاد میکنیم عملیات ارسال درخواست به سرور و تغییرات State بر مبنای آن را شروع میکنیم.
در ادامه نیاز به طراحی رابط کاربری داریم.
class PersonPage extends StatefulWidget {
const PersonPage({Key? key}) : super(key: key);
@override
State<PersonPage> createState() => _PersonPageState();
}
class _PersonPageState extends State<PersonPage> {
@override
void initState() {
// TODO: implement initState
super.initState();
context.read<PersonCubit>().FetchPersonList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black12,
appBar: AppBar(
title: Text('Clean Architecture'),
),
body: Container(
child: BlocBuilder<PersonCubit,PersonState>(
builder: (context, state) {
if(state is PersonDone){
return ListView.builder(
itemCount: state.person_list.length,
itemBuilder: (context, index) => PersonItem(state.person_list[index]),
);
}else if(state is PersonError){
return Center(child: Text(state.data),);
}else if( state is PersonLoading)
return Center(child: CircularProgressIndicator(),);
else return Container();
},
),
),
);
}
}
در این صفحه با کمک context.read<PersonCubit>().FetchPersonList(); به کلاس Cubit دسترسی پیدا میکنیم و درخواست دریافت اطلاعات را شروع میکنیم.
با توجه به تغییرات State در ویجت BlocBuilder اطلاعات مختلفی را به کاربر نمایش میدهیم.
اگر اطلاعات به درستی دریافت شود یک لیست ویو وظیفه نمایش اطلاعات را برعهده میگیرد.
کدهای کلاس PersonItem به صورت زیر است.
class PersonItem extends StatelessWidget {
PersonEntity personEntity;
PersonItem(this.personEntity);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
margin: EdgeInsets.symmetric(vertical: 15,horizontal: 10),
padding: EdgeInsets.symmetric(vertical: 15,horizontal: 10),
child: Text("${personEntity.name} from ${personEntity.city}",
style: TextStyle(fontSize: 17,color: Colors.black),),
);
}
}
در آخر نوبت به کدهای صفحه main میرسد که بصورت زیر است.
void main() async{
WidgetsFlutterBinding.ensureInitialized();
setUpSL();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context)=> PersonCubit())
],
child: MaterialApp(
home: PersonPage(),
));
}
}
در اینجا متد setUpSL کار مقداردهی کردن Service Locator را انجام میدهد.
معماری تمیز جزو معماری پیچیده میباشد که برای درک کامل آن نیاز به تمرین دارید.
گیتهاب اکشن GitHub Actions یکی از ابزارهای گیتهاب است که به شما کمک میکنه تا…
اگر یک برنامه نویس فلاتر هستید و با از نسخه وب اپلیکیشن پروژتون استفاده میکنید…
به عنوان یک برنامه نویس فلاتر یا اندروید بعد از اتمام پروسه طراحی اپلیکیشن نیاز…
طراحی رابط کاربری اپلیکیشن پادکست خود را با استفاده از این کیت توسعه UI/UX فلاتر…
فایربیس، پلتفرمی قدرتمند از شرکت گوگل برای توسعه و مدیریت برنامههای موبایل و وب است.…
فلاتر یک فریم ورک برنامه نویسی چندسکویی است که این امکان را برای برنامه نویس…