فلاتر

آموزش پیاده سازی معماری تمیز در برنامه نویسی فلاتر Clean Architecture

معماری نرم‌افزار نقش مهمی در موفقیت پروژه‌های نرم‌افزاری دارد.

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

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

اهمیت معماری نرم‌افزار در توسعه نرم‌افزارهای پیچیده و با دامنه‌های ویژه بسیار بزرگ است و در واقع در تمام مراحل عمر نرم‌افزار اثرگذار است.

مزایا استفاده از معماری نرم افزار

  1. ترتیب و ساختار مشخص: معماری نرم‌افزار به تعیین ساختار کلی نرم‌افزار کمک می‌کند. این ساختار کمک می‌کند تا اجزاء مختلف نرم‌افزار به ترتیب و منظمی کار کنند و ارتباطاتشان به‌خوبی تنظیم شود.
  2. قابلیت توسعه‌پذیری: یک معماری خوب، امکان اضافه کردن و توسعه دادن ویژگی‌ها و تغییرات به نرم‌افزار را به طور آسان‌تر می‌کند. اگر معماری به خوبی طراحی شده باشد، افزودن یک ویژگی جدید نباید به تغییرات گسترده در کدها منجر شود.
  3. درک بهتر از نرم‌افزار: معماری نرم‌افزار باعث می‌شود که گروه‌های مختلف توسعه، تست و مدیریت بتوانند بهتر از ساختار کلی نرم‌افزار درک کنند. این باعث می‌شود که تیم‌ها بتوانند به بهترین شکل ممکن کار کنند.
  4. کاهش پیچیدگی: معماری نرم‌افزار می‌تواند به کاهش پیچیدگی کلی نرم‌افزار کمک کند. با تعیین قواعد و الگوهای خاص، تعاملات و ارتباطات بین اجزاء بهینه‌تر خواهند بود.
  5. قابلیت نگهداری و مدیریت: یک معماری مهیا به نگهداری و مدیریت بهتر نرم‌افزار است. این باعث می‌شود تا تغییرات و به‌روزرسانی‌ها به‌راحتی انجام شوند و نرم‌افزار به‌روز نگه داشته شود.
  6. کیفیت نرم‌افزار: معماری مناسب باعث می‌شود که نرم‌افزار با کیفیت و قابلیت اطمینان بالا تولید شود. این باعث می‌شود که مشکلات کمتری در مراحل تست و بهره‌برداری رخ دهد.
  7. تقسیم کار و همکاری: معماری نرم‌افزار به تیم‌ها کمک می‌کند تا کارها را به‌طور موثر تقسیم کنند و به صورت هماهنگ با یکدیگر کار کنند. این باعث می‌شود تا پروژه به بهترین شکل پیش برود.
  8. استفاده از استانداردها: با تعیین معماری مشخص، می‌توان از استانداردها و الگوهای طراحی معمول بهره‌برداری کرد که می‌تواند به کیفیت و قابلیت نگهداری بهتر کمک کند.
  9. کاهش ریسک: معماری نرم‌افزار مناسب می‌تواند به کاهش ریسک‌های مرتبط با توسعه نرم‌افزار و عدم موفقیت کمک کند.

در کل، معماری نرم‌افزار به ایجاد یک توانمندی برای مدیریت پیچیدگی‌ها، بهره‌وری بالا، کاهش هزینه‌ها و ارتقاء کیفیت نرم‌افزار کمک می‌کند و بر اهمیت بلندمدت نرم‌افزار تأثیر مثبت می‌گذارد.

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

بنابراین سرمایه‌گذاری روی معماری نرم‌افزار یک تصمیم استراتژیک برای موفقیت بلندمدت پروژه‌های نرم‌افزاری است.

معماری تمیز در فلاتر Clean Architecture

معماری تمیز یا “Clean Architecture” یک الگوی معماری نرم‌افزاری است که توسط رابرت مارتین (با نام مستعار ددی مارتین) ارائه شده است و به منظور سازماندهی منطق و اجزاء مختلف نرم‌افزار با هدف دستیابی به کد قابل نگهداری، تست‌پذیری و توسعه‌پذیری بهتر ایجاد شده است.

این معماری با تمرکز بر تفکیک منطق کاربری از جزئیات فنی و وابستگی‌ها، باعث ایجاد نرم‌افزارهایی می‌شود که قابلیت انعطاف‌پذیری و تغییرات در آن‌ها بسیار بیشتر است.

لایه منطق کسب‌وکار: شامل منطق کسب‌وکار و قوانین حاکم بر دامنه مسئله است. این لایه کاملاً مستقل از جزئیات پیاده‌سازی است.

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

لایه دسترسی به داده‌ها: مسئولیت آن ذخیره و بازیابی داده‌ها از دیتابیس‌ها و سایر منابع است.

لایه‌های پایین‌تر: شامل جزئیات مربوط به فریمورک‌ها، درایورهای دیتابیس و غیره است.

مزایای معماری تمیز:

  • قابلیت تست بالا: به دلیل جداسازی لایه‌ها، تست واحدها به راحتی امکان‌پذیر است.
  • قابلیت نگهداری بالا: تغییرات در یک لایه تأثیر کمتری بر لایه‌های دیگر دارد.
  • گسترش‌پذیری بالا: افزودن قابلیت‌های جدید آسان است.
  • مستقل از فناوری: بکارگیری فناوری‌های جدید بدون تغییر در منطق کسب‌وکار امکان‌پذیر است.

