my clean architecture

foreword    

    The clean arch architecture has been proposed for some time. The earliest origin was in a blog of uncle-bob, but I am afraid that it is no longer accessible. But it doesn't hinder, if you search carefully on the search engine, you can probably find a lot of alternative blog posts.

    This article will also introduce some basic ideas of clean arch, and provide its own implementation scheme.

clean arch

    First look at a basic picture drawn by uncle bob 

           

left    

    At first glance, this picture may be confusing, so let's look at it separately. First look at the left part (the left part of the braces), this picture represents the hierarchical structure of the entire system, which is divided into three layers from bottom to top: data layer, domain layer presentation layer.

    These three layers are single dependencies, the domain layer will depend on the data layer, and the presentation layer will depend on the domain layer. Each of these three layers has its own fine division of labor.

    data layer : responsible for reading and storing data. The data can be local or remote, so the reading and writing of the database and the final sending of network requests will probably be performed here.

    Domain layer : responsible and only responsible for the processing of business logic. He does not care whether the data layer reads data from the database or obtains data from the network. He just requests data from the data layer and waits for the return. After returning the data, process the data according to your own business needs. He also doesn't care how the business he provides is used by the upper layers.

   Presentation layer : This layer is mainly responsible for the data presentation of the view layer and some view layer logic. Use the services provided by the domain layer to complete the final display.

    PS: There is a big problem here, which is the definition of view logic and business logic. Of course, different people have different definitions for using a logic, so the implementation of this part may be different for different people. But personally, there is a design habit, which is to ensure the platform independence of the domain layer. For example, I am developing android, but the domain layer should not have a class that can only be used on android, such as activity.

    Understanding layered logic is the basis of all subsequent analyses, so this part is very important, and it will have a deeper understanding if it is verified with the examples described later.

    

middle   

    Let's look at the part in the middle braces. If you know rxjava, then you may have a deeper understanding of observable and subscriber. But to be honest, the concepts of observable, subscriber and rxjava are still somewhat different, and our clean arch does not require the use of a third-party library such as rxjava.

    The so-called observable is the subscriber, the provider. The subscriber is equivalent to a subscriber, a consumer. For example, the data layer is undoubtedly a provider, and what he does is to provide data.

    For the domain layer, the first is a consumer, why do you say this, because he will get data from the data layer, that is, consume the data. But he is not necessarily a consumer. If some business logic does not require data, then there is no need to request the data layer. However, the domain layer must be a provider, which provides services to the upper layer.

    The presentation layer is of course the consumer, and he needs to obtain business from the domain. If you want to be more serious, then of course presentation is also a provider, like who provides it? Provide to the user, expose an interface to the user, and wait for the user to request it.

    But if you look at the whole process, it can be simplified as shown in the figure.

Right

    The content on the right is very simple, just an arrow, but the content represented by this arrow is still relatively deep, that is, the flow of data. Our data must flow in one direction, from the bottom to the top, which is also in line with the flow architecture model.  

    This flow tells us that we should not request business processing data from the domain layer after obtaining data in the presentation layer. Instead, it should be passed to the upper layer after the domain layer has processed the data. If you have requirements like the above, please consider creating a new business to provide final data directly.

 

example

  You can download a Clean Arch demo     at  https://github.com/zzxzzg/GuardZ/tree/master/CleanArch .

    Open the demo, there are the following three modules

                                                   

    The app is the official demo of Google, and the code has not been modified.

    The first clean arch I came into contact with was this demo, so it is fundamentally instructive. But what I recommend here are the remaining two modules, which is a very small demo written by the author, which deeply integrates rxjava.

    Compared with Google's demo, both in terms of understanding difficulty and coding complexity have been greatly improved. So the next content will revolve around this set of simple codes.

    Suppose we have a business requirement, whether to display the opening page, the rule is that the opening page needs to be displayed when the current version is started for the first time. Let's analyze this business and split it according to the three layers mentioned above.

    The first is the presentation layer, the interface display. There is nothing to say about this. It is to display the opening page or directly display the content.

    And the data layer, do we need data? Yes, we need a SharePreference to record the version number. Although this data is very simple, it is indeed a data that needs to be recorded.

    Of course, there is also the domain layer, which needs to provide two services, save the version number, and compare the version number, and determine whether to display the opening page according to the rules.

data layers

    Let's start with data and look at the code. First, we define a virtual class.

public abstract class IAppinfoRepository extends IRepository {

    public abstract Observable<Integer> getCurrentVersionObservable();

    public abstract Flowable<Integer> getCurrentVersionFlowable();

    public abstract Single<Integer> getCurrentVersionSingle();

    public abstract Maybe<Integer> getCurrentVersionMaybe();

    public abstract Completable setCurrentVersionLauncher(int currentVersion);
}

    It seems to provide a lot of methods, and also inherits an IRepository. First of all, IRepository is actually just what I use to do some public methods or tools that all Repository needs to do. You can also not inherit and directly define IAppinfoRepository as an interface. (The Repository suffix indicates that this is a data provider, which is my naming convention in clean arch.)

    There are many discovery methods, but in fact, it is just the implementation of multiple rxjava types of getCurrentVersion. In fact, we don't need so many, only one of them can complete the task. Added here just to make the code look more foreign. When I actually write the code, I won't implement it all. You see about set, I only implemented one version.

    Then define AppinfoRepository

public class AppinfoRepository extends IAppinfoRepository {
    private IAppinfoRepository preferenceAppinfo;

    public AppinfoRepository() {
        preferenceAppinfo = new PreferenceAppinfo();
    }

    @Override
    public Observable<Integer> getCurrentVersionObservable() {
        return preferenceAppinfo.getCurrentVersionObservable();
    }

    @Override
    public Flowable<Integer> getCurrentVersionFlowable() {
        return preferenceAppinfo.getCurrentVersionFlowable();
    }

    @Override
    public Single<Integer> getCurrentVersionSingle() {
        return preferenceAppinfo.getCurrentVersionSingle();
    }

    @Override
    public Maybe<Integer> getCurrentVersionMaybe() {
        return preferenceAppinfo.getCurrentVersionMaybe();
    }

    @Override
    public Completable setCurrentVersionLauncher(int currentVersion) {
        return preferenceAppinfo.setCurrentVersionLauncher(currentVersion);
    }

}

    This class is equivalent to an encapsulation class, which is finally exposed to the domain layer and provides an interface class. As for whether the specific data is obtained locally, obtained from the network, or if you fake data, it has nothing to do with the domain.

    The final implementation class is PerferenceRepository

public class PreferenceAppinfo extends IAppinfoRepository {
    private SharedPreferences mPreferences;
    public PreferenceAppinfo(){
        mPreferences = SharedPreferencesUtils.getSharedPreferences(
                SharedPreferencesUtils.FileName.NORMAL_PREFERENCE,
                Context.MODE_PRIVATE
        );
    }

    @Override
    public Observable<Integer> getCurrentVersionObservable() {
        Observable<Integer> integerObservable= getObservable(new RepositoryCallback<Integer>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<Integer> e) {
                int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
                e.onNext(version);
                e.onComplete();
            }
        });
        return integerObservable;
    }

    @Override
    public Flowable<Integer> getCurrentVersionFlowable() {
        Flowable<Integer> integerFlowable = Flowable.create(new FlowableOnSubscribe<Integer>() {
            @Override
            public void subscribe(@NonNull FlowableEmitter<Integer> e) throws Exception {
                int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
                e.onNext(version);
                e.onComplete();
            }
        }, BackpressureStrategy.MISSING);
        return integerFlowable;
    }

    @Override
    public Single<Integer> getCurrentVersionSingle() {
        Single<Integer> single = Single.create(new SingleOnSubscribe<Integer>() {
            @Override
            public void subscribe(@NonNull SingleEmitter<Integer> e) throws Exception {
                int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
                e.onSuccess(version);
            }
        });
        return single;
    }

    @Override
    public Maybe<Integer> getCurrentVersionMaybe() {
        Maybe<Integer> maybe = Maybe.create(new MaybeOnSubscribe<Integer>() {
            @Override
            public void subscribe(@NonNull MaybeEmitter<Integer> e) throws Exception {
                int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
                e.onSuccess(version);
            }
        });
        return maybe;
    }

    @Override
    public Completable setCurrentVersionLauncher(final int currentVersion) {
        return Completable.create(new CompletableOnSubscribe() {
            @Override
            public void subscribe(@NonNull CompletableEmitter e) throws Exception {
                mPreferences.edit().putInt(SharedPreferencesUtils.Key.APP_VERSION,currentVersion).commit();
                e.onComplete();
            }
        });
    }
}

    Used to actually store data and read data in the local sharepereference.

domain layer

    The implementation of the domain layer is mainly two case classes. (The Case prefix indicates that this class is used to provide a business function to the upper layer)

public class CaseSetCurrentVersion extends UseCase<CaseSetCurrentVersion.RequestValue,CaseSetCurrentVersion.ResponseValue> {

    private AppinfoRepository mRepository;

    @Override
    @Deprecated
    public Flowable<ResponseValue> asFlowable() {
        throw useCrF();
    }

    @Override
    @Deprecated
    public Observable<ResponseValue> asObservable() {
        throw useCrO();
    }