تفاوت با سایر معماری‌ها:

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

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

در مقایسه با معماری‌های غیرمتمرکز مانند اورکید، معماری تمیز همبستگی قوی‌تری بین منطق کسب‌وکار و پیاده‌سازی دارد.

معماری تمیز برای فناوری‌هایی مانند برنامه‌نویسی فلاتر، توسعه وب، موبایل و غیره بسیار مفید است. زیرا:

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

در مجموع، معماری تمیز با تمرکز بر منطق کسب‌وکار، انعطاف‌پذیری بالاتری را برای حوزه‌های مختلف از جمله برنامه‌نویسی فلاتر فراهم می‌کند و پیاده‌سازی‌ها را برای تغییرات آینده آماده می‌سازد.

استفاده از 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 دریافت می‌کنند.

مزایای تزریق وابستگی:

  • کاهش coupling بین کلاس‌ها
  • آسان شدن تست یکاها چون می‌توان وابستگی‌ها را Mock کرد
  • انعطاف‌پذیری بیشتر در انتخاب پیاده‌سازی‌های مختلف یک Interface
  • تسهیل بازیابی وابستگی‌ها در رانتایم

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

ابتدا در پوشه 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 وابستگی های کل پروژه را تعریف میکنیم.

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

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

ساختار پوشه های پروژه ها ما به این شکل خواهد بود.

پیاده سازی Service

در این کلاس های مربوط به ارتباط با سرور را طراحی میکنیم.

برای شروع کلاسی به نام 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

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

این لایه هم مثل لایه دیگر معماری تمیز از چندین بخش تشکیل شده است.

بخش Data source

این بخش مسئول دریافت اطلاعات مورد نیاز از منابع مختلف میباشد.

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

در این مثال ما از یک وب سرویس استفاده میکنیم.

یک پوشه به نام 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 را بازگردانی میکنیم.

بخش Model

در ادامه در همین لایه پوشه مدل را ایجاد میکنیم و فایل 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

در این لایه پوشه جدیدی به نام 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 میباشد.

لایه Domain

در این لایه ما موارد مختلف را بصورت قراردادی تعریف میکنیم.

یعنی بخش هایی که برای کار کردن برنامه لازم است را به صورت اینترفیس طراحی میکنیم و پیاده سازی اکثر آنها در بخش Data اتفاق میافتد.

بخش Entity

برای شروع پوشه ای به نام Entity در این بخش ایجاد کنید و فایلی تحت عنوان person_entity در آن قرار دهید.

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

class PersonEntity{
  String? name;
  String? city;
  PersonEntity({this.name, this.city});
}

پیش تر گفتیم که دو نوع کلاس مدل داریم و این نوع دوم آن میباشد.

بحش Repository

در لایه قبلی پوشه ای به نام repository داشتیم که در اینجا هم آن را ایجاد میکنیم و فایل fetch_person_repo را در آن قرار میدهیم.

abstract class FetchPersonRepository{
  Future<ApiResponse> FetchPerson();
}

در لایه قبلی پیاده سازی این کلاس را انجام دادیم و در این قسمت فقط مواردی که نیاز به پیاده سازی شدن دارند به بصورت اینترفیس در فلاتر تعریف میکنیم.

بخش Use Case

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

usecase ها عملکردها یا اکشن های مورد نیاز برنامه میباشند. برای مثال در اینجا اکشنی که نیاز داریم ارتباط با سرور برای دریافت اطلاعات میباشد.

usecase به عنوان پل میان دو لایه عمل میکند.

فایلی به نام fetch_person_usecase ایجاد کنید و کدهای زیر را در آن قرار دهید.

class FetchPersonUseCase{
  final FetchPersonRepository fetchPersonRepository = sl<FetchPersonRepository>();
  Future<ApiResponse> FetchPerson(){
    return fetchPersonRepository.FetchPerson();
  }
}

لایه Presentation

این لایه شامل بخش طراحی کاربری و مدیریت 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 را انجام میدهد.

معماری تمیز جزو معماری پیچیده میباشد که برای درک کامل آن نیاز به تمرین دارید.

Hesam

Recent Posts

گیتهاب اکشن چیست؟ آموزش استفاده از گیتهاب اکشن در برنامه نویسی فلاتر

گیتهاب اکشن GitHub Actions یکی از ابزارهای گیتهاب است که به شما کمک می‌کنه تا…

2 هفته ago

آموزش افزایش سرعت اجرای وب اپلیکیشن های فلاتر

اگر یک برنامه نویس فلاتر هستید و با از نسخه وب اپلیکیشن پروژتون استفاده میکنید…

1 ماه ago

آموزش جامع انتشار اپلیکیشن اندروید و فلاتر در فروشگاه گوگل پلی Google play

به عنوان یک برنامه نویس فلاتر یا اندروید بعد از اتمام پروسه طراحی اپلیکیشن نیاز…

2 ماه ago

دانلود سورس کد رابط کاربری اپلیکیشن فلاتر پروژه پادکست

طراحی رابط کاربری اپلیکیشن پادکست خود را با استفاده از این کیت توسعه UI/UX فلاتر…

3 ماه ago

فایربیس چیست؟ معرفی سرویس ابری Firebase و کاربردهای آن

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

3 ماه ago

آموزش پیاده سازی Method Channel در فلاتر + فیلم

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

3 ماه ago