    @Override
    public Completable asCompletable() {
        mRepository = new AppinfoRepository();
        return mRepository.setCurrentVersionLauncher(getRequestValues().mVersionCode);
    }


    public static final class ResponseValue implements UseCase.ResponseValue{

    }

    public static final class RequestValue implements UseCase.RequestValues{
        public int mVersionCode;

        public RequestValue(int versionCode){
            mVersionCode =  versionCode;
        }
    }
}

    For example, the CaseSetCurrentVersion class provides a business function to the upper layer to save the current version number. The specific implementation is to call the interface provided by AppinfoRepository upwards.

    There is also a CaseLoadPageSwitch to determine whether the current version is greater than the saved version, which is also a business. The implementation principle is the same as CaseSetCurrentVersion.

presentation layer

    Regarding the top layer, we can continue to use other architecture types. For example, in the demo, we continue to use the mvp architecture mode for processing in the presentation, so let's take a look at the MainPresenter corresponding to MainActivity.

public class MainPresenter {
    CaseSetCurrentVersion mCaseSetCurrentVersion;
    CaseLoadPageSwitch mLoadPageSwitch;

    Context mContext;
    public MainPresenter(Contracts.IMainActivity activity,Context context){
        mCaseSetCurrentVersion = new CaseSetCurrentVersion();
        mLoadPageSwitch = new CaseLoadPageSwitch();
        mContext = context;
        launcher();
    }

    public void setCurrentVersion(){
        int versionCode = getVersionCode(mContext);
        CaseSetCurrentVersion.RequestValue requestValue =  new CaseSetCurrentVersion.RequestValue(versionCode);
        mCaseSetCurrentVersion.setRequestValues(requestValue);
        mCaseSetCurrentVersion.asCompletable().subscribeOn(Schedulers.io()).subscribe();
    }


    public void launcher(){
        int versionCode = getVersionCode(mContext);
        CaseLoadPageSwitch.RequestValue requestValue = new CaseLoadPageSwitch.RequestValue(versionCode);
        mLoadPageSwitch.setRequestValues(requestValue);
        mLoadPageSwitch.asObservable().subscribe(new Consumer<CaseLoadPageSwitch.ResponseValue>() {
            @Override
            public void accept(@NonNull CaseLoadPageSwitch.ResponseValue responseValue) throws Exception {
                if(responseValue.isFirstLauncher){
                    setCurrentVersion();
                    //mLauncherView.loadIntroView();
                }else{
                    //mLauncherView.loadMainView();
                }
            }
        });
    }

    public static int getVersionCode(Context context) {
        try {
            PackageManager manager = context.getPackageManager();
            PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
    }
}

    First initialize two cases, and then call CaseLoadPageSwitch to judge the current version and determine whether it is the first start of the current version.

    If it is the first time to start, continue to call the business module that saves the current version, and then call the function of the activity to display the boot page, otherwise directly display the home page content.

    It can be seen that the presenter is only responsible for communicating with the interface layer, and all business processing will be delegated to the case provided in the domain for processing.

Replenish

    The above is basically the design idea of ​​this architecture, but there are some additions.

    1. Regarding the interaction of the two cases, for example, we can modify the business of CaseLoadPageSwitch. This is only used to judge the version. We can extend it to directly store the current version number if it is started for the first time. So I can directly call CaseSetCurrentVersion in CaseLoadPageSwitch to operate.

    2. Here we get the current version number in the presenter instead of putting this function into the case. There are several reasons. First of all, getting the version number is platform-related and does not want to enter the domain (of course, this rule is sometimes It's better to break it, because after all, you are developing an android application, and the blind pursuit of platform independence may confuse your business. Here, this rule is mainly used as a measure of trade-off). In addition, he needs the parameter of the parameter Context, which does not want to be passed everywhere. Once it runs through the entire app, the logic complexity will definitely increase geometrically.

experience feelings

    I tried to use this architecture in an actual project, and I have the following feelings during the actual operation

    1. The clean arch is not friendly in the initial stage of the app, it requires a lot of pre-work, and the new business code will be relatively complex and redundant.

    2. The team members are familiar with the architecture and implement it according to the architecture. After a certain amount of project code is accumulated, the superiority will gradually be displayed, clearer ideas, and more reusable case (business) classes.

    3. In the project maintenance stage, the problem location is intuitive. The addition of new services is simple and hardly affects the original code.

    4. Clean arch cooperates with modular development and rxjava to exert its maximum power.

    In conclusion, clean arch is an architecture that clarifies the system logic by increasing the amount of code. If you desire to write the least code, then clean arch is not suitable for you. If you desire a clean system architecture, then clean arch and his The name is as clean.

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324998838&siteId=291194637